rubydb-activerecord 0.1.2 → 0.1.3

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.
@@ -1,766 +1,772 @@
1
- # frozen_string_literal: true
2
-
3
- require "active_record"
4
- require "active_record/connection_adapters/abstract_adapter"
5
- require "active_record/connection_adapters/abstract/schema_definitions"
6
- require "active_record/connection_adapters/abstract/schema_statements"
7
-
8
- require "rubydb"
9
- require "rubydb/rails/adapter"
10
- require "rubydb/rails/connection"
11
- require "rubydb/rails/database_statements"
12
- require "rubydb/rails/schema_statements"
13
- require "rubydb/rails/quoting"
14
- require "rubydb/rails/type"
15
- require "rubydb/rails/result"
16
-
17
- module ActiveRecord
18
- module ConnectionAdapters
19
- # RubyDB adapter for ActiveRecord
20
- class RubyDBAdapter < AbstractAdapter
21
- include RubyDB::Rails::DatabaseStatements
22
- include RubyDB::Rails::SchemaStatements
23
- include RubyDB::Rails::Quoting
24
-
25
- ADAPTER_NAME = "RubyDB"
26
-
27
- # ActiveRecord 7.2 uses adapter-level methods while compiling hash-form
28
- # order clauses (for example, `order(created_at: :desc)`). Keep these
29
- # independent of a live connection so relation construction is safe
30
- # during schema-cache and query setup as well.
31
- def self.quote_table_name(name)
32
- quote_column_name(name)
33
- end
34
-
35
- def self.quote_column_name(name)
36
- "\"#{name.to_s.gsub('"', '""')}\""
37
- end
38
-
39
- NATIVE_DATABASE_TYPES = {
40
- primary_key: "INTEGER PRIMARY KEY AUTOINCREMENT",
41
- string: { name: "VARCHAR", limit: 255 },
42
- text: { name: "TEXT" },
43
- integer: { name: "INTEGER" },
44
- bigint: { name: "BIGINT" },
45
- smallint: { name: "SMALLINT" },
46
- float: { name: "FLOAT" },
47
- decimal: { name: "DECIMAL", precision: 10, scale: 2 },
48
- datetime: { name: "TIMESTAMP" },
49
- timestamp: { name: "TIMESTAMP" },
50
- time: { name: "TIME" },
51
- date: { name: "DATE" },
52
- binary: { name: "BLOB" },
53
- boolean: { name: "BOOLEAN" },
54
- json: { name: "JSON" },
55
- uuid: { name: "UUID" }
56
- }
57
-
58
- # ActiveRecord 7.2 constructs adapters with a single configuration hash.
59
- # Accept trailing deprecated arguments so applications upgrading from older
60
- # ActiveRecord versions do not fail during connection establishment.
61
- def initialize(config, *)
62
- super(config)
63
-
64
- @connection = RubyDB::Rails::Connection.new(config)
65
- @connection.connect
66
-
67
- @prepared_statements = {}
68
- @transaction_depth = 0
69
- @query_cache_enabled = false
70
- @query_cache = {}
71
- @statements = {}
72
- @statement_counter = 0
73
- # AbstractAdapter uses this monitor while creating transactions. It
74
- # must be re-entrant because ActiveRecord acquires it recursively.
75
- @lock = Monitor.new
76
- end
77
-
78
- def adapter_name
79
- ADAPTER_NAME
80
- end
81
-
82
- def supports_migrations?
83
- true
84
- end
85
-
86
- def supports_primary_key?
87
- true
88
- end
89
-
90
- def supports_index_sort_order?
91
- true
92
- end
93
-
94
- def supports_transactions?
95
- true
96
- end
97
-
98
- def supports_savepoints?
99
- true
100
- end
101
-
102
- def supports_foreign_keys?
103
- true
104
- end
105
-
106
- def supports_views?
107
- true
108
- end
109
-
110
- def supports_json?
111
- true
112
- end
113
-
114
- def supports_uuid?
115
- true
116
- end
117
-
118
- def supports_bulk_alter?
119
- false
120
- end
121
-
122
- def native_database_types
123
- NATIVE_DATABASE_TYPES
124
- end
125
-
126
- # ==================== SCHEMA METHODS ====================
127
-
128
- def primary_key(table_name)
129
- return @connection.engine.table_columns(table_name).find(&:primary_key?)&.name&.to_s || "id" if embedded?
130
-
131
- result = execute("PRAGMA table_info(#{quote_table_name(table_name)})")
132
- row = result.find { |r| r["pk"] == 1 }
133
- row ? row["name"] : "id"
134
- end
135
-
136
- def tables
137
- return @connection.engine.list_tables.map(&:to_s) if embedded?
138
-
139
- result = execute("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")
140
- result.map { |row| row["name"] }
141
- end
142
-
143
- def table_exists?(table_name)
144
- tables.include?(table_name.to_s)
145
- end
146
-
147
- # ActiveRecord's schema cache asks for these before a model is first
148
- # instantiated. The embedded engine has catalog metadata already, so
149
- # avoid generating unsupported SQLite catalog queries.
150
- def data_sources
151
- return tables if embedded?
152
-
153
- super
154
- end
155
-
156
- def data_source_exists?(name)
157
- return table_exists?(name) if embedded?
158
-
159
- super
160
- end
161
-
162
- def views
163
- return [] if embedded?
164
-
165
- super
166
- end
167
-
168
- def indexes(table_name)
169
- if embedded?
170
- return @connection.engine.index_manager.get_indexes_for_table(table_name.to_s).map do |index|
171
- ActiveRecord::ConnectionAdapters::IndexDefinition.new(
172
- table_name.to_s,
173
- index.name.to_s,
174
- index.unique,
175
- index.columns.map(&:to_s)
176
- )
177
- end
178
- end
179
-
180
- result = execute("SELECT name, sql FROM sqlite_master WHERE type='index' AND tbl_name=?", [table_name])
181
- result.map do |row|
182
- {
183
- name: row["name"],
184
- columns: parse_index_columns(row["sql"]),
185
- unique: row["sql"].include?("UNIQUE")
186
- }
187
- end
188
- end
189
-
190
- def columns(table_name)
191
- return embedded_columns(table_name) if embedded?
192
-
193
- result = execute("PRAGMA table_info(#{quote_table_name(table_name)})")
194
- result.map do |row|
195
- ActiveRecord::ConnectionAdapters::Column.new(
196
- row["name"],
197
- row["default"],
198
- RubyDB::Rails::Type.to_rails(row["type"]),
199
- {
200
- null: row["notnull"] == 0,
201
- primary_key: row["pk"] == 1,
202
- limit: extract_limit(row["type"])
203
- }
204
- )
205
- end
206
- end
207
-
208
- def column_exists?(table_name, column_name)
209
- columns(table_name).any? { |c| c.name == column_name }
210
- end
211
-
212
- # ==================== QUERY METHODS ====================
213
-
214
- def execute(sql, name = nil)
215
- sql = sql_for_execution(sql)
216
- log(sql, name) do
217
- @connection.execute(sql)
218
- end
219
- end
220
-
221
- def exec_query(sql, name = nil, binds = [])
222
- sql = sql_for_execution(sql)
223
- log(sql, name) do
224
- params = bind_values(binds)
225
- active_record_result(@connection.execute(sql, params))
226
- end
227
- end
228
-
229
- def exec_delete(sql, name = nil, binds = [])
230
- sql = sql_for_execution(sql)
231
- log(sql, name) do
232
- params = bind_values(binds)
233
- result = @connection.execute(sql, params)
234
- result.affected_rows
235
- end
236
- end
237
-
238
- def exec_update(sql, name = nil, binds = [])
239
- sql = sql_for_execution(sql)
240
- log(sql, name) do
241
- params = bind_values(binds)
242
- result = @connection.execute(sql, params)
243
- result.affected_rows
244
- end
245
- end
246
-
247
- def exec_insert(sql, name = nil, binds = [], pk = nil, sequence_name = nil, returning: nil)
248
- sql = sql_for_execution(sql)
249
- log(sql, name) do
250
- params = bind_values(binds)
251
- result = @connection.execute(sql, params)
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record"
4
+ require "active_record/connection_adapters/abstract_adapter"
5
+ require "active_record/connection_adapters/abstract/schema_definitions"
6
+ require "active_record/connection_adapters/abstract/schema_statements"
7
+
8
+ require "rubydb"
9
+ require "rubydb/rails/adapter"
10
+ require "rubydb/rails/connection"
11
+ require "rubydb/rails/database_statements"
12
+ require "rubydb/rails/schema_statements"
13
+ require "rubydb/rails/quoting"
14
+ require "rubydb/rails/type"
15
+ require "rubydb/rails/result"
16
+
17
+ module ActiveRecord
18
+ module ConnectionAdapters
19
+ # RubyDB adapter for ActiveRecord
20
+ class RubyDBAdapter < AbstractAdapter
21
+ include RubyDB::Rails::DatabaseStatements
22
+ include RubyDB::Rails::SchemaStatements
23
+ include RubyDB::Rails::Quoting
24
+
25
+ ADAPTER_NAME = "RubyDB"
26
+
27
+ # ActiveRecord 7.2 uses adapter-level methods while compiling hash-form
28
+ # order clauses (for example, `order(created_at: :desc)`). Keep these
29
+ # independent of a live connection so relation construction is safe
30
+ # during schema-cache and query setup as well.
31
+ def self.quote_table_name(name)
32
+ quote_column_name(name)
33
+ end
34
+
35
+ def self.quote_column_name(name)
36
+ "\"#{name.to_s.gsub('"', '""')}\""
37
+ end
38
+
39
+ NATIVE_DATABASE_TYPES = {
40
+ primary_key: "INTEGER PRIMARY KEY AUTOINCREMENT",
41
+ string: {name: "VARCHAR", limit: 255},
42
+ text: {name: "TEXT"},
43
+ integer: {name: "INTEGER"},
44
+ bigint: {name: "BIGINT"},
45
+ smallint: {name: "SMALLINT"},
46
+ float: {name: "FLOAT"},
47
+ decimal: {name: "DECIMAL", precision: 10, scale: 2},
48
+ datetime: {name: "TIMESTAMP"},
49
+ timestamp: {name: "TIMESTAMP"},
50
+ time: {name: "TIME"},
51
+ date: {name: "DATE"},
52
+ binary: {name: "BLOB"},
53
+ boolean: {name: "BOOLEAN"},
54
+ json: {name: "JSON"},
55
+ uuid: {name: "UUID"}
56
+ }
57
+
58
+ # ActiveRecord 7.2 constructs adapters with a single configuration hash.
59
+ # Accept trailing deprecated arguments so applications upgrading from older
60
+ # ActiveRecord versions do not fail during connection establishment.
61
+ def initialize(config, *)
62
+ super(config)
63
+
64
+ @connection = RubyDB::Rails::Connection.new(config)
65
+ @connection.connect
66
+
67
+ @prepared_statements = {}
68
+ @transaction_depth = 0
69
+ @query_cache_enabled = false
70
+ @query_cache = {}
71
+ @statements = {}
72
+ @statement_counter = 0
73
+ # AbstractAdapter uses this monitor while creating transactions. It
74
+ # must be re-entrant because ActiveRecord acquires it recursively.
75
+ @lock = Monitor.new
76
+ end
77
+
78
+ def adapter_name
79
+ ADAPTER_NAME
80
+ end
81
+
82
+ def supports_migrations?
83
+ true
84
+ end
85
+
86
+ def supports_primary_key?
87
+ true
88
+ end
89
+
90
+ def supports_index_sort_order?
91
+ true
92
+ end
93
+
94
+ def supports_transactions?
95
+ true
96
+ end
97
+
98
+ def supports_savepoints?
99
+ true
100
+ end
101
+
102
+ def supports_foreign_keys?
103
+ true
104
+ end
105
+
106
+ def supports_views?
107
+ true
108
+ end
109
+
110
+ def supports_json?
111
+ true
112
+ end
113
+
114
+ def supports_uuid?
115
+ true
116
+ end
117
+
118
+ def supports_bulk_alter?
119
+ false
120
+ end
121
+
122
+ def native_database_types
123
+ NATIVE_DATABASE_TYPES
124
+ end
125
+
126
+ # ==================== SCHEMA METHODS ====================
127
+
128
+ def primary_key(table_name)
129
+ return @connection.engine.table_columns(table_name).find(&:primary_key?)&.name&.to_s || "id" if embedded?
130
+
131
+ remote_metadata(table_name)[:columns].find do |column|
132
+ column[:primary_key] || column["primary_key"]
133
+ end&.then { |column| (column[:name] || column["name"]).to_s } || "id"
134
+ end
135
+
136
+ def tables
137
+ return @connection.engine.list_tables.map(&:to_s) if embedded?
138
+
139
+ Array(remote_metadata[:tables]).map(&:to_s)
140
+ end
141
+
142
+ def table_exists?(table_name)
143
+ tables.include?(table_name.to_s)
144
+ end
145
+
146
+ # ActiveRecord's schema cache asks for these before a model is first
147
+ # instantiated. The embedded engine has catalog metadata already, so
148
+ # avoid generating unsupported SQLite catalog queries.
149
+ def data_sources
150
+ tables
151
+ end
152
+
153
+ def data_source_exists?(name)
154
+ table_exists?(name)
155
+ end
156
+
157
+ def views
158
+ []
159
+ end
160
+
161
+ def indexes(table_name)
162
+ if embedded?
163
+ return @connection.engine.index_manager.get_indexes_for_table(table_name.to_s).map do |index|
164
+ ActiveRecord::ConnectionAdapters::IndexDefinition.new(
165
+ table_name.to_s,
166
+ index.name.to_s,
167
+ index.unique,
168
+ index.columns.map(&:to_s)
169
+ )
170
+ end
171
+ end
172
+
173
+ Array(remote_metadata(table_name)[:indexes]).map do |index|
174
+ ActiveRecord::ConnectionAdapters::IndexDefinition.new(
175
+ table_name.to_s,
176
+ index[:name] || index["name"],
177
+ index[:unique] || index["unique"],
178
+ index[:columns] || index["columns"] || []
179
+ )
180
+ end
181
+ end
182
+
183
+ def columns(table_name)
184
+ return embedded_columns(table_name) if embedded?
185
+
186
+ remote_columns(table_name)
187
+ end
188
+
189
+ def remote_columns(table_name)
190
+ Array(remote_metadata(table_name)[:columns]).map do |row|
191
+ type = row[:type] || row["type"]
192
+ nullable = row.key?(:nullable) ? row[:nullable] : row["nullable"]
193
+ ActiveRecord::ConnectionAdapters::Column.new(
194
+ row[:name] || row["name"],
195
+ rails_default_value(row.key?(:default) ? row[:default] : row["default"]),
196
+ ActiveRecord::ConnectionAdapters::SqlTypeMetadata.new(
197
+ sql_type: type.to_s.upcase,
198
+ type: rails_type_for(type),
199
+ limit: extract_limit(type.to_s)
200
+ ),
201
+ nullable
202
+ )
203
+ end
204
+ end
205
+
206
+ def column_exists?(table_name, column_name)
207
+ columns(table_name).any? { |c| c.name == column_name }
208
+ end
209
+
210
+ # ==================== QUERY METHODS ====================
211
+
212
+ def execute(sql, name = nil)
213
+ sql = sql_for_execution(sql)
214
+ log(sql, name) do
215
+ @connection.execute(sql)
216
+ end
217
+ end
218
+
219
+ def exec_query(sql, name = nil, binds = [])
220
+ sql = sql_for_execution(sql)
221
+ log(sql, name) do
222
+ params = bind_values(binds)
223
+ active_record_result(@connection.execute(sql, params))
224
+ end
225
+ end
226
+
227
+ def exec_delete(sql, name = nil, binds = [])
228
+ sql = sql_for_execution(sql)
229
+ log(sql, name) do
230
+ params = bind_values(binds)
231
+ result = @connection.execute(sql, params)
232
+ result.affected_rows
233
+ end
234
+ end
235
+
236
+ def exec_update(sql, name = nil, binds = [])
237
+ sql = sql_for_execution(sql)
238
+ log(sql, name) do
239
+ params = bind_values(binds)
240
+ result = @connection.execute(sql, params)
241
+ result.affected_rows
242
+ end
243
+ end
244
+
245
+ def exec_insert(sql, name = nil, binds = [], pk = nil, sequence_name = nil, returning: nil)
246
+ sql = sql_for_execution(sql)
247
+ log(sql, name) do
248
+ params = bind_values(binds)
249
+ result = @connection.execute(sql, params)
252
250
  # RubyDB keeps a physical row id for storage operations and a
253
251
  # logical primary-key value for SQL/ActiveRecord. They can differ
254
252
  # after deletes, so ActiveRecord must receive the logical id.
255
253
  id = result.inserted_id || result.row_id
256
- ActiveRecord::Result.new([pk || "id"], id.nil? ? [] : [[id]])
257
- end
258
- end
259
-
260
- def insert(arel, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = [], returning: nil)
261
- sql, binds = to_sql_and_binds(arel, binds)
262
- result = exec_insert(sql, name, binds, pk, sequence_name, returning: returning)
263
- return returning_column_values(result) unless returning.nil?
264
-
265
- id_value || last_inserted_id(result)
266
- end
267
-
268
- def update(arel, name = nil, binds = [])
269
- sql, binds = to_sql_and_binds(arel, binds)
270
- exec_update(sql, name, binds)
271
- end
272
-
273
- def delete(arel, name = nil, binds = [])
274
- sql, binds = to_sql_and_binds(arel, binds)
275
- exec_delete(sql, name, binds)
276
- end
277
-
278
- def select_all(sql, name = nil, binds = [], preparable: nil, async: false, allow_retry: false)
279
- sql, binds = to_sql_and_binds(sql, binds)
280
- exec_query(sql, name, binds)
281
- end
282
-
283
- def select_one(sql, name = nil, binds = [])
284
- sql, binds = to_sql_and_binds(sql, binds)
285
- result = exec_query(sql, name, binds)
286
- result.first
287
- end
288
-
289
- def select_value(sql, name = nil, binds = [])
290
- sql, binds = to_sql_and_binds(sql, binds)
291
- result = exec_query(sql, name, binds)
292
- result.first&.values&.first
293
- end
294
-
295
- def select_values(sql, name = nil, binds = [])
296
- sql, binds = to_sql_and_binds(sql, binds)
297
- result = exec_query(sql, name, binds)
298
- result.map { |row| row.values.first }
299
- end
300
-
301
- def select_rows(sql, name = nil, binds = [])
302
- sql, binds = to_sql_and_binds(sql, binds)
303
- result = exec_query(sql, name, binds)
304
- result.map { |row| row.values }
305
- end
306
-
307
- # ==================== TRANSACTION METHODS ====================
308
-
309
- def begin_db_transaction
310
- @transaction_depth += 1
311
- @connection.begin_db_transaction if @transaction_depth == 1
312
- end
313
-
314
- def commit_db_transaction
315
- return if @transaction_depth <= 0
316
-
317
- @transaction_depth -= 1
318
- @connection.commit_db_transaction if @transaction_depth == 0
319
- end
320
-
321
- def rollback_db_transaction
322
- return if @transaction_depth <= 0
323
-
324
- @transaction_depth -= 1
325
- @connection.rollback_db_transaction if @transaction_depth == 0
326
- @transaction_depth = 0 if @transaction_depth < 0
327
- end
328
-
329
- def create_savepoint(name)
330
- execute("SAVEPOINT #{name}")
331
- end
332
-
333
- def rollback_to_savepoint(name)
334
- execute("ROLLBACK TO SAVEPOINT #{name}")
335
- end
336
-
337
- def release_savepoint(name)
338
- execute("RELEASE SAVEPOINT #{name}")
339
- end
340
-
341
- def in_transaction?
342
- @transaction_depth > 0
343
- end
344
-
345
- def transaction_joinable?
346
- true
347
- end
348
-
349
- def transactional?
350
- true
351
- end
352
-
353
- # ==================== SCHEMA STATEMENT METHODS ====================
354
-
355
- def create_table(table_name, **options, &block)
356
- RubyDB::Rails::SchemaStatements.instance_method(:create_table).bind_call(self, table_name, options, &block)
357
- end
358
-
359
- def drop_table(table_name, **options)
360
- sql = +"DROP TABLE"
361
- sql << " IF EXISTS" if options[:if_exists]
362
- sql << " #{quote_table_name(table_name)}"
363
- sql << " CASCADE" if options[:cascade]
364
- execute(sql)
365
- end
366
-
367
- def add_column(table_name, column_name, type, **options)
368
- sql = "ALTER TABLE #{quote_table_name(table_name)}"
369
- sql << " ADD COLUMN #{quote_column_name(column_name)} #{type_to_sql(type, options)}"
370
-
371
- if options[:null] == false
372
- sql << " NOT NULL"
373
- end
374
-
375
- if options[:default]
376
- sql << " DEFAULT #{quote_default(options[:default])}"
377
- end
378
-
379
- if options[:primary_key]
380
- sql << " PRIMARY KEY"
381
- end
382
-
383
- execute(sql)
384
- end
385
-
386
- def remove_column(table_name, column_name, type = nil, **options)
387
- sql = "ALTER TABLE #{quote_table_name(table_name)}"
388
- sql << " DROP COLUMN #{quote_column_name(column_name)}"
389
- sql << " CASCADE" if options[:cascade]
390
- execute(sql)
391
- end
392
-
393
- def change_column(table_name, column_name, type, **options)
394
- # Change column type
395
- sql = "ALTER TABLE #{quote_table_name(table_name)}"
396
- sql << " ALTER COLUMN #{quote_column_name(column_name)}"
397
- sql << " TYPE #{type_to_sql(type, options)}"
398
- execute(sql)
399
-
400
- # Change nullability
401
- if options.key?(:null)
402
- sql = "ALTER TABLE #{quote_table_name(table_name)}"
403
- sql << " ALTER COLUMN #{quote_column_name(column_name)}"
404
- sql << (options[:null] ? " DROP" : " SET") + " NOT NULL"
405
- execute(sql)
406
- end
407
-
408
- # Change default
409
- if options.key?(:default)
410
- sql = "ALTER TABLE #{quote_table_name(table_name)}"
411
- sql << " ALTER COLUMN #{quote_column_name(column_name)}"
412
- if options[:default].nil?
413
- sql << " DROP DEFAULT"
414
- else
415
- sql << " SET DEFAULT #{quote_default(options[:default])}"
416
- end
417
- execute(sql)
418
- end
419
- end
420
-
421
- def rename_column(table_name, column_name, new_column_name)
422
- sql = "ALTER TABLE #{quote_table_name(table_name)}"
423
- sql << " RENAME COLUMN #{quote_column_name(column_name)}"
424
- sql << " TO #{quote_column_name(new_column_name)}"
425
- execute(sql)
426
- end
427
-
428
- def rename_table(old_name, new_name)
429
- sql = "ALTER TABLE #{quote_table_name(old_name)}"
430
- sql << " RENAME TO #{quote_table_name(new_name)}"
431
- execute(sql)
432
- end
433
-
434
- def add_index(table_name, column_name, **options)
435
- index_name = options[:name] || "idx_#{table_name}_#{Array(column_name).join('_')}"
436
- sql = +"CREATE"
437
- sql << " UNIQUE" if options[:unique]
438
- sql << " INDEX #{quote_column_name(index_name)}"
439
- sql << " ON #{quote_table_name(table_name)}"
440
- sql << " (#{Array(column_name).map { |c| quote_column_name(c) }.join(', ')})"
441
- sql << " WHERE #{options[:where]}" if options[:where]
442
- execute(sql)
443
- end
444
-
445
- def remove_index(table_name, column_name = nil, **options)
446
- index_name = options[:name]
447
- if index_name.nil?
448
- column_name ||= options[:column] || options[:columns]
449
- index_name = "idx_#{table_name}_#{Array(column_name).join('_')}"
450
- end
451
-
452
- sql = "DROP INDEX #{quote_column_name(index_name)}"
453
- execute(sql)
454
- end
455
-
456
- def add_foreign_key(from_table, to_table, **options)
457
- fk_name = options[:name] || "fk_#{from_table}_to_#{to_table}"
458
- sql = "ALTER TABLE #{quote_table_name(from_table)}"
459
- sql << " ADD CONSTRAINT #{quote_column_name(fk_name)}"
460
- sql << " FOREIGN KEY (#{quote_column_name(options[:column] || :id)})"
461
- sql << " REFERENCES #{quote_table_name(to_table)}"
462
- sql << " (#{quote_column_name(options[:primary_key] || :id)})"
463
- sql << " ON DELETE #{options[:on_delete]}" if options[:on_delete]
464
- sql << " ON UPDATE #{options[:on_update]}" if options[:on_update]
465
- execute(sql)
466
- end
467
-
468
- def foreign_keys(table_name)
469
- return super unless embedded?
470
-
471
- constraints = @connection.engine.table_metadata[table_name.to_s]&.fetch(:constraints, []) || []
472
- constraints.filter_map do |constraint|
473
- type = constraint[:type] || constraint["type"]
474
- next unless type.to_s.upcase == "FOREIGN_KEY"
475
-
476
- columns = constraint[:columns] || constraint["columns"] || []
477
- reference_table = constraint[:reference_table] || constraint["reference_table"]
478
- reference_columns = constraint[:reference_columns] || constraint["reference_columns"] || ["id"]
479
- options = {
480
- column: Array(columns).first.to_s,
481
- primary_key: Array(reference_columns).first.to_s,
482
- name: constraint[:name] || constraint["name"]
483
- }
484
- options[:on_delete] = (constraint[:on_delete] || constraint["on_delete"]).to_s if constraint[:on_delete] || constraint["on_delete"]
485
- options[:on_update] = (constraint[:on_update] || constraint["on_update"]).to_s if constraint[:on_update] || constraint["on_update"]
486
- ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(table_name.to_s, reference_table.to_s, options)
487
- end
488
- end
489
-
490
- def remove_foreign_key(from_table, **options)
491
- fk_name = options[:name] || "fk_#{from_table}_to_#{options[:to_table]}"
492
- sql = "ALTER TABLE #{quote_table_name(from_table)}"
493
- sql << " DROP CONSTRAINT #{quote_column_name(fk_name)}"
494
- execute(sql)
495
- end
496
-
497
- def add_timestamps(table_name, **options)
498
- add_column(table_name, :created_at, :datetime, options)
499
- add_column(table_name, :updated_at, :datetime, options)
500
- end
501
-
502
- def remove_timestamps(table_name, **options)
503
- remove_column(table_name, :updated_at, options)
504
- remove_column(table_name, :created_at, options)
505
- end
506
-
507
- def change_column_null(table_name, column_name, null, default = nil)
508
- if default
509
- sql = "UPDATE #{quote_table_name(table_name)}"
510
- sql << " SET #{quote_column_name(column_name)} = #{quote(default)}"
511
- sql << " WHERE #{quote_column_name(column_name)} IS NULL"
512
- execute(sql)
513
- end
514
-
515
- sql = "ALTER TABLE #{quote_table_name(table_name)}"
516
- sql << " ALTER COLUMN #{quote_column_name(column_name)}"
517
- sql << (null ? " DROP" : " SET") + " NOT NULL"
518
- execute(sql)
519
- end
520
-
521
- def change_column_default(table_name, column_name, default)
522
- sql = "ALTER TABLE #{quote_table_name(table_name)}"
523
- sql << " ALTER COLUMN #{quote_column_name(column_name)}"
524
- if default.nil?
525
- sql << " DROP DEFAULT"
526
- else
527
- sql << " SET DEFAULT #{quote_default(default)}"
528
- end
529
- execute(sql)
530
- end
531
-
532
- # ==================== QUOTING METHODS ====================
533
-
534
- def quote(value, column = nil)
535
- @connection.quote(value, column)
536
- end
537
-
538
- def quote_table_name(name)
539
- @connection.quote_table_name(name)
540
- end
541
-
542
- def quote_column_name(name)
543
- @connection.quote_column_name(name)
544
- end
545
-
546
- def quote_default(value)
547
- quote(value)
548
- end
549
-
550
- # ==================== TYPE CASTING ====================
551
-
552
- def type_cast(value, type)
553
- RubyDB::Rails::Type.serialize(value, type)
554
- end
555
-
556
- def type_cast_from_database(value, type)
557
- RubyDB::Rails::Type.deserialize(value, type)
558
- end
559
-
560
- # ==================== SCHEMA VERSION ====================
561
-
562
- def schema_version
563
- result = execute("SELECT version FROM schema_migrations ORDER BY version DESC LIMIT 1")
564
- result.first ? result.first["version"] : nil
565
- end
566
-
567
- def schema_migrations
568
- result = execute("SELECT version FROM schema_migrations ORDER BY version")
569
- result.map { |row| row["version"] }
570
- end
571
-
572
- def dump_schema
573
- schema = +""
574
- tables.each do |table|
575
- table_columns = columns(table)
576
- primary_key_name = if embedded?
577
- @connection.engine.table_columns(table).find(&:primary_key?)&.name
578
- else
579
- primary_key(table)
580
- end
581
- primary_key = table_columns.find { |column| column.name.to_s == primary_key_name.to_s } if primary_key_name
582
- automatic_id = primary_key && primary_key_name.to_s == "id" && primary_key.type.to_sym == :integer
583
- table_options = automatic_id ? "" : ", id: false"
584
- schema << "create_table \"#{table}\"#{table_options} do |t|\n"
585
- table_columns.each do |col|
586
- next if automatic_id && primary_key && col.name.to_s == primary_key.name.to_s
587
-
588
- type = RubyDB::Rails::Type.to_rails(col.type)
589
- schema << " t.#{type} \"#{col.name}\""
590
- schema << ", primary_key: true" if primary_key && col.name.to_s == primary_key.name.to_s
591
- schema << ", default: #{schema_literal(col.default, col.type)}" unless col.default.nil?
592
- schema << ", null: false" unless col.null
593
- schema << "\n"
594
- end
595
- schema << "end\n\n"
596
-
597
- indexes(table).each do |index|
598
- schema << "add_index \"#{table}\", #{index.columns.map(&:to_s).inspect}"
599
- schema << ", unique: true" if index.unique
600
- schema << ", name: #{index.name.to_s.inspect}\n"
601
- end
602
- schema << "\n" if indexes(table).any?
603
- end
604
- schema
605
- end
606
-
607
- def schema_literal(value, type = nil)
608
- if type.to_sym == :boolean && value.is_a?(String) && %w[true false].include?(value.downcase)
609
- return value.downcase
610
- end
611
-
612
- case value
613
- when true then "true"
614
- when false then "false"
615
- when Numeric then value.to_s
616
- else value.to_s.inspect
617
- end
618
- end
619
-
620
- # ==================== CONNECTION MANAGEMENT ====================
621
-
622
- def reset!
623
- @connection.disconnect
624
- @connection.connect
625
- @prepared_statements.clear
626
- @query_cache.clear
627
- @statements.clear
628
- end
629
-
630
- def disconnect!
631
- @connection.disconnect
632
- end
633
-
634
- def reconnect!
635
- reset!
636
- end
637
-
638
- def active?
639
- @connection.connected?
640
- end
641
-
642
- def close
643
- @connection.disconnect
644
- end
645
-
646
- # ==================== QUERY CACHE ====================
647
-
648
- def clear_cache!
649
- @query_cache.clear
650
- end
651
-
652
- def enable_query_cache!
653
- @query_cache_enabled = true
654
- @query_cache.clear
655
- end
656
-
657
- def disable_query_cache!
658
- @query_cache_enabled = false
659
- @query_cache.clear
660
- end
661
-
662
- def query_cache_enabled
663
- @query_cache_enabled
664
- end
665
-
666
- # ==================== PREPARED STATEMENTS ====================
667
-
668
- def prepare_statement(sql)
669
- @lock.synchronize do
670
- stmt_id = "stmt_#{Time.now.to_i}_#{@statement_counter}"
671
- @statement_counter += 1
672
-
673
- result = @connection.prepare(sql)
674
- @prepared_statements[stmt_id] = {
675
- id: result.statement_id,
676
- sql: sql,
677
- created_at: Time.now
678
- }
679
-
680
- stmt_id
681
- end
682
- end
683
-
684
- def execute_prepared_statement(stmt_id, params = [])
685
- @lock.synchronize do
686
- stmt = @prepared_statements[stmt_id]
687
- return nil unless stmt
688
-
689
- @connection.execute_prepared(stmt[:id], params)
690
- end
691
- end
692
-
693
- def close_statement(stmt_id)
694
- @lock.synchronize do
695
- stmt = @prepared_statements.delete(stmt_id)
696
- if stmt
697
- @connection.close_statement(stmt[:id])
698
- end
699
- end
700
- end
701
-
702
- # ==================== VERSION INFORMATION ====================
703
-
704
- def dbms_version
705
- RubyDB::VERSION
706
- end
707
-
708
- # ==================== FEATURE SUPPORT ====================
709
-
710
- def supports_datetime_with_precision?
711
- true
712
- end
713
-
714
- def supports_materialized_views?
715
- false
716
- end
717
-
718
- def supports_common_table_expressions?
719
- false
720
- end
721
-
722
- # ==================== PRIVATE METHODS ====================
723
-
724
- private
725
-
726
- def embedded?
727
- !@connection.engine.nil?
728
- end
729
-
730
- def embedded_columns(table_name)
731
- @connection.engine.table_columns(table_name).map do |column|
732
- ActiveRecord::ConnectionAdapters::Column.new(
733
- column.name.to_s,
734
- column.has_default? ? rails_default_value(column.default) : nil,
735
- ActiveRecord::ConnectionAdapters::SqlTypeMetadata.new(
736
- sql_type: column.type.to_s.upcase,
737
- type: rails_type_for(column.type),
738
- limit: column.options[:limit]
739
- ),
740
- column.nullable?
741
- )
742
- end
743
- end
744
-
745
- def rails_type_for(type)
746
- case type.to_sym
747
- when :integer, :bigint, :smallint then :integer
748
- when :float then :float
749
- when :decimal then :decimal
750
- when :boolean then :boolean
751
- when :date then :date
752
- when :time then :time
753
- when :datetime, :timestamp then :datetime
754
- when :binary, :blob then :binary
755
- when :json then :json
756
- else :string
757
- end
758
- end
759
-
760
- # ActiveRecord's generic Column deduplication is string-oriented. RubyDB
761
- # persists typed defaults, so serialize scalar defaults at this boundary
254
+ ActiveRecord::Result.new([pk || "id"], id.nil? ? [] : [[id]])
255
+ end
256
+ end
257
+
258
+ def insert(arel, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = [], returning: nil)
259
+ sql, binds = to_sql_and_binds(arel, binds)
260
+ result = exec_insert(sql, name, binds, pk, sequence_name, returning: returning)
261
+ return returning_column_values(result) unless returning.nil?
262
+
263
+ id_value || last_inserted_id(result)
264
+ end
265
+
266
+ def update(arel, name = nil, binds = [])
267
+ sql, binds = to_sql_and_binds(arel, binds)
268
+ exec_update(sql, name, binds)
269
+ end
270
+
271
+ def delete(arel, name = nil, binds = [])
272
+ sql, binds = to_sql_and_binds(arel, binds)
273
+ exec_delete(sql, name, binds)
274
+ end
275
+
276
+ def select_all(sql, name = nil, binds = [], preparable: nil, async: false, allow_retry: false)
277
+ sql, binds = to_sql_and_binds(sql, binds)
278
+ exec_query(sql, name, binds)
279
+ end
280
+
281
+ def select_one(sql, name = nil, binds = [])
282
+ sql, binds = to_sql_and_binds(sql, binds)
283
+ result = exec_query(sql, name, binds)
284
+ result.first
285
+ end
286
+
287
+ def select_value(sql, name = nil, binds = [])
288
+ sql, binds = to_sql_and_binds(sql, binds)
289
+ result = exec_query(sql, name, binds)
290
+ result.first&.values&.first
291
+ end
292
+
293
+ def select_values(sql, name = nil, binds = [])
294
+ sql, binds = to_sql_and_binds(sql, binds)
295
+ result = exec_query(sql, name, binds)
296
+ result.map { |row| row.values.first }
297
+ end
298
+
299
+ def select_rows(sql, name = nil, binds = [])
300
+ sql, binds = to_sql_and_binds(sql, binds)
301
+ result = exec_query(sql, name, binds)
302
+ result.map { |row| row.values }
303
+ end
304
+
305
+ # ==================== TRANSACTION METHODS ====================
306
+
307
+ def begin_db_transaction
308
+ @transaction_depth += 1
309
+ @connection.begin_db_transaction if @transaction_depth == 1
310
+ end
311
+
312
+ def commit_db_transaction
313
+ return if @transaction_depth <= 0
314
+
315
+ @transaction_depth -= 1
316
+ @connection.commit_db_transaction if @transaction_depth == 0
317
+ end
318
+
319
+ def rollback_db_transaction
320
+ return if @transaction_depth <= 0
321
+
322
+ @transaction_depth -= 1
323
+ @connection.rollback_db_transaction if @transaction_depth == 0
324
+ @transaction_depth = 0 if @transaction_depth < 0
325
+ end
326
+
327
+ def create_savepoint(name)
328
+ execute("SAVEPOINT #{name}")
329
+ end
330
+
331
+ def rollback_to_savepoint(name)
332
+ execute("ROLLBACK TO SAVEPOINT #{name}")
333
+ end
334
+
335
+ def release_savepoint(name)
336
+ execute("RELEASE SAVEPOINT #{name}")
337
+ end
338
+
339
+ def in_transaction?
340
+ @transaction_depth > 0
341
+ end
342
+
343
+ def transaction_joinable?
344
+ true
345
+ end
346
+
347
+ def transactional?
348
+ true
349
+ end
350
+
351
+ # ==================== SCHEMA STATEMENT METHODS ====================
352
+
353
+ def create_table(table_name, **options, &)
354
+ RubyDB::Rails::SchemaStatements.instance_method(:create_table).bind_call(self, table_name, options, &)
355
+ end
356
+
357
+ def drop_table(table_name, **options)
358
+ sql = +"DROP TABLE"
359
+ sql << " IF EXISTS" if options[:if_exists]
360
+ sql << " #{quote_table_name(table_name)}"
361
+ sql << " CASCADE" if options[:cascade]
362
+ execute(sql)
363
+ end
364
+
365
+ def add_column(table_name, column_name, type, **options)
366
+ sql = "ALTER TABLE #{quote_table_name(table_name)}"
367
+ sql << " ADD COLUMN #{quote_column_name(column_name)} #{type_to_sql(type, options)}"
368
+
369
+ if options[:null] == false
370
+ sql << " NOT NULL"
371
+ end
372
+
373
+ if options[:default]
374
+ sql << " DEFAULT #{quote_default(options[:default])}"
375
+ end
376
+
377
+ if options[:primary_key]
378
+ sql << " PRIMARY KEY"
379
+ end
380
+
381
+ execute(sql)
382
+ end
383
+
384
+ def remove_column(table_name, column_name, type = nil, **options)
385
+ sql = "ALTER TABLE #{quote_table_name(table_name)}"
386
+ sql << " DROP COLUMN #{quote_column_name(column_name)}"
387
+ sql << " CASCADE" if options[:cascade]
388
+ execute(sql)
389
+ end
390
+
391
+ def change_column(table_name, column_name, type, **options)
392
+ # Change column type
393
+ sql = "ALTER TABLE #{quote_table_name(table_name)}"
394
+ sql << " ALTER COLUMN #{quote_column_name(column_name)}"
395
+ sql << " TYPE #{type_to_sql(type, options)}"
396
+ execute(sql)
397
+
398
+ # Change nullability
399
+ if options.key?(:null)
400
+ sql = "ALTER TABLE #{quote_table_name(table_name)}"
401
+ sql << " ALTER COLUMN #{quote_column_name(column_name)}"
402
+ sql << (options[:null] ? " DROP" : " SET") + " NOT NULL"
403
+ execute(sql)
404
+ end
405
+
406
+ # Change default
407
+ if options.key?(:default)
408
+ sql = "ALTER TABLE #{quote_table_name(table_name)}"
409
+ sql << " ALTER COLUMN #{quote_column_name(column_name)}"
410
+ sql << if options[:default].nil?
411
+ " DROP DEFAULT"
412
+ else
413
+ " SET DEFAULT #{quote_default(options[:default])}"
414
+ end
415
+ execute(sql)
416
+ end
417
+ end
418
+
419
+ def rename_column(table_name, column_name, new_column_name)
420
+ sql = "ALTER TABLE #{quote_table_name(table_name)}"
421
+ sql << " RENAME COLUMN #{quote_column_name(column_name)}"
422
+ sql << " TO #{quote_column_name(new_column_name)}"
423
+ execute(sql)
424
+ end
425
+
426
+ def rename_table(old_name, new_name)
427
+ sql = "ALTER TABLE #{quote_table_name(old_name)}"
428
+ sql << " RENAME TO #{quote_table_name(new_name)}"
429
+ execute(sql)
430
+ end
431
+
432
+ def add_index(table_name, column_name, **options)
433
+ index_name = options[:name] || "idx_#{table_name}_#{Array(column_name).join("_")}"
434
+ sql = +"CREATE"
435
+ sql << " UNIQUE" if options[:unique]
436
+ sql << " INDEX #{quote_column_name(index_name)}"
437
+ sql << " ON #{quote_table_name(table_name)}"
438
+ sql << " (#{Array(column_name).map { |c| quote_column_name(c) }.join(", ")})"
439
+ sql << " WHERE #{options[:where]}" if options[:where]
440
+ execute(sql)
441
+ end
442
+
443
+ def remove_index(table_name, column_name = nil, **options)
444
+ index_name = options[:name]
445
+ if index_name.nil?
446
+ column_name ||= options[:column] || options[:columns]
447
+ index_name = "idx_#{table_name}_#{Array(column_name).join("_")}"
448
+ end
449
+
450
+ sql = "DROP INDEX #{quote_column_name(index_name)}"
451
+ execute(sql)
452
+ end
453
+
454
+ def add_foreign_key(from_table, to_table, **options)
455
+ fk_name = options[:name] || "fk_#{from_table}_to_#{to_table}"
456
+ sql = "ALTER TABLE #{quote_table_name(from_table)}"
457
+ sql << " ADD CONSTRAINT #{quote_column_name(fk_name)}"
458
+ sql << " FOREIGN KEY (#{quote_column_name(options[:column] || :id)})"
459
+ sql << " REFERENCES #{quote_table_name(to_table)}"
460
+ sql << " (#{quote_column_name(options[:primary_key] || :id)})"
461
+ sql << " ON DELETE #{options[:on_delete]}" if options[:on_delete]
462
+ sql << " ON UPDATE #{options[:on_update]}" if options[:on_update]
463
+ execute(sql)
464
+ end
465
+
466
+ def foreign_keys(table_name)
467
+ constraints = if embedded?
468
+ @connection.engine.table_metadata[table_name.to_s]&.fetch(:constraints, []) || []
469
+ else
470
+ Array(remote_metadata(table_name)[:constraints])
471
+ end
472
+ constraints.filter_map do |constraint|
473
+ definition = constraint.transform_keys(&:to_sym)
474
+ next unless definition[:type].to_s.downcase.include?("foreign")
475
+
476
+ columns = definition[:columns] || []
477
+ reference_table = definition[:reference_table]
478
+ reference_columns = definition[:reference_columns] || ["id"]
479
+ options = {
480
+ column: Array(columns).first.to_s,
481
+ primary_key: Array(reference_columns).first.to_s,
482
+ name: definition[:name]
483
+ }
484
+ options[:on_delete] = definition[:on_delete].to_s if definition[:on_delete]
485
+ options[:on_update] = definition[:on_update].to_s if definition[:on_update]
486
+ ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(table_name.to_s, reference_table.to_s, options)
487
+ end
488
+ end
489
+
490
+ def remove_foreign_key(from_table, **options)
491
+ fk_name = options[:name] || "fk_#{from_table}_to_#{options[:to_table]}"
492
+ sql = "ALTER TABLE #{quote_table_name(from_table)}"
493
+ sql << " DROP CONSTRAINT #{quote_column_name(fk_name)}"
494
+ execute(sql)
495
+ end
496
+
497
+ def add_timestamps(table_name, **options)
498
+ add_column(table_name, :created_at, :datetime, options)
499
+ add_column(table_name, :updated_at, :datetime, options)
500
+ end
501
+
502
+ def remove_timestamps(table_name, **options)
503
+ remove_column(table_name, :updated_at, options)
504
+ remove_column(table_name, :created_at, options)
505
+ end
506
+
507
+ def change_column_null(table_name, column_name, null, default = nil)
508
+ if default
509
+ sql = "UPDATE #{quote_table_name(table_name)}"
510
+ sql << " SET #{quote_column_name(column_name)} = #{quote(default)}"
511
+ sql << " WHERE #{quote_column_name(column_name)} IS NULL"
512
+ execute(sql)
513
+ end
514
+
515
+ sql = "ALTER TABLE #{quote_table_name(table_name)}"
516
+ sql << " ALTER COLUMN #{quote_column_name(column_name)}"
517
+ sql << (null ? " DROP" : " SET") + " NOT NULL"
518
+ execute(sql)
519
+ end
520
+
521
+ def change_column_default(table_name, column_name, default)
522
+ sql = "ALTER TABLE #{quote_table_name(table_name)}"
523
+ sql << " ALTER COLUMN #{quote_column_name(column_name)}"
524
+ sql << if default.nil?
525
+ " DROP DEFAULT"
526
+ else
527
+ " SET DEFAULT #{quote_default(default)}"
528
+ end
529
+ execute(sql)
530
+ end
531
+
532
+ # ==================== QUOTING METHODS ====================
533
+
534
+ def quote(value, column = nil)
535
+ @connection.quote(value, column)
536
+ end
537
+
538
+ def quote_table_name(name)
539
+ @connection.quote_table_name(name)
540
+ end
541
+
542
+ def quote_column_name(name)
543
+ @connection.quote_column_name(name)
544
+ end
545
+
546
+ def quote_default(value)
547
+ quote(value)
548
+ end
549
+
550
+ # ==================== TYPE CASTING ====================
551
+
552
+ def type_cast(value, type)
553
+ RubyDB::Rails::Type.serialize(value, type)
554
+ end
555
+
556
+ def type_cast_from_database(value, type)
557
+ RubyDB::Rails::Type.deserialize(value, type)
558
+ end
559
+
560
+ # ==================== SCHEMA VERSION ====================
561
+
562
+ def schema_version
563
+ result = execute("SELECT version FROM schema_migrations ORDER BY version DESC LIMIT 1")
564
+ result.first ? result.first["version"] : nil
565
+ end
566
+
567
+ def schema_migrations
568
+ result = execute("SELECT version FROM schema_migrations ORDER BY version")
569
+ result.map { |row| row["version"] }
570
+ end
571
+
572
+ def dump_schema
573
+ schema = +""
574
+ tables.each do |table|
575
+ table_columns = columns(table)
576
+ primary_key_name = if embedded?
577
+ @connection.engine.table_columns(table).find(&:primary_key?)&.name
578
+ else
579
+ primary_key(table)
580
+ end
581
+ primary_key = table_columns.find { |column| column.name.to_s == primary_key_name.to_s } if primary_key_name
582
+ automatic_id = primary_key && primary_key_name.to_s == "id" && primary_key.type.to_sym == :integer
583
+ table_options = automatic_id ? "" : ", id: false"
584
+ schema << "create_table \"#{table}\"#{table_options} do |t|\n"
585
+ table_columns.each do |col|
586
+ next if automatic_id && primary_key && col.name.to_s == primary_key.name.to_s
587
+
588
+ type = RubyDB::Rails::Type.to_rails(col.type)
589
+ schema << " t.#{type} \"#{col.name}\""
590
+ schema << ", primary_key: true" if primary_key && col.name.to_s == primary_key.name.to_s
591
+ schema << ", default: #{schema_literal(col.default, col.type)}" unless col.default.nil?
592
+ schema << ", null: false" unless col.null
593
+ schema << "\n"
594
+ end
595
+ schema << "end\n\n"
596
+
597
+ indexes(table).each do |index|
598
+ schema << "add_index \"#{table}\", #{index.columns.map(&:to_s).inspect}"
599
+ schema << ", unique: true" if index.unique
600
+ schema << ", name: #{index.name.to_s.inspect}\n"
601
+ end
602
+ schema << "\n" if indexes(table).any?
603
+ end
604
+ schema
605
+ end
606
+
607
+ def schema_literal(value, type = nil)
608
+ if type.to_sym == :boolean && value.is_a?(String) && %w[true false].include?(value.downcase)
609
+ return value.downcase
610
+ end
611
+
612
+ case value
613
+ when true then "true"
614
+ when false then "false"
615
+ when Numeric then value.to_s
616
+ else value.to_s.inspect
617
+ end
618
+ end
619
+
620
+ # ==================== CONNECTION MANAGEMENT ====================
621
+
622
+ def reset!
623
+ @connection.disconnect
624
+ @connection.connect
625
+ @prepared_statements.clear
626
+ @query_cache.clear
627
+ @statements.clear
628
+ end
629
+
630
+ def disconnect!
631
+ @connection.disconnect
632
+ end
633
+
634
+ def reconnect!
635
+ reset!
636
+ end
637
+
638
+ def active?
639
+ @connection.connected?
640
+ end
641
+
642
+ def close
643
+ @connection.disconnect
644
+ end
645
+
646
+ # ==================== QUERY CACHE ====================
647
+
648
+ def clear_cache!
649
+ @query_cache.clear
650
+ end
651
+
652
+ def enable_query_cache!
653
+ @query_cache_enabled = true
654
+ @query_cache.clear
655
+ end
656
+
657
+ def disable_query_cache!
658
+ @query_cache_enabled = false
659
+ @query_cache.clear
660
+ end
661
+
662
+ attr_reader :query_cache_enabled
663
+
664
+ # ==================== PREPARED STATEMENTS ====================
665
+
666
+ def prepare_statement(sql)
667
+ @lock.synchronize do
668
+ stmt_id = "stmt_#{Time.now.to_i}_#{@statement_counter}"
669
+ @statement_counter += 1
670
+
671
+ result = @connection.prepare(sql)
672
+ @prepared_statements[stmt_id] = {
673
+ id: result.statement_id,
674
+ sql: sql,
675
+ created_at: Time.now
676
+ }
677
+
678
+ stmt_id
679
+ end
680
+ end
681
+
682
+ def execute_prepared_statement(stmt_id, params = [])
683
+ @lock.synchronize do
684
+ stmt = @prepared_statements[stmt_id]
685
+ return nil unless stmt
686
+
687
+ @connection.execute_prepared(stmt[:id], params)
688
+ end
689
+ end
690
+
691
+ def close_statement(stmt_id)
692
+ @lock.synchronize do
693
+ stmt = @prepared_statements.delete(stmt_id)
694
+ if stmt
695
+ @connection.close_statement(stmt[:id])
696
+ end
697
+ end
698
+ end
699
+
700
+ # ==================== VERSION INFORMATION ====================
701
+
702
+ def dbms_version
703
+ RubyDB::VERSION
704
+ end
705
+
706
+ # ==================== FEATURE SUPPORT ====================
707
+
708
+ def supports_datetime_with_precision?
709
+ true
710
+ end
711
+
712
+ def supports_materialized_views?
713
+ false
714
+ end
715
+
716
+ def supports_common_table_expressions?
717
+ false
718
+ end
719
+
720
+ # ==================== PRIVATE METHODS ====================
721
+
722
+ private
723
+
724
+ def embedded?
725
+ !@connection.engine.nil?
726
+ end
727
+
728
+ def remote_metadata(table_name = nil)
729
+ raw = @connection.client.metadata(table_name)
730
+ metadata = raw[:metadata] || raw["metadata"] || raw
731
+ metadata.transform_keys(&:to_sym)
732
+ end
733
+
734
+ def embedded_columns(table_name)
735
+ @connection.engine.table_columns(table_name).map do |column|
736
+ ActiveRecord::ConnectionAdapters::Column.new(
737
+ column.name.to_s,
738
+ column.has_default? ? rails_default_value(column.default) : nil,
739
+ ActiveRecord::ConnectionAdapters::SqlTypeMetadata.new(
740
+ sql_type: column.type.to_s.upcase,
741
+ type: rails_type_for(column.type),
742
+ limit: column.options[:limit]
743
+ ),
744
+ column.nullable?
745
+ )
746
+ end
747
+ end
748
+
749
+ def rails_type_for(type)
750
+ case type.to_sym
751
+ when :integer, :bigint, :smallint then :integer
752
+ when :float then :float
753
+ when :decimal then :decimal
754
+ when :boolean then :boolean
755
+ when :date then :date
756
+ when :time then :time
757
+ when :datetime, :timestamp then :datetime
758
+ when :binary, :blob then :binary
759
+ when :json then :json
760
+ else :string
761
+ end
762
+ end
763
+
764
+ # ActiveRecord's generic Column deduplication is string-oriented. RubyDB
765
+ # persists typed defaults, so serialize scalar defaults at this boundary
762
766
  # and let ActiveRecord cast them through the column type map.
763
767
  def rails_default_value(value)
768
+ return nil if value.nil?
769
+
764
770
  # RubyDB exposes SQL defaults as AST literals. ActiveRecord expects
765
771
  # the scalar payload when it builds its Column metadata; calling
766
772
  # `to_s` on the wrapper would leak the Ruby object inspection into
@@ -768,21 +774,21 @@ module ActiveRecord
768
774
  value = value.value if value.respond_to?(:value) && !value.is_a?(String)
769
775
  value.is_a?(String) ? value : value.to_s
770
776
  end
771
-
772
- def sql_for_execution(sql)
773
- sql = sql.to_sql if sql.respond_to?(:to_sql)
774
- sql
775
- end
776
-
777
- def active_record_result(result)
778
- rows = result.to_a
779
- columns = if rows.first.respond_to?(:keys)
780
- rows.first.keys.map(&:to_s)
781
- else
782
- result.columns.map do |column|
783
- column.is_a?(Hash) ? (column[:name] || column["name"] || column) : column
784
- end.map(&:to_s)
785
- end
777
+
778
+ def sql_for_execution(sql)
779
+ sql = sql.to_sql if sql.respond_to?(:to_sql)
780
+ sql
781
+ end
782
+
783
+ def active_record_result(result)
784
+ rows = result.to_a
785
+ columns = if rows.first.respond_to?(:keys)
786
+ rows.first.keys.map(&:to_s)
787
+ else
788
+ result.columns.map do |column|
789
+ column.is_a?(Hash) ? (column[:name] || column["name"] || column) : column
790
+ end.map(&:to_s)
791
+ end
786
792
  values = rows.map do |row|
787
793
  columns.map do |column|
788
794
  if row.respond_to?(:key?) && row.key?(column)
@@ -792,110 +798,106 @@ module ActiveRecord
792
798
  end
793
799
  end
794
800
  end
795
- ActiveRecord::Result.new(columns, values)
796
- end
797
-
798
- # ActiveRecord normally supplies QueryAttribute objects, but migration
799
- # and schema code can also pass raw values or two-element bind pairs.
800
- # Normalize all supported forms at the adapter boundary.
801
- def bind_values(binds)
802
- binds.map do |bind|
803
- value = if bind.respond_to?(:value_for_database)
804
- bind.value_for_database
805
- elsif bind.respond_to?(:value)
806
- bind.value
807
- elsif bind.is_a?(Array) && bind.length == 2
808
- bind.last
809
- else
810
- bind
811
- end
812
- value.respond_to?(:value_for_database) ? value.value_for_database : value
813
- end
814
- end
815
-
816
- def parse_index_columns(sql)
817
- if sql =~ /\(([^)]+)\)/
818
- $1.split(",").map(&:strip)
819
- else
820
- []
821
- end
822
- end
823
-
824
- def extract_limit(type)
825
- if type =~ /VARCHAR\((\d+)\)/
826
- $1.to_i
827
- else
828
- nil
829
- end
830
- end
831
-
832
- def log(sql, name = nil, &block)
833
- start_time = Time.now
834
- result = block.call
835
- elapsed_ms = (Time.now - start_time) * 1000
836
-
837
- if @logger
838
- @logger.debug " #{name || 'SQL'} (#{elapsed_ms.round(2)}ms) #{sql}"
839
- end
840
-
841
- result
842
- end
843
-
844
- def type_to_sql(type, options = {})
845
- case type.to_sym
846
- when :integer
847
- "INTEGER"
848
- when :bigint
849
- "BIGINT"
850
- when :smallint
851
- "SMALLINT"
852
- when :float
853
- "FLOAT"
854
- when :decimal
855
- precision = options[:precision] || 10
856
- scale = options[:scale] || 2
857
- "DECIMAL(#{precision}, #{scale})"
858
- when :boolean
859
- "BOOLEAN"
860
- when :text
861
- "TEXT"
862
- when :string
863
- limit = options[:limit] || 255
864
- "VARCHAR(#{limit})"
865
- when :binary
866
- "BLOB"
867
- when :date
868
- "DATE"
869
- when :time
870
- "TIME"
871
- when :datetime, :timestamp
872
- "TIMESTAMP"
873
- when :json
874
- "JSON"
875
- when :uuid
876
- "UUID"
877
- else
878
- "TEXT"
879
- end
880
- end
881
- end
882
- end
883
- end
884
-
885
- # Rails 7.2 introduced explicit adapter registration. Rails 7.1 loads custom
886
- # adapters through the conventional `rubydb_connection` hook instead.
887
- if ActiveRecord::ConnectionAdapters.respond_to?(:register)
888
- ActiveRecord::ConnectionAdapters.register("rubydb", "ActiveRecord::ConnectionAdapters::RubyDBAdapter")
889
- else
890
- module ActiveRecord
891
- module ConnectionHandling
892
- def rubydb_adapter_class
893
- ConnectionAdapters::RubyDBAdapter
894
- end
895
-
896
- def rubydb_connection(config)
897
- rubydb_adapter_class.new(config)
898
- end
899
- end
900
- end
901
- end
801
+ ActiveRecord::Result.new(columns, values)
802
+ end
803
+
804
+ # ActiveRecord normally supplies QueryAttribute objects, but migration
805
+ # and schema code can also pass raw values or two-element bind pairs.
806
+ # Normalize all supported forms at the adapter boundary.
807
+ def bind_values(binds)
808
+ binds.map do |bind|
809
+ value = if bind.respond_to?(:value_for_database)
810
+ bind.value_for_database
811
+ elsif bind.respond_to?(:value)
812
+ bind.value
813
+ elsif bind.is_a?(Array) && bind.length == 2
814
+ bind.last
815
+ else
816
+ bind
817
+ end
818
+ value.respond_to?(:value_for_database) ? value.value_for_database : value
819
+ end
820
+ end
821
+
822
+ def parse_index_columns(sql)
823
+ if sql =~ /\(([^)]+)\)/
824
+ $1.split(",").map(&:strip)
825
+ else
826
+ []
827
+ end
828
+ end
829
+
830
+ def extract_limit(type)
831
+ if type =~ /VARCHAR\((\d+)\)/
832
+ $1.to_i
833
+ end
834
+ end
835
+
836
+ def log(sql, name = nil, &block)
837
+ start_time = Time.now
838
+ result = block.call
839
+ elapsed_ms = (Time.now - start_time) * 1000
840
+
841
+ @logger&.debug " #{name || "SQL"} (#{elapsed_ms.round(2)}ms) #{sql}"
842
+
843
+ result
844
+ end
845
+
846
+ def type_to_sql(type, options = {})
847
+ case type.to_sym
848
+ when :integer
849
+ "INTEGER"
850
+ when :bigint
851
+ "BIGINT"
852
+ when :smallint
853
+ "SMALLINT"
854
+ when :float
855
+ "FLOAT"
856
+ when :decimal
857
+ precision = options[:precision] || 10
858
+ scale = options[:scale] || 2
859
+ "DECIMAL(#{precision}, #{scale})"
860
+ when :boolean
861
+ "BOOLEAN"
862
+ when :text
863
+ "TEXT"
864
+ when :string
865
+ limit = options[:limit] || 255
866
+ "VARCHAR(#{limit})"
867
+ when :binary
868
+ "BLOB"
869
+ when :date
870
+ "DATE"
871
+ when :time
872
+ "TIME"
873
+ when :datetime, :timestamp
874
+ "TIMESTAMP"
875
+ when :json
876
+ "JSON"
877
+ when :uuid
878
+ "UUID"
879
+ else
880
+ "TEXT"
881
+ end
882
+ end
883
+ end
884
+ end
885
+ end
886
+
887
+ # Rails 7.2 introduced explicit adapter registration. Rails 7.1 loads custom
888
+ # adapters through the conventional `rubydb_connection` hook instead.
889
+ if ActiveRecord::ConnectionAdapters.respond_to?(:register)
890
+ ActiveRecord::ConnectionAdapters.register("rubydb", "ActiveRecord::ConnectionAdapters::RubyDBAdapter")
891
+ else
892
+ module ActiveRecord
893
+ module ConnectionHandling
894
+ def rubydb_adapter_class
895
+ ConnectionAdapters::RubyDBAdapter
896
+ end
897
+
898
+ def rubydb_connection(config)
899
+ rubydb_adapter_class.new(config)
900
+ end
901
+ end
902
+ end
903
+ end