activerecord-rdb-adapter 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []