activerecord-jdbc-adapter 70.2-java → 72.0-java

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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +26 -26
  3. data/.gitignore +8 -0
  4. data/Gemfile +17 -4
  5. data/README.md +10 -5
  6. data/RUNNING_TESTS.md +36 -0
  7. data/activerecord-jdbc-adapter.gemspec +2 -2
  8. data/lib/arjdbc/abstract/connection_management.rb +25 -10
  9. data/lib/arjdbc/abstract/core.rb +15 -13
  10. data/lib/arjdbc/abstract/database_statements.rb +36 -36
  11. data/lib/arjdbc/abstract/relation_query_attribute_monkey_patch.rb +24 -0
  12. data/lib/arjdbc/abstract/statement_cache.rb +2 -7
  13. data/lib/arjdbc/abstract/transaction_support.rb +39 -22
  14. data/lib/arjdbc/jdbc/adapter.rb +0 -1
  15. data/lib/arjdbc/jdbc/adapter_java.jar +0 -0
  16. data/lib/arjdbc/jdbc/column.rb +0 -34
  17. data/lib/arjdbc/jdbc/connection_methods.rb +1 -1
  18. data/lib/arjdbc/mysql/adapter.rb +108 -32
  19. data/lib/arjdbc/mysql/adapter_hash_config.rb +159 -0
  20. data/lib/arjdbc/mysql.rb +1 -1
  21. data/lib/arjdbc/postgresql/adapter.rb +267 -114
  22. data/lib/arjdbc/postgresql/adapter_hash_config.rb +98 -0
  23. data/lib/arjdbc/postgresql/base/array_encoder.rb +3 -1
  24. data/lib/arjdbc/postgresql/database_statements.rb +20 -0
  25. data/lib/arjdbc/postgresql/oid_types.rb +10 -29
  26. data/lib/arjdbc/postgresql/schema_statements.rb +57 -0
  27. data/lib/arjdbc/postgresql.rb +1 -1
  28. data/lib/arjdbc/sqlite3/adapter.rb +343 -172
  29. data/lib/arjdbc/sqlite3/adapter_hash_config.rb +91 -0
  30. data/lib/arjdbc/sqlite3/column.rb +117 -0
  31. data/lib/arjdbc/sqlite3/connection_methods.rb +7 -2
  32. data/lib/arjdbc/sqlite3/pragmas.rb +105 -0
  33. data/lib/arjdbc/sqlite3.rb +1 -1
  34. data/lib/arjdbc/version.rb +1 -1
  35. data/lib/arjdbc.rb +13 -1
  36. data/rakelib/02-test.rake +2 -2
  37. data/rakelib/rails.rake +2 -0
  38. data/src/java/arjdbc/jdbc/RubyJdbcConnection.java +9 -2
  39. data/src/java/arjdbc/mysql/MySQLRubyJdbcConnection.java +12 -5
  40. data/src/java/arjdbc/sqlite3/SQLite3RubyJdbcConnection.java +14 -3
  41. metadata +15 -11
  42. data/lib/arjdbc/jdbc/base_ext.rb +0 -17
@@ -16,6 +16,11 @@ require "active_record/connection_adapters/sqlite3/schema_definitions"
16
16
  require "active_record/connection_adapters/sqlite3/schema_dumper"
17
17
  require "active_record/connection_adapters/sqlite3/schema_statements"
18
18
  require "active_support/core_ext/class/attribute"
19
+ require "arjdbc/sqlite3/column"
20
+ require "arjdbc/sqlite3/adapter_hash_config"
21
+ require "arjdbc/sqlite3/pragmas"
22
+
23
+ require "arjdbc/abstract/relation_query_attribute_monkey_patch"
19
24
 
20
25
  module SQLite3
21
26
  module Constants
@@ -55,18 +60,12 @@ module ArJdbc
55
60
  # DIFFERENCE: Some common constant names to reduce differences in rest of this module from AR5 version
56
61
  ConnectionAdapters = ::ActiveRecord::ConnectionAdapters
57
62
  IndexDefinition = ::ActiveRecord::ConnectionAdapters::IndexDefinition
63
+ ForeignKeyDefinition = ::ActiveRecord::ConnectionAdapters::ForeignKeyDefinition
58
64
  Quoting = ::ActiveRecord::ConnectionAdapters::SQLite3::Quoting
59
65
  RecordNotUnique = ::ActiveRecord::RecordNotUnique
60
66
  SchemaCreation = ConnectionAdapters::SQLite3::SchemaCreation
61
67
  SQLite3Adapter = ConnectionAdapters::AbstractAdapter
62
68
 
63
- ADAPTER_NAME = 'SQLite'
64
-
65
- # DIFFERENCE: FQN
66
- include ::ActiveRecord::ConnectionAdapters::SQLite3::Quoting
67
- include ::ActiveRecord::ConnectionAdapters::SQLite3::SchemaStatements
68
- include ::ActiveRecord::ConnectionAdapters::SQLite3::DatabaseStatements
69
-
70
69
  NATIVE_DATABASE_TYPES = {
71
70
  primary_key: "integer PRIMARY KEY AUTOINCREMENT NOT NULL",
72
71
  string: { name: "varchar" },
@@ -81,7 +80,16 @@ module ArJdbc
81
80
  boolean: { name: "boolean" },
82
81
  json: { name: "json" },
83
82
  }
84
-
83
+
84
+ DEFAULT_PRAGMAS = {
85
+ "foreign_keys" => true,
86
+ "journal_mode" => :wal,
87
+ "synchronous" => :normal,
88
+ "mmap_size" => 134217728, # 128 megabytes
89
+ "journal_size_limit" => 67108864, # 64 megabytes
90
+ "cache_size" => 2000
91
+ }
92
+
85
93
  class StatementPool < ConnectionAdapters::StatementPool # :nodoc:
86
94
  private
87
95
  def dealloc(stmt)
@@ -89,20 +97,8 @@ module ArJdbc
89
97
  end
90
98
  end
91
99
 
92
- def initialize(connection, logger, connection_options, config)
93
- @memory_database = config[:database] == ":memory:"
94
- super(connection, logger, config)
95
- configure_connection
96
- end
97
-
98
100
  def self.database_exists?(config)
99
- config = config.symbolize_keys
100
- if config[:database] == ":memory:"
101
- true
102
- else
103
- database_file = defined?(Rails.root) ? File.expand_path(config[:database], Rails.root) : config[:database]
104
- File.exist?(database_file)
105
- end
101
+ @config[:database] == ":memory:" || File.exist?(@config[:database].to_s)
106
102
  end
107
103
 
108
104
  def supports_ddl_transactions?
@@ -153,6 +149,10 @@ module ArJdbc
153
149
  database_version >= "3.8.3"
154
150
  end
155
151
 
152
+ def supports_insert_returning?
153
+ database_version >= "3.35.0"
154
+ end
155
+
156
156
  def supports_insert_on_conflict?
157
157
  database_version >= "3.24.0"
158
158
  end
@@ -165,20 +165,39 @@ module ArJdbc
165
165
  !@memory_database
166
166
  end
167
167
 
168
+ def supports_virtual_columns?
169
+ database_version >= "3.31.0"
170
+ end
171
+
172
+ def connected?
173
+ !(@raw_connection.nil? || @raw_connection.closed?)
174
+ end
175
+
168
176
  def active?
169
- !@raw_connection.closed?
177
+ if connected?
178
+ @lock.synchronize do
179
+ if @raw_connection&.active?
180
+ verified!
181
+ true
182
+ end
183
+ end
184
+ end || false
170
185
  end
171
186
 
172
- def reconnect!
173
- super
174
- connect if @connection.closed?
187
+ def return_value_after_insert?(column) # :nodoc:
188
+ column.auto_populated?
175
189
  end
176
190
 
191
+ # MISSING: alias :reset! :reconnect!
192
+
177
193
  # Disconnects from the database if already connected. Otherwise, this
178
194
  # method does nothing.
179
195
  def disconnect!
180
- super
181
- @connection.close rescue nil
196
+ @lock.synchronize do
197
+ super
198
+ @raw_connection&.close rescue nil
199
+ @raw_connection = nil
200
+ end
182
201
  end
183
202
 
184
203
  def supports_index_sort_order?
@@ -191,7 +210,7 @@ module ArJdbc
191
210
 
192
211
  # Returns the current database encoding format as a string, eg: 'UTF-8'
193
212
  def encoding
194
- @connection.encoding.to_s
213
+ any_raw_connection.encoding.to_s
195
214
  end
196
215
 
197
216
  def supports_explain?
@@ -218,8 +237,14 @@ module ArJdbc
218
237
  end
219
238
  end
220
239
 
221
- def all_foreign_keys_valid? # :nodoc:
222
- execute("PRAGMA foreign_key_check").blank?
240
+ def check_all_foreign_keys_valid! # :nodoc:
241
+ sql = "PRAGMA foreign_key_check"
242
+ result = execute(sql)
243
+
244
+ unless result.blank?
245
+ tables = result.map { |row| row["table"] }
246
+ raise ActiveRecord::StatementInvalid.new("Foreign key violations found: #{tables.join(", ")}", sql: sql)
247
+ end
223
248
  end
224
249
 
225
250
  # SCHEMA STATEMENTS ========================================
@@ -234,18 +259,18 @@ module ArJdbc
234
259
 
235
260
  index_name = index_name_for_remove(table_name, column_name, options)
236
261
 
237
- exec_query "DROP INDEX #{quote_column_name(index_name)}"
262
+ internal_exec_query "DROP INDEX #{quote_column_name(index_name)}"
238
263
  end
239
264
 
240
-
241
265
  # Renames a table.
242
266
  #
243
267
  # Example:
244
268
  # rename_table('octopuses', 'octopi')
245
- def rename_table(table_name, new_name)
269
+ def rename_table(table_name, new_name, **options)
270
+ validate_table_length!(new_name) unless options[:_uses_legacy_table_name]
246
271
  schema_cache.clear_data_source_cache!(table_name.to_s)
247
272
  schema_cache.clear_data_source_cache!(new_name.to_s)
248
- exec_query "ALTER TABLE #{quote_table_name(table_name)} RENAME TO #{quote_table_name(new_name)}"
273
+ internal_exec_query "ALTER TABLE #{quote_table_name(table_name)} RENAME TO #{quote_table_name(new_name)}"
249
274
  rename_table_indexes(table_name, new_name)
250
275
  end
251
276
 
@@ -285,8 +310,10 @@ module ArJdbc
285
310
  end
286
311
 
287
312
  def change_column_null(table_name, column_name, null, default = nil) #:nodoc:
313
+ validate_change_column_null_argument!(null)
314
+
288
315
  unless null || default.nil?
289
- exec_query("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
316
+ internal_exec_query("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
290
317
  end
291
318
  alter_table(table_name) do |definition|
292
319
  definition[column_name].null = null
@@ -295,10 +322,7 @@ module ArJdbc
295
322
 
296
323
  def change_column(table_name, column_name, type, **options) #:nodoc:
297
324
  alter_table(table_name) do |definition|
298
- definition[column_name].instance_eval do
299
- self.type = aliased_types(type.to_s, type)
300
- self.options.merge!(options)
301
- end
325
+ definition.change_column(column_name, type, **options)
302
326
  end
303
327
  end
304
328
 
@@ -308,22 +332,59 @@ module ArJdbc
308
332
  rename_column_indexes(table_name, column.name, new_column_name)
309
333
  end
310
334
 
335
+ def add_timestamps(table_name, **options)
336
+ options[:null] = false if options[:null].nil?
337
+
338
+ if !options.key?(:precision)
339
+ options[:precision] = 6
340
+ end
341
+
342
+ alter_table(table_name) do |definition|
343
+ definition.column :created_at, :datetime, **options
344
+ definition.column :updated_at, :datetime, **options
345
+ end
346
+ end
347
+
311
348
  def add_reference(table_name, ref_name, **options) # :nodoc:
312
349
  super(table_name, ref_name, type: :integer, **options)
313
350
  end
314
351
  alias :add_belongs_to :add_reference
315
352
 
353
+ FK_REGEX = /.*FOREIGN KEY\s+\("([^"]+)"\)\s+REFERENCES\s+"(\w+)"\s+\("(\w+)"\)/
354
+ DEFERRABLE_REGEX = /DEFERRABLE INITIALLY (\w+)/
316
355
  def foreign_keys(table_name)
317
- fk_info = exec_query("PRAGMA foreign_key_list(#{quote(table_name)})", "SCHEMA")
318
- fk_info.map do |row|
356
+ # SQLite returns 1 row for each column of composite foreign keys.
357
+ fk_info = internal_exec_query("PRAGMA foreign_key_list(#{quote(table_name)})", "SCHEMA")
358
+ # Deferred or immediate foreign keys can only be seen in the CREATE TABLE sql
359
+ fk_defs = table_structure_sql(table_name)
360
+ .select do |column_string|
361
+ column_string.start_with?("CONSTRAINT") &&
362
+ column_string.include?("FOREIGN KEY")
363
+ end
364
+ .to_h do |fk_string|
365
+ _, from, table, to = fk_string.match(FK_REGEX).to_a
366
+ _, mode = fk_string.match(DEFERRABLE_REGEX).to_a
367
+ deferred = mode&.downcase&.to_sym || false
368
+ [[table, from, to], deferred]
369
+ end
370
+
371
+ grouped_fk = fk_info.group_by { |row| row["id"] }.values.each { |group| group.sort_by! { |row| row["seq"] } }
372
+ grouped_fk.map do |group|
373
+ row = group.first
319
374
  options = {
320
- column: row["from"],
321
- primary_key: row["to"],
322
375
  on_delete: extract_foreign_key_action(row["on_delete"]),
323
- on_update: extract_foreign_key_action(row["on_update"])
376
+ on_update: extract_foreign_key_action(row["on_update"]),
377
+ deferrable: fk_defs[[row["table"], row["from"], row["to"]]]
324
378
  }
325
- # DIFFERENCE: FQN
326
- ::ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(table_name, row["table"], options)
379
+
380
+ if group.one?
381
+ options[:column] = row["from"]
382
+ options[:primary_key] = row["to"]
383
+ else
384
+ options[:column] = group.map { |row| row["from"] }
385
+ options[:primary_key] = group.map { |row| row["to"] }
386
+ end
387
+ ForeignKeyDefinition.new(table_name, row["table"], options)
327
388
  end
328
389
  end
329
390
 
@@ -342,6 +403,7 @@ module ArJdbc
342
403
  end
343
404
  end
344
405
 
406
+ sql << " RETURNING #{insert.returning}" if insert.returning
345
407
  sql
346
408
  end
347
409
 
@@ -349,6 +411,10 @@ module ArJdbc
349
411
  @config.fetch(:flags, 0).anybits?(::SQLite3::Constants::Open::SHAREDCACHE)
350
412
  end
351
413
 
414
+ def use_insert_returning?
415
+ @use_insert_returning
416
+ end
417
+
352
418
  def get_database_version # :nodoc:
353
419
  SQLite3Adapter::Version.new(query_value("SELECT sqlite_version(*)", "SCHEMA"))
354
420
  end
@@ -359,6 +425,35 @@ module ArJdbc
359
425
  end
360
426
  end
361
427
 
428
+ # DIFFERENCE: here to
429
+ def new_column_from_field(table_name, field, definitions)
430
+ default = field["dflt_value"]
431
+
432
+ type_metadata = fetch_type_metadata(field["type"])
433
+ default_value = extract_value_from_default(default)
434
+ generated_type = extract_generated_type(field)
435
+
436
+ if generated_type.present?
437
+ default_function = default
438
+ else
439
+ default_function = extract_default_function(default_value, default)
440
+ end
441
+
442
+ rowid = is_column_the_rowid?(field, definitions)
443
+
444
+ ActiveRecord::ConnectionAdapters::SQLite3Column.new(
445
+ field["name"],
446
+ default_value,
447
+ type_metadata,
448
+ field["notnull"].to_i == 0,
449
+ default_function,
450
+ collation: field["collation"],
451
+ auto_increment: field["auto_increment"],
452
+ rowid: rowid,
453
+ generated_type: generated_type
454
+ )
455
+ end
456
+
362
457
  private
363
458
  # See https://www.sqlite.org/limits.html,
364
459
  # the default value is 999 when not configured.
@@ -367,7 +462,12 @@ module ArJdbc
367
462
  end
368
463
 
369
464
  def table_structure(table_name)
370
- structure = exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", "SCHEMA")
465
+ structure = if supports_virtual_columns?
466
+ internal_exec_query("PRAGMA table_xinfo(#{quote_table_name(table_name)})", "SCHEMA")
467
+ else
468
+ internal_exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", "SCHEMA")
469
+ end
470
+
371
471
  raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure.empty?
372
472
  table_structure_with_collation(table_name, structure)
373
473
  end
@@ -377,15 +477,18 @@ module ArJdbc
377
477
  case default
378
478
  when /^null$/i
379
479
  nil
380
- # Quoted types
381
- when /^'(.*)'$/m
480
+ # Quoted types
481
+ when /^'([^|]*)'$/m
382
482
  $1.gsub("''", "'")
383
- # Quoted types
384
- when /^"(.*)"$/m
483
+ # Quoted types
484
+ when /^"([^|]*)"$/m
385
485
  $1.gsub('""', '"')
386
- # Numeric types
486
+ # Numeric types
387
487
  when /\A-?\d+(\.\d*)?\z/
388
488
  $&
489
+ # Binary columns
490
+ when /x'(.*)'/
491
+ [ $1 ].pack("H*")
389
492
  else
390
493
  # Anything else is blank or some function
391
494
  # and we can't know the value of that, so return nil.
@@ -398,14 +501,15 @@ module ArJdbc
398
501
  end
399
502
 
400
503
  def has_default_function?(default_value, default)
401
- !default_value && %r{\w+\(.*\)|CURRENT_TIME|CURRENT_DATE|CURRENT_TIMESTAMP}.match?(default)
504
+ !default_value && %r{\w+\(.*\)|CURRENT_TIME|CURRENT_DATE|CURRENT_TIMESTAMP|\|\|}.match?(default)
402
505
  end
403
506
 
404
507
  # See: https://www.sqlite.org/lang_altertable.html
405
508
  # SQLite has an additional restriction on the ALTER TABLE statement
406
509
  def invalid_alter_table_type?(type, options)
407
- type.to_sym == :primary_key || options[:primary_key] ||
408
- options[:null] == false && options[:default].nil?
510
+ type == :primary_key || options[:primary_key] ||
511
+ options[:null] == false && options[:default].nil? ||
512
+ (type == :virtual && options[:stored])
409
513
  end
410
514
 
411
515
  def alter_table(
@@ -461,24 +565,40 @@ module ArJdbc
461
565
  options[:rename][column.name.to_sym] ||
462
566
  column.name) : column.name
463
567
 
464
- if column.has_default?
568
+ column_options = {
569
+ limit: column.limit,
570
+ precision: column.precision,
571
+ scale: column.scale,
572
+ null: column.null,
573
+ collation: column.collation,
574
+ primary_key: column_name == from_primary_key
575
+ }
576
+
577
+ if column.virtual?
578
+ column_options[:as] = column.default_function
579
+ column_options[:stored] = column.virtual_stored?
580
+ column_options[:type] = column.type
581
+ elsif column.has_default?
465
582
  type = lookup_cast_type_from_column(column)
466
583
  default = type.deserialize(column.default)
584
+ default = -> { column.default_function } if default.nil?
585
+
586
+ unless column.auto_increment?
587
+ column_options[:default] = default
588
+ end
467
589
  end
468
590
 
469
- @definition.column(column_name, column.type,
470
- limit: column.limit, default: default,
471
- precision: column.precision, scale: column.scale,
472
- null: column.null, collation: column.collation,
473
- primary_key: column_name == from_primary_key
474
- )
591
+ column_type = column.virtual? ? :virtual : (column.bigint? ? :bigint : column.type)
592
+ @definition.column(column_name, column_type, **column_options)
475
593
  end
476
594
 
477
595
  yield @definition if block_given?
478
596
  end
479
597
  copy_table_indexes(from, to, options[:rename] || {})
598
+
599
+ columns_to_copy = @definition.columns.reject { |col| col.options.key?(:as) }.map(&:name)
480
600
  copy_table_contents(from, to,
481
- @definition.columns.map(&:name),
601
+ columns_to_copy,
482
602
  options[:rename] || {})
483
603
  end
484
604
 
@@ -519,8 +639,8 @@ module ArJdbc
519
639
  quoted_columns = columns.map { |col| quote_column_name(col) } * ","
520
640
  quoted_from_columns = from_columns_to_copy.map { |col| quote_column_name(col) } * ","
521
641
 
522
- exec_query("INSERT INTO #{quote_table_name(to)} (#{quoted_columns})
523
- SELECT #{quoted_from_columns} FROM #{quote_table_name(from)}")
642
+ internal_exec_query("INSERT INTO #{quote_table_name(to)} (#{quoted_columns})
643
+ SELECT #{quoted_from_columns} FROM #{quote_table_name(from)}")
524
644
  end
525
645
 
526
646
  def translate_exception(exception, message:, sql:, binds:)
@@ -530,46 +650,44 @@ module ArJdbc
530
650
  # column *column_name* is not unique
531
651
  if exception.message.match?(/(column(s)? .* (is|are) not unique|UNIQUE constraint failed: .*)/i)
532
652
  # DIFFERENCE: FQN
533
- ::ActiveRecord::RecordNotUnique.new(message, sql: sql, binds: binds)
653
+ ::ActiveRecord::RecordNotUnique.new(message, sql: sql, binds: binds, connection_pool: @pool)
534
654
  elsif exception.message.match?(/(.* may not be NULL|NOT NULL constraint failed: .*)/i)
535
655
  # DIFFERENCE: FQN
536
- ::ActiveRecord::NotNullViolation.new(message, sql: sql, binds: binds)
656
+ ::ActiveRecord::NotNullViolation.new(message, sql: sql, binds: binds, connection_pool: @pool)
537
657
  elsif exception.message.match?(/FOREIGN KEY constraint failed/i)
538
658
  # DIFFERENCE: FQN
539
- ::ActiveRecord::InvalidForeignKey.new(message, sql: sql, binds: binds)
659
+ ::ActiveRecord::InvalidForeignKey.new(message, sql: sql, binds: binds, connection_pool: @pool)
540
660
  elsif exception.message.match?(/called on a closed database/i)
541
661
  # DIFFERENCE: FQN
542
- ::ActiveRecord::ConnectionNotEstablished.new(exception)
662
+ ::ActiveRecord::ConnectionNotEstablished.new(exception, connection_pool: @pool)
663
+ elsif exception.message.match?(/sql error/i)
664
+ ::ActiveRecord::StatementInvalid.new(message, sql: sql, binds: binds, connection_pool: @pool)
665
+ elsif exception.message.match?(/write a readonly database/i)
666
+ message = message.sub('org.sqlite.SQLiteException', 'SQLite3::ReadOnlyException')
667
+ ::ActiveRecord::StatementInvalid.new(message, sql: sql, binds: binds, connection_pool: @pool)
543
668
  else
544
669
  super
545
670
  end
546
671
  end
547
672
 
548
673
  COLLATE_REGEX = /.*\"(\w+)\".*collate\s+\"(\w+)\".*/i.freeze
674
+ PRIMARY_KEY_AUTOINCREMENT_REGEX = /.*\"(\w+)\".+PRIMARY KEY AUTOINCREMENT/i
675
+ GENERATED_ALWAYS_AS_REGEX = /.*"(\w+)".+GENERATED ALWAYS AS \((.+)\) (?:STORED|VIRTUAL)/i
549
676
 
550
677
  def table_structure_with_collation(table_name, basic_structure)
551
678
  collation_hash = {}
552
- sql = <<~SQL
553
- SELECT sql FROM
554
- (SELECT * FROM sqlite_master UNION ALL
555
- SELECT * FROM sqlite_temp_master)
556
- WHERE type = 'table' AND name = #{quote(table_name)}
557
- SQL
679
+ auto_increments = {}
680
+ generated_columns = {}
558
681
 
559
- # Result will have following sample string
560
- # CREATE TABLE "users" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
561
- # "password_digest" varchar COLLATE "NOCASE");
562
- result = query_value(sql, "SCHEMA")
682
+ column_strings = table_structure_sql(table_name, basic_structure.map { |column| column["name"] })
563
683
 
564
- if result
565
- # Splitting with left parentheses and discarding the first part will return all
566
- # columns separated with comma(,).
567
- columns_string = result.split("(", 2).last
568
-
569
- columns_string.split(",").each do |column_string|
684
+ if column_strings.any?
685
+ column_strings.each do |column_string|
570
686
  # This regex will match the column name and collation type and will save
571
687
  # the value in $1 and $2 respectively.
572
688
  collation_hash[$1] = $2 if COLLATE_REGEX =~ column_string
689
+ auto_increments[$1] = true if PRIMARY_KEY_AUTOINCREMENT_REGEX =~ column_string
690
+ generated_columns[$1] = $2 if GENERATED_ALWAYS_AS_REGEX =~ column_string
573
691
  end
574
692
 
575
693
  basic_structure.map do |column|
@@ -579,6 +697,14 @@ module ArJdbc
579
697
  column["collation"] = collation_hash[column_name]
580
698
  end
581
699
 
700
+ if auto_increments.has_key?(column_name)
701
+ column["auto_increment"] = true
702
+ end
703
+
704
+ if generated_columns.has_key?(column_name)
705
+ column["dflt_value"] = generated_columns[column_name]
706
+ end
707
+
582
708
  column
583
709
  end
584
710
  else
@@ -586,107 +712,96 @@ module ArJdbc
586
712
  end
587
713
  end
588
714
 
589
- def arel_visitor
590
- Arel::Visitors::SQLite.new(self)
591
- end
715
+ UNQUOTED_OPEN_PARENS_REGEX = /\((?![^'"]*['"][^'"]*$)/
716
+ FINAL_CLOSE_PARENS_REGEX = /\);*\z/
592
717
 
593
- def build_statement_pool
594
- StatementPool.new(self.class.type_cast_config_to_integer(@config[:statement_limit]))
595
- end
718
+ def table_structure_sql(table_name, column_names = nil)
719
+ unless column_names
720
+ column_info = table_info(table_name)
721
+ column_names = column_info.map { |column| column["name"] }
722
+ end
596
723
 
597
- def connect
598
- @connection = ::SQLite3::Database.new(
599
- @config[:database].to_s,
600
- @config.merge(results_as_hash: true)
601
- )
602
- end
724
+ sql = <<~SQL
725
+ SELECT sql FROM
726
+ (SELECT * FROM sqlite_master UNION ALL
727
+ SELECT * FROM sqlite_temp_master)
728
+ WHERE type = 'table' AND name = #{quote(table_name)}
729
+ SQL
603
730
 
604
- def configure_connection
605
- # FIXME: missing from adapter
606
- # @connection.busy_timeout(self.class.type_cast_config_to_integer(@config[:timeout])) if @config[:timeout]
731
+ # Result will have following sample string
732
+ # CREATE TABLE "users" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
733
+ # "password_digest" varchar COLLATE "NOCASE",
734
+ # "o_id" integer,
735
+ # CONSTRAINT "fk_rails_78146ddd2e" FOREIGN KEY ("o_id") REFERENCES "os" ("id"));
736
+ result = query_value(sql, "SCHEMA")
607
737
 
608
- execute("PRAGMA foreign_keys = ON", "SCHEMA")
609
- end
738
+ return [] unless result
610
739
 
611
- end
612
- # DIFFERENCE: A registration here is moved down to concrete class so we are not registering part of an adapter.
613
- end
740
+ # Splitting with left parentheses and discarding the first part will return all
741
+ # columns separated with comma(,).
742
+ result.partition(UNQUOTED_OPEN_PARENS_REGEX)
743
+ .last
744
+ .sub(FINAL_CLOSE_PARENS_REGEX, "")
745
+ # column definitions can have a comma in them, so split on commas followed
746
+ # by a space and a column name in quotes or followed by the keyword CONSTRAINT
747
+ .split(/,(?=\s(?:CONSTRAINT|"(?:#{Regexp.union(column_names).source})"))/i)
748
+ .map(&:strip)
749
+ end
614
750
 
615
- module ActiveRecord::ConnectionAdapters
616
- class SQLite3Column < JdbcColumn
617
- def initialize(name, *args)
618
- if Hash === name
619
- super
751
+ def table_info(table_name)
752
+ if supports_virtual_columns?
753
+ internal_exec_query("PRAGMA table_xinfo(#{quote_table_name(table_name)})", "SCHEMA")
620
754
  else
621
- super(nil, name, *args)
755
+ internal_exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", "SCHEMA")
622
756
  end
623
757
  end
624
758
 
625
- def self.string_to_binary(value)
626
- value
759
+ def arel_visitor
760
+ Arel::Visitors::SQLite.new(self)
627
761
  end
628
762
 
629
- def self.binary_to_string(value)
630
- if value.respond_to?(:encoding) && value.encoding != Encoding::ASCII_8BIT
631
- value = value.force_encoding(Encoding::ASCII_8BIT)
632
- end
633
- value
763
+ def build_statement_pool
764
+ StatementPool.new(self.class.type_cast_config_to_integer(@config[:statement_limit]))
634
765
  end
635
766
 
636
- # @override {ActiveRecord::ConnectionAdapters::JdbcColumn#init_column}
637
- def init_column(name, default, *args)
638
- if default =~ /NULL/
639
- @default = nil
767
+ def reconnect
768
+ if active?
769
+ @raw_connection.rollback rescue nil
640
770
  else
641
- super
771
+ connect
642
772
  end
643
773
  end
644
774
 
645
- # @override {ActiveRecord::ConnectionAdapters::JdbcColumn#default_value}
646
- def default_value(value)
647
- # JDBC returns column default strings with actual single quotes :
648
- return $1 if value =~ /^'(.*)'$/
649
-
650
- value
651
- end
652
-
653
- # @override {ActiveRecord::ConnectionAdapters::Column#type_cast}
654
- def type_cast(value)
655
- return nil if value.nil?
656
- case type
657
- when :string then value
658
- when :primary_key
659
- value.respond_to?(:to_i) ? value.to_i : ( value ? 1 : 0 )
660
- when :float then value.to_f
661
- when :decimal then self.class.value_to_decimal(value)
662
- when :boolean then self.class.value_to_boolean(value)
663
- else super
775
+ def configure_connection
776
+ if @config[:timeout] && @config[:retries]
777
+ raise ArgumentError, "Cannot specify both timeout and retries arguments"
778
+ elsif @config[:timeout]
779
+ # FIXME: missing from adapter
780
+ # @raw_connection.busy_timeout(self.class.type_cast_config_to_integer(@config[:timeout]))
781
+ elsif @config[:retries]
782
+ retries = self.class.type_cast_config_to_integer(@config[:retries])
783
+ raw_connection.busy_handler do |count|
784
+ count <= retries
785
+ end
664
786
  end
665
- end
666
-
667
- private
668
787
 
669
- # @override {ActiveRecord::ConnectionAdapters::Column#extract_limit}
670
- def extract_limit(sql_type)
671
- return nil if sql_type =~ /^(real)\(\d+/i
672
788
  super
673
- end
674
789
 
675
- def extract_precision(sql_type)
676
- case sql_type
677
- when /^(real)\((\d+)(,\d+)?\)/i then $2.to_i
678
- else super
679
- end
680
- end
681
-
682
- def extract_scale(sql_type)
683
- case sql_type
684
- when /^(real)\((\d+)\)/i then 0
685
- when /^(real)\((\d+)(,(\d+))\)/i then $4.to_i
686
- else super
790
+ pragmas = @config.fetch(:pragmas, {}).stringify_keys
791
+ DEFAULT_PRAGMAS.merge(pragmas).each do |pragma, value|
792
+ if ::SQLite3::Pragmas.respond_to?(pragma)
793
+ stmt = ::SQLite3::Pragmas.public_send(pragma, value)
794
+ raw_execute(stmt, "SCHEMA")
795
+ else
796
+ warn "Unknown SQLite pragma: #{pragma}"
797
+ end
687
798
  end
688
799
  end
689
800
  end
801
+ # DIFFERENCE: A registration here is moved down to concrete class so we are not registering part of an adapter.
802
+ end
803
+
804
+ module ActiveRecord::ConnectionAdapters
690
805
 
691
806
  remove_const(:SQLite3Adapter) if const_defined?(:SQLite3Adapter)
692
807
 
@@ -695,13 +810,77 @@ module ActiveRecord::ConnectionAdapters
695
810
  # ActiveRecord::ConnectionAdapters::SQLite3Adapter. Once we can do that we can remove the
696
811
  # module SQLite3 above and remove a majority of this file.
697
812
  class SQLite3Adapter < AbstractAdapter
813
+ ADAPTER_NAME = "SQLite"
814
+
815
+ class << self
816
+ def new_client(conn_params, adapter_instance)
817
+ jdbc_connection_class.new(conn_params, adapter_instance)
818
+ end
819
+
820
+ def dbconsole(config, options = {})
821
+ args = []
822
+
823
+ args << "-#{options[:mode]}" if options[:mode]
824
+ args << "-header" if options[:header]
825
+ args << File.expand_path(config.database, const_defined?(:Rails) && Rails.respond_to?(:root) ? Rails.root : nil)
826
+
827
+ find_cmd_and_exec("sqlite3", *args)
828
+ end
829
+
830
+ def jdbc_connection_class
831
+ ::ActiveRecord::ConnectionAdapters::SQLite3JdbcConnection
832
+ end
833
+ end
834
+
835
+ # NOTE: include these modules before all then override some methods with the
836
+ # ones defined in ArJdbc::SQLite3 java part and ArJdbc::Abstract
837
+ include ::ActiveRecord::ConnectionAdapters::SQLite3::Quoting
838
+ include ::ActiveRecord::ConnectionAdapters::SQLite3::SchemaStatements
839
+ include ::ActiveRecord::ConnectionAdapters::SQLite3::DatabaseStatements
840
+
698
841
  include ArJdbc::Abstract::Core
699
842
  include ArJdbc::SQLite3
843
+ include ArJdbc::SQLite3Config
844
+
700
845
  include ArJdbc::Abstract::ConnectionManagement
701
846
  include ArJdbc::Abstract::DatabaseStatements
702
847
  include ArJdbc::Abstract::StatementCache
703
848
  include ArJdbc::Abstract::TransactionSupport
704
849
 
850
+
851
+ ##
852
+ # :singleton-method:
853
+ # Configure the SQLite3Adapter to be used in a strict strings mode.
854
+ # This will disable double-quoted string literals, because otherwise typos can silently go unnoticed.
855
+ # For example, it is possible to create an index for a non existing column.
856
+ # If you wish to enable this mode you can add the following line to your application.rb file:
857
+ #
858
+ # config.active_record.sqlite3_adapter_strict_strings_by_default = true
859
+ class_attribute :strict_strings_by_default, default: false # Does not actually do anything right now
860
+
861
+ def initialize(...)
862
+ super
863
+
864
+ @memory_database = false
865
+ case @config[:database].to_s
866
+ when ""
867
+ raise ArgumentError, "No database file specified. Missing argument: database"
868
+ when ":memory:"
869
+ @memory_database = true
870
+ end
871
+
872
+ # assign arjdbc extra connection params
873
+ conn_params = build_connection_config(@config.compact)
874
+
875
+ # NOTE: strict strings is not supported by the jdbc driver yet,
876
+ # hope it will supported soon, I open a issue in their repository.
877
+ # https://github.com/xerial/sqlite-jdbc/issues/1153
878
+ #
879
+ # @config[:strict] = ConnectionAdapters::SQLite3Adapter.strict_strings_by_default unless @config.key?(:strict)
880
+
881
+ @connection_parameters = conn_params
882
+ end
883
+
705
884
  def self.represent_boolean_as_integer=(value) # :nodoc:
706
885
  if value == false
707
886
  raise "`.represent_boolean_as_integer=` is now always true, so make sure your application can work with it and remove this settings."
@@ -736,23 +915,15 @@ module ActiveRecord::ConnectionAdapters
736
915
  # SQLite driver doesn't support all types of insert statements with executeUpdate so
737
916
  # make it act like a regular query and the ids will be returned from #last_inserted_id
738
917
  # example: INSERT INTO "aircraft" DEFAULT VALUES
739
- def exec_insert(sql, name = nil, binds = [], pk = nil, sequence_name = nil)
740
- exec_query(sql, name, binds)
918
+ def exec_insert(sql, name = nil, binds = [], pk = nil, sequence_name = nil, returning: nil)
919
+ sql, binds = sql_for_insert(sql, pk, binds, returning)
920
+ internal_exec_query(sql, name, binds)
741
921
  end
742
922
 
743
923
  def jdbc_column_class
744
924
  ::ActiveRecord::ConnectionAdapters::SQLite3Column
745
925
  end
746
926
 
747
- def jdbc_connection_class(spec)
748
- self.class.jdbc_connection_class
749
- end
750
-
751
- # @see ActiveRecord::ConnectionAdapters::JdbcAdapter#jdbc_connection_class
752
- def self.jdbc_connection_class
753
- ::ActiveRecord::ConnectionAdapters::SQLite3JdbcConnection
754
- end
755
-
756
927
  # Note: This is not an override of ours but a moved line from AR Sqlite3Adapter to register ours vs our copied module (which would be their class).
757
928
  # ActiveSupport.run_load_hooks(:active_record_sqlite3adapter, SQLite3Adapter)
758
929