activerecord-rdb-adapter 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: eae56962a83ba7f51da156eb041ef6b9961e450d29088e2481b3085c4913d1b8
4
+ data.tar.gz: 38fcc781c4852ef2dd1897d0affebaac9006e038dbb90a67efe4ba9995a4e408
5
+ SHA512:
6
+ metadata.gz: 1f68f8293992979c5f6168746cabed86d7c68290424aa566134db01e40cf3ec9097e53628c05bf71906197ebaeb4f6dec9271a062ba6e9d6b4b6050e00ebd064
7
+ data.tar.gz: c5dc9601b8205894ca3f668f25848643a69b79093b2069d87d9a60e2d80eb889f77e00b465e797ee765fea0d77b8c5eeb5746776017ca0a5d432c153f6b94890
data/README.md ADDED
@@ -0,0 +1,3 @@
1
+ ActiveRecord adapter that allows you to use ActiveRecord 5+(Rails 5.2 is recommended) with [RedDatabase 3+](http://reddatabase.ru/) and Firebird 3
2
+
3
+ inspired by https://github.com/rowland/activerecord-fb-adapter and https://github.com/FabioMR/firebird_adapter
@@ -0,0 +1,35 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ module Rdb
4
+ module DatabaseLimits # :nodoc:
5
+ def table_alias_length
6
+ 31
7
+ end
8
+
9
+ def column_name_length
10
+ 31
11
+ end
12
+
13
+ def table_name_length
14
+ 31
15
+ end
16
+
17
+ def index_name_length
18
+ 31
19
+ end
20
+
21
+ def indexes_per_table
22
+ 65_535
23
+ end
24
+
25
+ def in_clause_length
26
+ 1_499
27
+ end
28
+
29
+ def sql_query_length
30
+ 32_767
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,99 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ module Rdb
4
+ module DatabaseStatements # :nodoc:
5
+ def execute(sql, name = nil)
6
+ log(sql, name) do
7
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
8
+ @connection.query(sql)
9
+ end
10
+ end
11
+ end
12
+
13
+ def exec_query(sql, name = 'SQL', binds = [], prepare: false)
14
+ type_casted_binds = type_casted_binds(binds)
15
+
16
+ log(sql, name, binds, type_casted_binds) do
17
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
18
+ result = @connection.execute(sql, *type_casted_binds)
19
+ if result.is_a?(Fb::Cursor)
20
+ fields = result.fields.map(&:name)
21
+ rows = result.fetchall.map do |row|
22
+ row.map do |col|
23
+ col.encode('UTF-8', @connection.encoding)
24
+ rescue StandardError
25
+ col
26
+ end
27
+ end
28
+ result.close
29
+ ActiveRecord::Result.new(fields, rows)
30
+ else
31
+ result
32
+ end
33
+ end
34
+ end
35
+ rescue StandardError => e
36
+ raise e.message.encode('UTF-8', @connection.encoding)
37
+ end
38
+
39
+ def explain(arel, binds = [])
40
+ to_sql(arel, binds)
41
+ end
42
+
43
+ # Begins the transaction (and turns off auto-committing).
44
+ def begin_db_transaction
45
+ log('begin transaction', nil) do
46
+ begin_isolated_db_transaction(default_transaction_isolation)
47
+ end
48
+ end
49
+
50
+ # Default isolation levels for transactions. This method exists
51
+ # in 4.0.2+, so it's here for backward compatibility with AR 3
52
+ def transaction_isolation_levels
53
+ {
54
+ read_committed: 'READ COMMITTED',
55
+ repeatable_read: 'REPEATABLE READ',
56
+ serializable: 'SERIALIZABLE'
57
+ }
58
+ end
59
+
60
+ # Allows providing the :transaction option to ActiveRecord::Base.transaction
61
+ # in 4.0.2+. Can accept verbatim isolation options like 'WAIT READ COMMITTED'
62
+ def begin_isolated_db_transaction(isolation)
63
+ @connection.transaction transaction_isolation_levels.fetch(isolation, isolation)
64
+ end
65
+
66
+ # Commits the transaction (and turns on auto-committing).
67
+ def commit_db_transaction
68
+ log('commit transaction', nil) { @connection.commit }
69
+ end
70
+
71
+ # Rolls back the transaction (and turns on auto-committing). Must be
72
+ # done if the transaction block raises an exception or returns false.
73
+ def rollback_db_transaction
74
+ log('rollback transaction', nil) { @connection.rollback }
75
+ end
76
+
77
+ def default_sequence_name(table_name, _column = nil)
78
+ "#{table_name.to_s.tr('-', '_')[0, table_name_length - 4]}_seq"
79
+ end
80
+
81
+ # Set the sequence to the max value of the table's column.
82
+ def reset_sequence!(table, column, sequence = nil)
83
+ sequence ||= default_sequence_name(table, column)
84
+ max_id = select_value("select max(#{column}) from #{table}")
85
+ execute("alter sequence #{sequence} restart with #{max_id}")
86
+ end
87
+
88
+ # Uses the raw connection to get the next sequence value.
89
+ def next_sequence_value(sequence_name)
90
+ @connection.query("SELECT NEXT VALUE FOR #{sequence_name} FROM RDB$DATABASE")[0][0]
91
+ end
92
+
93
+ def last_inserted_id(_result)
94
+ nil
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,147 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ module Rdb
4
+ module Quoting # :nodoc:
5
+ QUOTED_FALSE = "'false'".freeze
6
+ QUOTED_TRUE = "'true'".freeze
7
+
8
+ QUOTED_POSITION = '"POSITION"'.freeze
9
+ QUOTED_VALUE = '"VALUE"'.freeze
10
+
11
+ def quote_string(string) # :nodoc:
12
+ string.gsub(/'/, "''")
13
+ end
14
+
15
+ def quoted_date(time)
16
+ if time.is_a?(Time) || time.is_a?(DateTime)
17
+ time.localtime.strftime('%d.%m.%Y %H:%M:%S')
18
+ else
19
+ time.strftime('%d.%m.%Y')
20
+ end
21
+ end
22
+
23
+ def quote_column_name(column_name) # :nodoc:
24
+ column = column_name.dup.to_s
25
+ column.gsub!(/(?<=[^\"\w]|^)position(?=[^\"\w]|$)/i, QUOTED_POSITION)
26
+ column.gsub!(/(?<=[^\"\w]|^)value(?=[^\"\w]|$)/i, QUOTED_VALUE)
27
+ column.delete!('"')
28
+ column.upcase!
29
+ @connection.dialect == 1 ? column.to_s : %("#{column}")
30
+ end
31
+
32
+ def quote_table_name_for_assignment(_table, attr)
33
+ quote_column_name(attr)
34
+ end
35
+
36
+ def unquoted_true
37
+ true
38
+ end
39
+
40
+ def quoted_true # :nodoc:
41
+ QUOTED_TRUE
42
+ end
43
+
44
+ def unquoted_false
45
+ false
46
+ end
47
+
48
+ def quoted_false # :nodoc:
49
+ QUOTED_FALSE
50
+ end
51
+
52
+ def type_cast_from_column(column, value) # :nodoc:
53
+ if column
54
+ type = column.type || lookup_cast_type_from_column(column)
55
+ type.serialize(value)
56
+ else
57
+ value
58
+ end
59
+ end
60
+
61
+ def lookup_cast_type_from_column(column) # :nodoc:
62
+ type = column.try(:sql_type) || column.try(:type)
63
+ lookup_cast_type(type)
64
+ end
65
+
66
+ def type_casted_binds(binds) # :nodoc:
67
+ if binds.first.is_a?(Array)
68
+ binds.map { |column, value| type_cast(value, column) }
69
+ else
70
+ binds.map { |attr| type_cast(attr.value_for_database, attr) }
71
+ end
72
+ end
73
+
74
+ private
75
+
76
+ def id_value_for_database(value)
77
+ if primary_key = value.class.primary_key
78
+ value.instance_variable_get(:@attributes)[primary_key].value_for_database
79
+ end
80
+ end
81
+
82
+ def _quote(value)
83
+ case value
84
+ when Time, DateTime
85
+ "'#{value.strftime('%d.%m.%Y %H:%M')}'"
86
+ when Date
87
+ "'#{value.strftime('%d.%m.%Y')}'"
88
+ else
89
+ super
90
+ end
91
+ end
92
+
93
+ def _type_cast(value)
94
+ case value
95
+ when Symbol, ActiveSupport::Multibyte::Chars, Type::Binary::Data
96
+ value.to_s
97
+ when Array
98
+ value.to_yaml
99
+ when Hash then
100
+ encode_hash(value)
101
+ when true then
102
+ unquoted_true
103
+ when false then
104
+ unquoted_false
105
+ # BigDecimals need to be put in a non-normalized form and quoted.
106
+ when BigDecimal then
107
+ value.to_s('F')
108
+ when Type::Time::Value then
109
+ quoted_time(value)
110
+ when Date, Time, DateTime then
111
+ quoted_date(value)
112
+ when *types_which_need_no_typecasting
113
+ value
114
+ else
115
+ raise TypeError
116
+ end
117
+ end
118
+
119
+ def rdb_to_ar_case(column_name)
120
+ /[[:lower:]]/.match?(column_name) ? column_name : column_name.downcase
121
+ end
122
+
123
+ def ar_to_rdb_case(column_name)
124
+ /[[:upper:]]/.match?(column_name) ? column_name : column_name.upcase
125
+ end
126
+
127
+ def encode_hash(value)
128
+ if value.is_a?(Hash)
129
+ value.to_yaml
130
+ else
131
+ value
132
+ end
133
+ end
134
+
135
+ if defined? Encoding
136
+ def decode(str)
137
+ Base64.decode64(str).force_encoding(@connection.encoding)
138
+ end
139
+ else
140
+ def decode(str)
141
+ Base64.decode64(str)
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,53 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ module Rdb
4
+ class SchemaCreation < AbstractAdapter::SchemaCreation # :nodoc:
5
+
6
+ private
7
+
8
+ def visit_ColumnDefinition(o)
9
+ o.sql_type = type_to_sql(o.type, o.options)
10
+ column_sql = "#{quote_column_name(o.name)} #{o.sql_type}"
11
+ add_column_options!(column_sql, column_options(o)) unless o.type == :primary_key
12
+ column_sql
13
+ end
14
+
15
+ def add_column_options!(sql, options)
16
+ sql << " DEFAULT #{quote_default_expression(options[:default], options[:column])}" if options_include_default?(options)
17
+ # must explicitly check for :null to allow change_column to work on migrations
18
+ if !options[:null]
19
+ sql << " NOT NULL"
20
+ end
21
+ if options[:auto_increment]
22
+ sql << " AUTO_INCREMENT"
23
+ end
24
+ if options[:primary_key]
25
+ sql << " PRIMARY KEY"
26
+ end
27
+ sql
28
+ end
29
+
30
+ def visit_TableDefinition(o)
31
+ create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE #{quote_table_name(o.name)} "
32
+
33
+ statements = o.columns.map(&method(:accept))
34
+ statements << accept(o.primary_keys) if o.primary_keys
35
+
36
+ if supports_indexes_in_create?
37
+ statements.concat(o.indexes.map { |column_name, options| index_in_create(o.name, column_name, options) })
38
+ end
39
+
40
+ if supports_foreign_keys_in_create?
41
+ statements.concat(o.foreign_keys.map { |to_table, options| foreign_key_in_create(o.name, to_table, options) })
42
+ end
43
+
44
+ create_sql << "(#{statements.join(', ')})" if statements.present?
45
+ add_table_options!(create_sql, table_options(o))
46
+ create_sql << " AS #{@conn.to_sql(o.as)}" if o.as
47
+ create_sql
48
+ end
49
+
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,23 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ module Rdb
4
+ class SchemaDumper < ConnectionAdapters::SchemaDumper # :nodoc:
5
+ private
6
+
7
+ def column_spec_for_primary_key(column)
8
+ spec = super
9
+ spec.delete(:auto_increment) if column.type == :integer && column.auto_increment?
10
+ spec
11
+ end
12
+
13
+ def schema_type(column)
14
+ if column.bigint?
15
+ :bigint
16
+ else
17
+ column.type.type
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,425 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ module Rdb
4
+ module SchemaStatements # :nodoc:
5
+ methods_to_commit = %i[add_column
6
+ create_table
7
+ rename_column
8
+ remove_column
9
+ change_column
10
+ change_column_default
11
+ change_column_null
12
+ remove_index
13
+ remove_index!
14
+ drop_table
15
+ create_sequence
16
+ drop_sequence
17
+ drop_trigger]
18
+
19
+ def tables(_name = nil)
20
+ @connection.table_names
21
+ end
22
+
23
+ def views
24
+ @connection.view_names
25
+ end
26
+
27
+ def indexes(table_name, _name = nil)
28
+ @connection.indexes.values.map do |ix|
29
+ IndexDefinition.new(table_name, ix.index_name, ix.unique, ix.columns) if ix.table_name == table_name.to_s && ix.index_name !~ /^rdb\$/
30
+ end.compact
31
+ end
32
+
33
+ def index_name_exists?(table_name, index_name)
34
+ index_name = index_name.to_s.upcase
35
+ indexes(table_name).detect { |i| i.name.upcase == index_name }
36
+ end
37
+
38
+ def columns(table_name, _name = nil)
39
+ @col_definitions ||= {}
40
+ @col_definitions[table_name] = column_definitions(table_name).map do |field|
41
+ sql_type_metadata = column_type_for(field)
42
+ rdb_opt = { domain: field[:domain], sub_type: field[:sql_subtype] }
43
+ RdbColumn.new(field[:name], field[:default], sql_type_metadata, field[:nullable], table_name, rdb_opt)
44
+ end
45
+ end
46
+
47
+ def create_table(name, options = {}) # :nodoc:
48
+ raise ActiveRecordError, 'Firebird does not support temporary tables' if options.key? :temporary
49
+
50
+ raise ActiveRecordError, 'Firebird does not support creating tables with a select' if options.key? :as
51
+
52
+ drop_table name, if_exists: true if options.key? :force
53
+
54
+ needs_sequence = options[:id]
55
+
56
+ super name, options do |table_def|
57
+ yield table_def if block_given?
58
+ needs_sequence ||= table_def.needs_sequence
59
+ end
60
+
61
+ return if options[:sequence] == false || !needs_sequence
62
+ create_sequence(options[:sequence] || default_sequence_name(name))
63
+ trg_sql = <<-END_SQL
64
+ CREATE TRIGGER N$#{name.upcase} FOR #{name.upcase}
65
+ ACTIVE BEFORE INSERT
66
+ AS
67
+ declare variable gen_val bigint;
68
+ BEGIN
69
+ if (new.ID is null) then
70
+ new.ID = next value for #{options[:sequence] || default_sequence_name(name)};
71
+ else begin
72
+ gen_val = gen_id(#{options[:sequence] || default_sequence_name(name)}, 1);
73
+ if (new.ID > gen_val) then
74
+ gen_val = gen_id(#{options[:sequence] || default_sequence_name(name)}, new.ID - gen_val);
75
+ end
76
+ END
77
+ END_SQL
78
+ execute(trg_sql)
79
+ end
80
+
81
+ def drop_table(name, options = {}) # :nodoc:
82
+ drop_sql = "DROP TABLE #{quote_table_name(name)}"
83
+ if options[:if_exists]
84
+ drop = !execute(squish_sql(<<-END_SQL))
85
+ select 1 from rdb$relations where rdb$relation_name = #{quote_table_name(name).tr('"', '\'')}
86
+ END_SQL
87
+ .empty?
88
+ end
89
+
90
+ trigger_name = "N$#{name.upcase}"
91
+ drop_trigger(trigger_name) if trigger_exists?(trigger_name)
92
+
93
+ sequence_name = options[:sequence] || default_sequence_name(name)
94
+ drop_sequence(sequence_name) if sequence_exists?(sequence_name)
95
+
96
+ execute(drop_sql) if drop
97
+ end
98
+
99
+ def create_sequence(sequence_name)
100
+ execute("CREATE SEQUENCE #{sequence_name}")
101
+ rescue StandardError
102
+ nil
103
+ end
104
+
105
+ def drop_sequence(sequence_name)
106
+ execute("DROP SEQUENCE #{sequence_name}")
107
+ rescue StandardError
108
+ nil
109
+ end
110
+
111
+ def drop_trigger(trigger_name)
112
+ execute("DROP TRIGGER #{trigger_name}")
113
+ rescue StandardError
114
+ nil
115
+ end
116
+
117
+ def trigger_exists?(trigger_name)
118
+ !execute(squish_sql(<<-END_SQL))
119
+ select 1
120
+ from rdb$triggers
121
+ where rdb$trigger_name = '#{trigger_name}'
122
+ END_SQL
123
+ .empty?
124
+ end
125
+
126
+ def add_column(table_name, column_name, type, options = {})
127
+ super
128
+
129
+ create_sequence(options[:sequence] || default_sequence_name(table_name)) if type == :primary_key && options[:sequence] != false
130
+
131
+ return unless options[:position]
132
+ # position is 1-based but add 1 to skip id column
133
+ execute(squish_sql(<<-end_sql))
134
+ ALTER TABLE #{quote_table_name(table_name)}
135
+ ALTER COLUMN #{quote_column_name(column_name)}
136
+ POSITION #{options[:position] + 1}
137
+ end_sql
138
+ end
139
+
140
+ def remove_column(table_name, column_name, type = nil, options = {})
141
+ indexes(table_name).each do |i|
142
+ remove_index! i.table, i.name if i.columns.any? { |c| c == column_name.to_s }
143
+ end
144
+
145
+ column_exist = !execute(squish_sql(<<-END_SQL))
146
+ select 1 from RDB$RELATION_FIELDS rf
147
+ where lower(rf.RDB$RELATION_NAME) = '#{table_name.downcase}' and lower(rf.RDB$FIELD_NAME) = '#{column_name.downcase}'
148
+ END_SQL
149
+ .empty?
150
+ super if column_exist
151
+ end
152
+
153
+ def remove_column_for_alter(_table_name, column_name, _type = nil, _options = {})
154
+ "DROP #{quote_column_name(column_name)}"
155
+ end
156
+
157
+ def change_column(table_name, column_name, type, options = {})
158
+ type_sql = type_to_sql(type, *options.values_at(:limit, :precision, :scale))
159
+
160
+ if %i[text string].include?(type)
161
+ copy_column = 'c_temp'
162
+ add_column table_name, copy_column, type, options
163
+ execute(squish_sql(<<-END_SQL))
164
+ UPDATE #{table_name} SET #{copy_column.quote_column_name} = #{column_name.to_s.quote_column_name};
165
+ END_SQL
166
+ remove_column table_name, column_name
167
+ rename_column table_name, copy_column, column_name
168
+ else
169
+ execute(squish_sql(<<-END_SQL))
170
+ ALTER TABLE #{quote_table_name(table_name)}
171
+ ALTER COLUMN #{quote_column_name(column_name)} TYPE #{type_sql}
172
+ END_SQL
173
+ end
174
+ change_column_null(table_name, column_name, !!options[:null]) if options.key?(:null)
175
+ change_column_default(table_name, column_name, options[:default]) if options.key?(:default)
176
+ end
177
+
178
+ def change_column_default(table_name, column_name, default)
179
+ execute(squish_sql(<<-END_SQL))
180
+ ALTER TABLE #{quote_table_name(table_name)}
181
+ ALTER #{quote_column_name(column_name)}
182
+ SET DEFAULT #{quote(default)}
183
+ END_SQL
184
+ end
185
+
186
+ def change_column_null(table_name, column_name, null, default = nil)
187
+ change_column_default(table_name, column_name, default) if default
188
+
189
+ db_column = columns(table_name).find { |c| c.name == column_name.to_s }
190
+ options = { null: null }
191
+ options[:default] = db_column.default if !default && db_column.default
192
+ options[:default] = default if default
193
+ ar_type = db_column.type
194
+ type = type_to_sql(ar_type.type, ar_type.limit, ar_type.precision, ar_type.scale)
195
+
196
+ copy_column = 'c_temp'
197
+ add_column table_name, copy_column, type, options
198
+ execute(squish_sql(<<-END_SQL))
199
+ UPDATE #{table_name} SET #{copy_column.quote_column_name} = #{column_name.to_s.quote_column_name};
200
+ END_SQL
201
+ remove_column table_name, column_name
202
+ rename_column table_name, copy_column, column_name
203
+ end
204
+
205
+ def rename_column(table_name, column_name, new_column_name)
206
+ execute(squish_sql(<<-END_SQL))
207
+ ALTER TABLE #{quote_table_name(table_name)}
208
+ ALTER #{quote_column_name(column_name)}
209
+ TO #{quote_column_name(new_column_name)}
210
+ END_SQL
211
+
212
+ rename_column_indexes(table_name, column_name, new_column_name)
213
+ end
214
+
215
+ def remove_index!(_table_name, index_name)
216
+ execute "DROP INDEX #{quote_column_name(index_name)}"
217
+ end
218
+
219
+ def remove_index(table_name, options = {})
220
+ index_name = index_name(table_name, options)
221
+ execute "DROP INDEX #{quote_column_name(index_name)}"
222
+ end
223
+
224
+ def index_name(table_name, options) #:nodoc:
225
+ if options.respond_to?(:keys) # legacy support
226
+ if options[:column]
227
+ index_name = "#{table_name}_#{Array.wrap(options[:column]) * '_'}"
228
+ if index_name.length > 31
229
+ "IDX_#{Digest::SHA256.hexdigest(index_name)[0..22]}"
230
+ else
231
+ index_name
232
+ end
233
+ elsif options[:name]
234
+ options[:name]
235
+ else
236
+ raise ArgumentError 'You must specify the index name'
237
+ end
238
+ else
239
+ index_name(table_name, column: options)
240
+ end
241
+ end
242
+
243
+ def index_exists?(table_name, column_name, options = {})
244
+ column_names = Array(column_name).map(&:to_s)
245
+ checks = []
246
+ checks << lambda { |i| i.columns == column_names }
247
+ checks << lambda(&:unique) if options[:unique]
248
+ checks << lambda { |i| i.name.upcase == options[:name].to_s.upcase } if options[:name]
249
+
250
+ indexes(table_name).any? { |i| checks.all? { |check| check[i] } }
251
+ end
252
+
253
+ def type_to_sql(type, limit = nil, precision = nil, scale = nil, **args)
254
+ if !args.nil? && !args.empty?
255
+ limit = args[:limit] if limit.nil?
256
+ precision = args[:precision] if precision.nil?
257
+ scale = args[:scale] if scale.nil?
258
+ end
259
+ case type
260
+ when :integer
261
+ integer_to_sql(limit)
262
+ when :float
263
+ float_to_sql(limit)
264
+ when :text
265
+ text_to_sql(limit)
266
+ # when :blob
267
+ # binary_to_sql(limit)
268
+ when :string
269
+ string_to_sql(limit)
270
+ else
271
+ type = type.to_sym if type
272
+ if native = native_database_types[type]
273
+ column_type_sql = (native.is_a?(Hash) ? native[:name] : native).dup
274
+
275
+ if type == :decimal # ignore limit, use precision and scale
276
+ scale ||= native[:scale]
277
+
278
+ if precision ||= native[:precision]
279
+ column_type_sql << if scale
280
+ "(#{precision},#{scale})"
281
+ else
282
+ "(#{precision})"
283
+ end
284
+ elsif scale
285
+ raise ArgumentError, 'Error adding decimal column: precision cannot be empty if scale is specified'
286
+ end
287
+
288
+ elsif %i[datetime timestamp time interval].include?(type) && precision ||= native[:precision]
289
+ if (0..6) === precision
290
+ column_type_sql << "(#{precision})"
291
+ else
292
+ raise(ActiveRecordError, "No #{native[:name]} type has precision of #{precision}. The allowed range of precision is from 0 to 6")
293
+ end
294
+ elsif (type != :primary_key) && (limit ||= native.is_a?(Hash) && native[:limit])
295
+ column_type_sql << "(#{limit})"
296
+ end
297
+
298
+ column_type_sql
299
+ else
300
+ type.to_s
301
+ end
302
+ end
303
+ end
304
+
305
+ def primary_key(table_name)
306
+ row = @connection.query(<<-END_SQL)
307
+ SELECT s.rdb$field_name
308
+ FROM rdb$indices i
309
+ JOIN rdb$index_segments s ON i.rdb$index_name = s.rdb$index_name
310
+ LEFT JOIN rdb$relation_constraints c ON i.rdb$index_name = c.rdb$index_name
311
+ WHERE i.rdb$relation_name = '#{ar_to_rdb_case(table_name)}'
312
+ AND c.rdb$constraint_type = 'PRIMARY KEY';
313
+ END_SQL
314
+
315
+ row.first && rdb_to_ar_case(row.first[0].rstrip)
316
+ end
317
+
318
+ def native_database_types
319
+ @native_database_types ||= initialize_native_database_types.freeze
320
+ end
321
+
322
+ def create_schema_dumper(options)
323
+ Rdb::SchemaDumper.create(self, options)
324
+ end
325
+
326
+ private
327
+
328
+ def column_definitions(table_name)
329
+ @connection.columns(table_name)
330
+ end
331
+
332
+ def new_column_from_field(table_name, field)
333
+ type_metadata = fetch_type_metadata(field['sql_type'])
334
+ ActiveRecord::ConnectionAdapters::Column.new(field['name'], field['default'], type_metadata, field['nullable'], table_name)
335
+ end
336
+
337
+ def column_type_for(field)
338
+ sql_type = RdbColumn.sql_type_for(field)
339
+ type = lookup_cast_type(sql_type)
340
+ { type: type, sql_type: type.type }
341
+ end
342
+
343
+ def integer_to_sql(limit)
344
+ return 'integer' if limit.nil?
345
+ case limit
346
+ when 1..2 then
347
+ 'smallint'
348
+ when 3..4 then
349
+ 'integer'
350
+ when 5..8 then
351
+ 'bigint'
352
+ else
353
+ raise ActiveRecordError "No integer type has byte size #{limit}. " \
354
+ 'Use a NUMERIC with PRECISION 0 instead.'
355
+ end
356
+ end
357
+
358
+ def float_to_sql(limit)
359
+ limit.nil? || limit <= 4 ? 'float' : 'double precision'
360
+ end
361
+
362
+ def text_to_sql(limit)
363
+ if limit && limit > 0
364
+ "VARCHAR(#{limit})"
365
+ else
366
+ 'BLOB SUB_TYPE TEXT'
367
+ end
368
+ end
369
+
370
+ def string_to_sql(limit)
371
+ if limit && limit > 0
372
+ "VARCHAR(#{limit})"
373
+ else
374
+ 'VARCHAR(150)'
375
+ end
376
+ end
377
+
378
+ def initialize_native_database_types
379
+ { primary_key: 'integer not null primary key',
380
+ string: { name: 'varchar', limit: 255 },
381
+ text: { name: 'blob sub_type text' },
382
+ integer: { name: 'integer' },
383
+ bigint: { name: 'bigint' },
384
+ float: { name: 'float' },
385
+ decimal: { name: 'decimal' },
386
+ datetime: { name: 'timestamp' },
387
+ timestamp: { name: 'timestamp' },
388
+ time: { name: 'time' },
389
+ date: { name: 'date' },
390
+ binary: { name: 'blob' },
391
+ boolean: { name: 'boolean' } }
392
+ end
393
+
394
+ def sequence_exists?(sequence_name)
395
+ @connection.generator_names.include?(sequence_name)
396
+ end
397
+
398
+ def create_table_definition(*args)
399
+ Rdb::TableDefinition.new(*args)
400
+ end
401
+
402
+ def squish_sql(sql)
403
+ sql.strip.gsub(/\s+/, ' ')
404
+ end
405
+
406
+ class << self
407
+ def after(*names)
408
+ names.flatten.each do |name|
409
+ m = ActiveRecord::ConnectionAdapters::Rdb::SchemaStatements.instance_method(name)
410
+ define_method(name) do |*args, &block|
411
+ m.bind(self).call(*args, &block)
412
+ yield
413
+ commit_db_transaction
414
+ end
415
+ end
416
+ end
417
+ end
418
+
419
+ after(methods_to_commit) do
420
+ puts 'Commiting transaction'
421
+ end
422
+ end
423
+ end
424
+ end
425
+ end
@@ -0,0 +1,28 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ module Rdb
4
+ module ColumnMethods # :nodoc:
5
+
6
+ attr_accessor :needs_sequence
7
+
8
+ def primary_key(name, type = :primary_key, **options)
9
+ self.needs_sequence = true
10
+ super
11
+ end
12
+
13
+ end
14
+
15
+ class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition # :nodoc:
16
+ include ColumnMethods
17
+
18
+ def new_column_definition(name, type, **options)
19
+ super
20
+ end
21
+ end
22
+
23
+ class Table < ActiveRecord::ConnectionAdapters::Table # :nodoc:
24
+ include ColumnMethods
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,152 @@
1
+ require 'fb'
2
+ require 'base64'
3
+ require 'arel'
4
+ require 'arel/visitors/rdb_visitor'
5
+
6
+ require 'active_record'
7
+ require 'active_record/base'
8
+ require 'active_record/connection_adapters/abstract_adapter'
9
+ require 'active_record/connection_adapters/rdb/database_statements'
10
+ require 'active_record/connection_adapters/rdb/database_limits'
11
+ require 'active_record/connection_adapters/rdb/schema_creation'
12
+ require 'active_record/connection_adapters/rdb/schema_dumper'
13
+ require 'active_record/connection_adapters/rdb/schema_statements'
14
+ require 'active_record/connection_adapters/rdb/quoting'
15
+ require 'active_record/connection_adapters/rdb/table_definition'
16
+ require 'active_record/connection_adapters/rdb_column'
17
+ require 'active_record/rdb_base'
18
+
19
+ module ActiveRecord
20
+ module ConnectionAdapters
21
+ class RdbAdapter < AbstractAdapter # :nodoc:
22
+ include Rdb::DatabaseLimits
23
+ include Rdb::DatabaseStatements
24
+ include Rdb::Quoting
25
+ include Rdb::SchemaStatements
26
+
27
+ @@default_transaction_isolation = :read_committed
28
+ cattr_accessor :default_transaction_isolation
29
+
30
+ ADAPTER_NAME = 'rdb'.freeze
31
+
32
+ def initialize(connection, logger = nil, config = {})
33
+ super(connection, logger, config)
34
+ # Our Responsibility
35
+ @config = config
36
+ @visitor = Arel::Visitors::Rdb.new self
37
+ end
38
+
39
+ def arel_visitor
40
+ Arel::Visitors::Rdb.new self
41
+ end
42
+
43
+ def valid_type?(type)
44
+ !native_database_types[type].nil? || !native_database_types[type.type].nil?
45
+ end
46
+
47
+ def adapter_name
48
+ ADAPTER_NAME
49
+ end
50
+
51
+ def schema_creation
52
+ Rdb::SchemaCreation.new self
53
+ end
54
+
55
+ def supports_migrations?
56
+ true
57
+ end
58
+
59
+ def supports_primary_key?
60
+ true
61
+ end
62
+
63
+ def supports_count_distinct?
64
+ true
65
+ end
66
+
67
+ def supports_ddl_transactions?
68
+ true
69
+ end
70
+
71
+ def supports_transaction_isolation?
72
+ true
73
+ end
74
+
75
+ def supports_savepoints?
76
+ true
77
+ end
78
+
79
+ def prefetch_primary_key?(_table_name = nil)
80
+ true
81
+ end
82
+
83
+ def ids_in_list_limit
84
+ 1499
85
+ end
86
+
87
+ def active?
88
+ return false unless @connection.open?
89
+ # return true if @connection.transaction_started
90
+ @connection.query('SELECT 1 FROM RDB$DATABASE')
91
+ true
92
+ rescue StandardError
93
+ false
94
+ end
95
+
96
+ def reconnect!
97
+ disconnect!
98
+ @connection = ::Fb::Database.connect(@config)
99
+ end
100
+
101
+ def disconnect!
102
+ super
103
+ begin
104
+ @connection.close
105
+ rescue StandardError
106
+ nil
107
+ end
108
+ end
109
+
110
+ def reset!
111
+ reconnect!
112
+ end
113
+
114
+ def requires_reloading?
115
+ false
116
+ end
117
+
118
+ def create_savepoint(name = current_savepoint_name)
119
+ execute("SAVEPOINT #{name}")
120
+ end
121
+
122
+ def rollback_to_savepoint(name = current_savepoint_name)
123
+ execute("ROLLBACK TO SAVEPOINT #{name}")
124
+ end
125
+
126
+ def release_savepoint(name = current_savepoint_name)
127
+ execute("RELEASE SAVEPOINT #{name}")
128
+ end
129
+
130
+ protected
131
+
132
+ def initialize_type_map(map)
133
+ super
134
+ map.register_type(/timestamp/i, Type::DateTime.new)
135
+ map.alias_type(/blob sub_type text/i, 'text')
136
+ end
137
+
138
+ def translate_exception(e, message)
139
+ case e.message
140
+ when /violation of FOREIGN KEY constraint/
141
+ ActiveRecord::InvalidForeignKey.new(message)
142
+ when /violation of PRIMARY or UNIQUE KEY constraint/, /attempt to store duplicate value/
143
+ ActiveRecord::RecordNotUnique.new(message)
144
+ when /This operation is not defined for system tables/
145
+ ActiveRecord::ActiveRecordError.new(message)
146
+ else
147
+ super
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,69 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ class RdbColumn < Column # :nodoc:
4
+ class << self
5
+ def sql_type_for(field)
6
+ sql_type = field[:sql_type]
7
+ sub_type = field[:sql_subtype]
8
+
9
+ sql_type << case sql_type
10
+ when /(numeric|decimal)/i
11
+ "(#{field[:precision]},#{field[:scale].abs})"
12
+ when /(int|float|double|char|varchar|bigint)/i
13
+ "(#{field[:length]})"
14
+ else
15
+ ''
16
+ end
17
+
18
+ sql_type << ' sub_type text' if /blob/i.match?(sql_type) && sub_type == 1
19
+ sql_type
20
+ end
21
+ end
22
+
23
+ attr_reader :sub_type, :domain
24
+
25
+ def initialize(name, default, sql_type_metadata = nil, null = true, table_name = nil, rdb_options = {})
26
+ @domain, @sub_type = rdb_options.values_at(:domain, :sub_type)
27
+ name = name.dup
28
+ name.downcase!
29
+ super(name, parse_default(default), sql_type_metadata, null, table_name)
30
+ end
31
+
32
+ def sql_type
33
+ @sql_type_metadata[:sql_type]
34
+ end
35
+
36
+ def type
37
+ @sql_type_metadata[:type]
38
+ end
39
+
40
+ def precision
41
+ @sql_type_metadata[:precision]
42
+ end
43
+
44
+ def scale
45
+ @sql_type_metadata[:scale]
46
+ end
47
+
48
+ def limit
49
+ @sql_type_metadata[:limit]
50
+ end
51
+
52
+ private
53
+
54
+ def parse_default(default)
55
+ return if default.nil? || /null/i.match?(default)
56
+ d = default.dup
57
+ d.gsub!(/^\s*DEFAULT\s+/i, '')
58
+ d.gsub!(/(^'|'$)/, '')
59
+ d
60
+ end
61
+
62
+ def simplified_type(field_type)
63
+ return :datetime if /timestamp/i.match?(field_type)
64
+ return :text if /blob sub_type text/i.match?(field_type)
65
+ super
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,33 @@
1
+ module ActiveRecord
2
+ module ConnectionHandling # :nodoc:
3
+ def rdb_connection(config)
4
+ require 'fb'
5
+ config = rdb_connection_config(config)
6
+ db = ::Fb::Database.new(config)
7
+ begin
8
+ connection = db.connect
9
+ rescue StandardError
10
+ unless config[:create]
11
+ require 'pp'
12
+ pp config
13
+ raise ConnectionNotEstablished, 'No Firebird connections established.'
14
+ end
15
+ connection = db.create.connect
16
+ end
17
+ ConnectionAdapters::RdbAdapter.new(connection, logger, config)
18
+ end
19
+
20
+ def rdb_connection_config(config)
21
+ config = config.symbolize_keys.dup.reverse_merge(downcase_names: true)
22
+ port = config[:port] || 3050
23
+ raise ArgumentError, 'No database specified. Missing argument: database.' unless config[:database]
24
+ config[:database] = File.expand_path(config[:database], defined?(Rails) && Rails.root) if config[:host].nil? || /localhost/i.match?(config[:host])
25
+ config[:database] = "#{config[:host]}/#{port}:#{config[:database]}" if config[:host]
26
+ # config[:charset] = config[:charset].gsub(/-/, '') if config[:charset]
27
+ # config[:encoding] = config[:encoding].gsub(/-/, '') if config[:encoding]
28
+ config[:page_size] = 8192 unless config[:page_size]
29
+ config[:readonly_selects] = true unless config[:readonly_selects].present?
30
+ config
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,82 @@
1
+ require 'active_record/tasks/database_tasks'
2
+
3
+ module ActiveRecord
4
+ module Tasks
5
+ class RdbDatabaseTasks # :nodoc:
6
+ delegate :rdb_connection_config, :establish_connection, to: ::ActiveRecord::Base
7
+
8
+ def initialize(configuration, root = ::ActiveRecord::Tasks::DatabaseTasks.root)
9
+ @root = root
10
+ @configuration = rdb_connection_config(configuration)
11
+ end
12
+
13
+ def create
14
+ rdb_database.create
15
+ establish_connection configuration
16
+ rescue ::Fb::Error => e
17
+ raise unless e.message.include?('database or file exists')
18
+ raise DatabaseAlreadyExists
19
+ end
20
+
21
+ def drop
22
+ rdb_database.drop
23
+ rescue ::Fb::Error => e
24
+ raise ::ActiveRecord::ConnectionNotEstablished, e.message
25
+ end
26
+
27
+ def purge
28
+ begin
29
+ drop
30
+ rescue StandardError
31
+ nil
32
+ end
33
+ create
34
+ end
35
+
36
+ def structure_dump(filename)
37
+ isql :extract, output: filename
38
+ end
39
+
40
+ def structure_load(filename)
41
+ isql input: filename
42
+ end
43
+
44
+ private
45
+
46
+ def rdb_database
47
+ ::Fb::Database.new(configuration)
48
+ end
49
+
50
+ # Executes isql commands to load/dump the schema.
51
+ # The generated command might look like this:
52
+ # isql db/development.fdb -user SYSDBA -password masterkey -extract
53
+ def isql(*args)
54
+ opts = args.extract_options!
55
+ user, pass = configuration.values_at(:username, :password)
56
+ user ||= configuration[:user]
57
+ opts.reverse_merge!(user: user, password: pass)
58
+ cmd = [isql_executable, configuration[:database]]
59
+ cmd += opts.map { |name, val| "-#{name} #{val}" }
60
+ cmd += args.map { |flag| "-#{flag}" }
61
+ cmd = cmd.join(' ')
62
+ raise "Error running: #{cmd}" unless Kernel.system(cmd)
63
+ end
64
+
65
+ def isql_create(*_args)
66
+ "#{isql_executable} -input "
67
+ end
68
+
69
+ # Finds the isql command line utility from the PATH
70
+ # Many linux distros call this program isql-fb, instead of isql
71
+ def isql_executable
72
+ require 'mkmf'
73
+ exe = %w[isql-fb isql].detect(&method(:find_executable0))
74
+ exe || abort('Unable to find isql or isql-fb in your $PATH')
75
+ end
76
+
77
+ attr_reader :configuration
78
+
79
+ attr_reader :root
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,10 @@
1
+ require 'active_record/connection_adapters/rdb_adapter'
2
+
3
+ if defined?(::Rails::Railtie) && ::ActiveRecord::VERSION::MAJOR > 3
4
+ class Railtie < ::Rails::Railtie # :nodoc:
5
+ rake_tasks do
6
+ load 'active_record/tasks/rdb_database_tasks.rb'
7
+ ActiveRecord::Tasks::DatabaseTasks.register_task(/rdb/, ActiveRecord::Tasks::RdbDatabaseTasks)
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,117 @@
1
+ module Arel
2
+ module Visitors
3
+ class Rdb < Arel::Visitors::ToSql # :nodoc
4
+
5
+ def preparable
6
+ false
7
+ end
8
+
9
+ private
10
+
11
+ def visit_Arel_Nodes_SelectStatement o, collector
12
+ collector << "SELECT "
13
+ collector = visit o.offset, collector if o.offset && !o.limit
14
+
15
+ collector = o.cores.inject(collector) {|c, x|
16
+ visit_Arel_Nodes_SelectCore(x, c)
17
+ }
18
+
19
+ unless o.orders.empty?
20
+ collector << ORDER_BY
21
+ len = o.orders.length - 1
22
+ o.orders.each_with_index {|x, i|
23
+ collector = visit(x, collector)
24
+ collector << COMMA unless len == i
25
+ }
26
+ end
27
+
28
+ if o.limit && o.offset
29
+ collector = limit_with_rows o, collector
30
+ elsif o.limit && !o.offset
31
+ collector = visit o.limit, collector
32
+ end
33
+
34
+ maybe_visit o.lock, collector
35
+ end
36
+
37
+ def visit_Arel_Nodes_SelectCore o, collector
38
+ if o.set_quantifier
39
+ collector = visit o.set_quantifier, collector
40
+ collector << SPACE
41
+ end
42
+
43
+ unless o.projections.empty?
44
+ len = o.projections.length - 1
45
+ o.projections.each_with_index do |x, i|
46
+ collector = visit(x, collector)
47
+ collector << COMMA unless len == i
48
+ end
49
+ end
50
+
51
+ if o.source && !o.source.empty?
52
+ collector << " FROM "
53
+ collector = visit o.source, collector
54
+ end
55
+
56
+ unless o.wheres.empty?
57
+ collector << WHERE
58
+ len = o.wheres.length - 1
59
+ o.wheres.each_with_index do |x, i|
60
+ collector = visit(x, collector)
61
+ collector << AND unless len == i
62
+ end
63
+ end
64
+ unless o.groups.empty?
65
+ collector << GROUP_BY
66
+ len = o.groups.length - 1
67
+ o.groups.each_with_index do |x, i|
68
+ collector = visit(x, collector)
69
+ collector << COMMA unless len == i
70
+ end
71
+ end
72
+
73
+ if Rails::VERSION::MAJOR < 5
74
+ collector = maybe_visit o.having, collector
75
+ else
76
+ unless o.havings.empty?
77
+ collector << " HAVING "
78
+ inject_join o.havings, collector, AND
79
+ end
80
+ end
81
+
82
+ collector
83
+ end
84
+
85
+ def visit_Arel_Nodes_Limit o, collector
86
+ collector << " ROWS "
87
+ visit o.expr, collector
88
+ end
89
+
90
+ def visit_Arel_Nodes_Offset o, collector
91
+ collector << " SKIP "
92
+ visit o.expr, collector
93
+ end
94
+
95
+ def limit_with_rows o, collector
96
+ o.offset.expr.value = ActiveModel::Attribute.with_cast_value("OFFSET".freeze,
97
+ o.offset.expr.value.value + 1,
98
+ ActiveModel::Type.default_value)
99
+ offset = o.offset.expr.value
100
+ o.limit.expr.value = ActiveModel::Attribute.with_cast_value("LIMIT".freeze,
101
+ (o.limit.expr.value.value) + (offset.value - 1),
102
+ ActiveModel::Type.default_value)
103
+ limit = o.limit.expr.value
104
+ collector << " ROWS "
105
+ collector.add_bind(offset) {|i| "?"}
106
+ collector << " TO "
107
+ collector.add_bind(limit) {|i| "?"}
108
+ end
109
+
110
+ def quote_column_name name
111
+ return name if Arel::Nodes::SqlLiteral === name
112
+ @connection.quote_column_name(name)
113
+ end
114
+
115
+ end
116
+ end
117
+ end
metadata ADDED
@@ -0,0 +1,86 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: activerecord-rdb-adapter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.4.1
5
+ platform: ruby
6
+ authors:
7
+ - Andrey Lobanov (RedSoft)
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-03-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: fb
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.9'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.9'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rails
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '5.1'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '5.1'
41
+ description: ActiveRecord Firebird and RedDatabase Adapter for Rails 5+
42
+ email: andrey.lobanov@red-soft.ru
43
+ executables: []
44
+ extensions: []
45
+ extra_rdoc_files: []
46
+ files:
47
+ - README.md
48
+ - lib/active_record/connection_adapters/rdb/database_limits.rb
49
+ - lib/active_record/connection_adapters/rdb/database_statements.rb
50
+ - lib/active_record/connection_adapters/rdb/quoting.rb
51
+ - lib/active_record/connection_adapters/rdb/schema_creation.rb
52
+ - lib/active_record/connection_adapters/rdb/schema_dumper.rb
53
+ - lib/active_record/connection_adapters/rdb/schema_statements.rb
54
+ - lib/active_record/connection_adapters/rdb/table_definition.rb
55
+ - lib/active_record/connection_adapters/rdb_adapter.rb
56
+ - lib/active_record/connection_adapters/rdb_column.rb
57
+ - lib/active_record/rdb_base.rb
58
+ - lib/active_record/tasks/rdb_database_tasks.rb
59
+ - lib/activerecord-rdb-adapter.rb
60
+ - lib/arel/visitors/rdb_visitor.rb
61
+ homepage: http://gitlab.red-soft.biz/andrey.lobanov/activerecord-rdb-adapter
62
+ licenses:
63
+ - MIT
64
+ metadata: {}
65
+ post_install_message:
66
+ rdoc_options: []
67
+ require_paths:
68
+ - lib
69
+ required_ruby_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ required_rubygems_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ requirements:
80
+ - Firebird library fb
81
+ rubyforge_project:
82
+ rubygems_version: 2.7.6
83
+ signing_key:
84
+ specification_version: 4
85
+ summary: ActiveRecord Firebird and RedDatabase Adapter
86
+ test_files: []