activerecord 7.1.6 → 7.2.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.
Files changed (193) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +839 -2248
  3. data/README.rdoc +16 -16
  4. data/examples/performance.rb +2 -2
  5. data/lib/active_record/association_relation.rb +1 -1
  6. data/lib/active_record/associations/alias_tracker.rb +31 -23
  7. data/lib/active_record/associations/association.rb +15 -8
  8. data/lib/active_record/associations/belongs_to_association.rb +31 -8
  9. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +3 -2
  10. data/lib/active_record/associations/builder/belongs_to.rb +1 -0
  11. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +2 -2
  12. data/lib/active_record/associations/builder/has_many.rb +3 -4
  13. data/lib/active_record/associations/builder/has_one.rb +3 -4
  14. data/lib/active_record/associations/collection_association.rb +16 -8
  15. data/lib/active_record/associations/collection_proxy.rb +14 -1
  16. data/lib/active_record/associations/errors.rb +265 -0
  17. data/lib/active_record/associations/has_many_association.rb +1 -1
  18. data/lib/active_record/associations/has_many_through_association.rb +7 -1
  19. data/lib/active_record/associations/join_dependency/join_association.rb +1 -1
  20. data/lib/active_record/associations/nested_error.rb +47 -0
  21. data/lib/active_record/associations/preloader/association.rb +2 -1
  22. data/lib/active_record/associations/preloader/branch.rb +7 -1
  23. data/lib/active_record/associations/preloader/through_association.rb +1 -3
  24. data/lib/active_record/associations/singular_association.rb +6 -0
  25. data/lib/active_record/associations/through_association.rb +1 -1
  26. data/lib/active_record/associations.rb +59 -292
  27. data/lib/active_record/attribute_assignment.rb +0 -2
  28. data/lib/active_record/attribute_methods/composite_primary_key.rb +84 -0
  29. data/lib/active_record/attribute_methods/primary_key.rb +23 -55
  30. data/lib/active_record/attribute_methods/read.rb +1 -13
  31. data/lib/active_record/attribute_methods/serialization.rb +5 -25
  32. data/lib/active_record/attribute_methods/time_zone_conversion.rb +7 -6
  33. data/lib/active_record/attribute_methods.rb +51 -60
  34. data/lib/active_record/attributes.rb +93 -68
  35. data/lib/active_record/autosave_association.rb +25 -32
  36. data/lib/active_record/base.rb +4 -5
  37. data/lib/active_record/callbacks.rb +1 -1
  38. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +24 -107
  39. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +1 -0
  40. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +294 -72
  41. data/lib/active_record/connection_adapters/abstract/database_statements.rb +34 -17
  42. data/lib/active_record/connection_adapters/abstract/query_cache.rb +201 -75
  43. data/lib/active_record/connection_adapters/abstract/quoting.rb +65 -91
  44. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +6 -2
  45. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +18 -6
  46. data/lib/active_record/connection_adapters/abstract/transaction.rb +125 -62
  47. data/lib/active_record/connection_adapters/abstract_adapter.rb +46 -44
  48. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +53 -15
  49. data/lib/active_record/connection_adapters/mysql/database_statements.rb +9 -1
  50. data/lib/active_record/connection_adapters/mysql/quoting.rb +43 -48
  51. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +6 -0
  52. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +19 -18
  53. data/lib/active_record/connection_adapters/mysql2_adapter.rb +12 -23
  54. data/lib/active_record/connection_adapters/pool_config.rb +7 -6
  55. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +27 -4
  56. data/lib/active_record/connection_adapters/postgresql/oid/interval.rb +1 -1
  57. data/lib/active_record/connection_adapters/postgresql/oid/uuid.rb +14 -4
  58. data/lib/active_record/connection_adapters/postgresql/quoting.rb +58 -58
  59. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +30 -8
  60. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +1 -1
  61. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +16 -12
  62. data/lib/active_record/connection_adapters/postgresql_adapter.rb +36 -26
  63. data/lib/active_record/connection_adapters/schema_cache.rb +123 -128
  64. data/lib/active_record/connection_adapters/sqlite3/column.rb +14 -1
  65. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +10 -6
  66. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +57 -46
  67. data/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +22 -0
  68. data/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb +13 -0
  69. data/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +16 -0
  70. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +26 -2
  71. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +133 -78
  72. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +15 -15
  73. data/lib/active_record/connection_adapters/trilogy_adapter.rb +19 -48
  74. data/lib/active_record/connection_adapters.rb +121 -0
  75. data/lib/active_record/connection_handling.rb +68 -49
  76. data/lib/active_record/core.rb +112 -44
  77. data/lib/active_record/counter_cache.rb +19 -10
  78. data/lib/active_record/database_configurations/connection_url_resolver.rb +9 -2
  79. data/lib/active_record/database_configurations/database_config.rb +19 -4
  80. data/lib/active_record/database_configurations/hash_config.rb +38 -34
  81. data/lib/active_record/database_configurations/url_config.rb +20 -1
  82. data/lib/active_record/database_configurations.rb +1 -1
  83. data/lib/active_record/delegated_type.rb +42 -18
  84. data/lib/active_record/dynamic_matchers.rb +2 -2
  85. data/lib/active_record/encryption/encryptable_record.rb +4 -4
  86. data/lib/active_record/encryption/encrypted_attribute_type.rb +25 -5
  87. data/lib/active_record/encryption/encryptor.rb +35 -19
  88. data/lib/active_record/encryption/key_provider.rb +1 -1
  89. data/lib/active_record/encryption/message_pack_message_serializer.rb +76 -0
  90. data/lib/active_record/encryption/message_serializer.rb +4 -0
  91. data/lib/active_record/encryption/null_encryptor.rb +4 -0
  92. data/lib/active_record/encryption/read_only_null_encryptor.rb +4 -0
  93. data/lib/active_record/enum.rb +31 -13
  94. data/lib/active_record/errors.rb +49 -23
  95. data/lib/active_record/explain.rb +13 -24
  96. data/lib/active_record/fixture_set/table_row.rb +19 -2
  97. data/lib/active_record/fixtures.rb +37 -31
  98. data/lib/active_record/future_result.rb +8 -4
  99. data/lib/active_record/gem_version.rb +2 -2
  100. data/lib/active_record/inheritance.rb +4 -2
  101. data/lib/active_record/insert_all.rb +18 -15
  102. data/lib/active_record/integration.rb +4 -1
  103. data/lib/active_record/internal_metadata.rb +48 -34
  104. data/lib/active_record/locking/optimistic.rb +7 -6
  105. data/lib/active_record/log_subscriber.rb +0 -21
  106. data/lib/active_record/message_pack.rb +1 -1
  107. data/lib/active_record/migration/command_recorder.rb +2 -3
  108. data/lib/active_record/migration/compatibility.rb +5 -3
  109. data/lib/active_record/migration/default_strategy.rb +4 -5
  110. data/lib/active_record/migration/pending_migration_connection.rb +2 -2
  111. data/lib/active_record/migration.rb +87 -77
  112. data/lib/active_record/model_schema.rb +31 -68
  113. data/lib/active_record/nested_attributes.rb +11 -3
  114. data/lib/active_record/normalization.rb +3 -7
  115. data/lib/active_record/persistence.rb +30 -352
  116. data/lib/active_record/query_cache.rb +19 -8
  117. data/lib/active_record/query_logs.rb +19 -0
  118. data/lib/active_record/querying.rb +25 -13
  119. data/lib/active_record/railtie.rb +39 -57
  120. data/lib/active_record/railties/controller_runtime.rb +13 -4
  121. data/lib/active_record/railties/databases.rake +42 -44
  122. data/lib/active_record/reflection.rb +98 -36
  123. data/lib/active_record/relation/batches/batch_enumerator.rb +15 -2
  124. data/lib/active_record/relation/batches.rb +14 -8
  125. data/lib/active_record/relation/calculations.rb +127 -89
  126. data/lib/active_record/relation/delegation.rb +8 -11
  127. data/lib/active_record/relation/finder_methods.rb +26 -12
  128. data/lib/active_record/relation/merger.rb +4 -6
  129. data/lib/active_record/relation/predicate_builder/array_handler.rb +2 -2
  130. data/lib/active_record/relation/predicate_builder/association_query_value.rb +10 -2
  131. data/lib/active_record/relation/predicate_builder.rb +3 -3
  132. data/lib/active_record/relation/query_attribute.rb +1 -1
  133. data/lib/active_record/relation/query_methods.rb +238 -65
  134. data/lib/active_record/relation/record_fetch_warning.rb +3 -0
  135. data/lib/active_record/relation/spawn_methods.rb +2 -18
  136. data/lib/active_record/relation/where_clause.rb +15 -21
  137. data/lib/active_record/relation.rb +508 -74
  138. data/lib/active_record/result.rb +31 -44
  139. data/lib/active_record/runtime_registry.rb +39 -0
  140. data/lib/active_record/sanitization.rb +24 -19
  141. data/lib/active_record/schema.rb +8 -6
  142. data/lib/active_record/schema_dumper.rb +48 -20
  143. data/lib/active_record/schema_migration.rb +30 -14
  144. data/lib/active_record/scoping/named.rb +1 -0
  145. data/lib/active_record/secure_token.rb +3 -3
  146. data/lib/active_record/signed_id.rb +27 -7
  147. data/lib/active_record/statement_cache.rb +7 -7
  148. data/lib/active_record/table_metadata.rb +1 -10
  149. data/lib/active_record/tasks/database_tasks.rb +69 -41
  150. data/lib/active_record/tasks/mysql_database_tasks.rb +1 -1
  151. data/lib/active_record/tasks/postgresql_database_tasks.rb +8 -1
  152. data/lib/active_record/tasks/sqlite_database_tasks.rb +2 -1
  153. data/lib/active_record/test_fixtures.rb +86 -89
  154. data/lib/active_record/testing/query_assertions.rb +121 -0
  155. data/lib/active_record/timestamp.rb +2 -2
  156. data/lib/active_record/token_for.rb +22 -12
  157. data/lib/active_record/touch_later.rb +1 -1
  158. data/lib/active_record/transaction.rb +132 -0
  159. data/lib/active_record/transactions.rb +73 -15
  160. data/lib/active_record/translation.rb +0 -2
  161. data/lib/active_record/type/serialized.rb +1 -3
  162. data/lib/active_record/type_caster/connection.rb +4 -4
  163. data/lib/active_record/validations/associated.rb +9 -3
  164. data/lib/active_record/validations/uniqueness.rb +15 -10
  165. data/lib/active_record/validations.rb +4 -1
  166. data/lib/active_record.rb +148 -39
  167. data/lib/arel/alias_predication.rb +1 -1
  168. data/lib/arel/collectors/bind.rb +3 -1
  169. data/lib/arel/collectors/composite.rb +7 -0
  170. data/lib/arel/collectors/sql_string.rb +1 -1
  171. data/lib/arel/collectors/substitute_binds.rb +1 -1
  172. data/lib/arel/crud.rb +2 -0
  173. data/lib/arel/delete_manager.rb +5 -0
  174. data/lib/arel/nodes/binary.rb +0 -6
  175. data/lib/arel/nodes/bound_sql_literal.rb +9 -5
  176. data/lib/arel/nodes/delete_statement.rb +4 -2
  177. data/lib/arel/nodes/{and.rb → nary.rb} +5 -2
  178. data/lib/arel/nodes/node.rb +4 -3
  179. data/lib/arel/nodes/sql_literal.rb +7 -0
  180. data/lib/arel/nodes/update_statement.rb +4 -2
  181. data/lib/arel/nodes.rb +2 -2
  182. data/lib/arel/predications.rb +1 -1
  183. data/lib/arel/select_manager.rb +7 -3
  184. data/lib/arel/tree_manager.rb +3 -2
  185. data/lib/arel/update_manager.rb +7 -1
  186. data/lib/arel/visitors/dot.rb +3 -0
  187. data/lib/arel/visitors/mysql.rb +9 -4
  188. data/lib/arel/visitors/postgresql.rb +1 -12
  189. data/lib/arel/visitors/sqlite.rb +25 -0
  190. data/lib/arel/visitors/to_sql.rb +31 -16
  191. data/lib/arel.rb +7 -3
  192. data/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt +4 -1
  193. metadata +16 -10
@@ -15,23 +15,13 @@ gem "sqlite3", ">= 1.4"
15
15
  require "sqlite3"
16
16
 
17
17
  module ActiveRecord
18
- module ConnectionHandling # :nodoc:
19
- def sqlite3_adapter_class
20
- ConnectionAdapters::SQLite3Adapter
21
- end
22
-
23
- def sqlite3_connection(config)
24
- sqlite3_adapter_class.new(config)
25
- end
26
- end
27
-
28
18
  module ConnectionAdapters # :nodoc:
29
19
  # = Active Record SQLite3 Adapter
30
20
  #
31
21
  # The SQLite3 adapter works with the sqlite3-ruby drivers
32
22
  # (available as gem from https://rubygems.org/gems/sqlite3).
33
23
  #
34
- # Options:
24
+ # ==== Options
35
25
  #
36
26
  # * <tt>:database</tt> - Path to the database file.
37
27
  class SQLite3Adapter < AbstractAdapter
@@ -88,6 +78,15 @@ module ActiveRecord
88
78
  json: { name: "json" },
89
79
  }
90
80
 
81
+ DEFAULT_PRAGMAS = {
82
+ "foreign_keys" => true,
83
+ "journal_mode" => :wal,
84
+ "synchronous" => :normal,
85
+ "mmap_size" => 134217728, # 128 megabytes
86
+ "journal_size_limit" => 67108864, # 64 megabytes
87
+ "cache_size" => 2000
88
+ }
89
+
91
90
  class StatementPool < ConnectionAdapters::StatementPool # :nodoc:
92
91
  alias reset clear
93
92
 
@@ -113,13 +112,9 @@ module ActiveRecord
113
112
  dirname = File.dirname(@config[:database])
114
113
  unless File.directory?(dirname)
115
114
  begin
116
- Dir.mkdir(dirname)
117
- rescue Errno::ENOENT => error
118
- if error.message.include?("No such file or directory")
119
- raise ActiveRecord::NoDatabaseError.new(connection_pool: @pool)
120
- else
121
- raise
122
- end
115
+ FileUtils.mkdir_p(dirname)
116
+ rescue SystemCallError
117
+ raise ActiveRecord::NoDatabaseError.new(connection_pool: @pool)
123
118
  end
124
119
  end
125
120
  end
@@ -196,12 +191,19 @@ module ActiveRecord
196
191
  !@memory_database
197
192
  end
198
193
 
199
- def active?
200
- @raw_connection && !@raw_connection.closed?
194
+ def supports_virtual_columns?
195
+ database_version >= "3.31.0"
196
+ end
197
+
198
+ def connected?
199
+ !(@raw_connection.nil? || @raw_connection.closed?)
201
200
  end
202
201
 
203
- def return_value_after_insert?(column) # :nodoc:
204
- column.auto_populated?
202
+ def active?
203
+ if connected?
204
+ verified!
205
+ true
206
+ end
205
207
  end
206
208
 
207
209
  alias :reset! :reconnect!
@@ -236,6 +238,10 @@ module ActiveRecord
236
238
  true
237
239
  end
238
240
 
241
+ def supports_deferrable_constraints?
242
+ true
243
+ end
244
+
239
245
  # REFERENTIAL INTEGRITY ====================================
240
246
 
241
247
  def disable_referential_integrity # :nodoc:
@@ -258,7 +264,7 @@ module ActiveRecord
258
264
 
259
265
  unless result.blank?
260
266
  tables = result.map { |row| row["table"] }
261
- raise ActiveRecord::StatementInvalid.new("Foreign key violations found: #{tables.join(", ")}", sql: sql)
267
+ raise ActiveRecord::StatementInvalid.new("Foreign key violations found: #{tables.join(", ")}", sql: sql, connection_pool: @pool)
262
268
  end
263
269
  end
264
270
 
@@ -290,6 +296,7 @@ module ActiveRecord
290
296
  end
291
297
 
292
298
  def add_column(table_name, column_name, type, **options) # :nodoc:
299
+ type = type.to_sym
293
300
  if invalid_alter_table_type?(type, options)
294
301
  alter_table(table_name) do |definition|
295
302
  definition.column(column_name, type, **options)
@@ -365,15 +372,31 @@ module ActiveRecord
365
372
  end
366
373
  alias :add_belongs_to :add_reference
367
374
 
375
+ FK_REGEX = /.*FOREIGN KEY\s+\("([^"]+)"\)\s+REFERENCES\s+"(\w+)"\s+\("(\w+)"\)/
376
+ DEFERRABLE_REGEX = /DEFERRABLE INITIALLY (\w+)/
368
377
  def foreign_keys(table_name)
369
378
  # SQLite returns 1 row for each column of composite foreign keys.
370
379
  fk_info = internal_exec_query("PRAGMA foreign_key_list(#{quote(table_name)})", "SCHEMA")
380
+ # Deferred or immediate foreign keys can only be seen in the CREATE TABLE sql
381
+ fk_defs = table_structure_sql(table_name)
382
+ .select do |column_string|
383
+ column_string.start_with?("CONSTRAINT") &&
384
+ column_string.include?("FOREIGN KEY")
385
+ end
386
+ .to_h do |fk_string|
387
+ _, from, table, to = fk_string.match(FK_REGEX).to_a
388
+ _, mode = fk_string.match(DEFERRABLE_REGEX).to_a
389
+ deferred = mode&.downcase&.to_sym || false
390
+ [[table, from, to], deferred]
391
+ end
392
+
371
393
  grouped_fk = fk_info.group_by { |row| row["id"] }.values.each { |group| group.sort_by! { |row| row["seq"] } }
372
394
  grouped_fk.map do |group|
373
395
  row = group.first
374
396
  options = {
375
397
  on_delete: extract_foreign_key_action(row["on_delete"]),
376
- on_update: extract_foreign_key_action(row["on_update"])
398
+ on_update: extract_foreign_key_action(row["on_update"]),
399
+ deferrable: fk_defs[[row["table"], row["from"], row["to"]]]
377
400
  }
378
401
 
379
402
  if group.one?
@@ -454,8 +477,8 @@ module ActiveRecord
454
477
  end
455
478
 
456
479
  def table_structure(table_name)
457
- structure = internal_exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", "SCHEMA")
458
- raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure.empty?
480
+ structure = table_info(table_name)
481
+ raise ActiveRecord::StatementInvalid.new("Could not find table '#{table_name}'", connection_pool: @pool) if structure.empty?
459
482
  table_structure_with_collation(table_name, structure)
460
483
  end
461
484
  alias column_definitions table_structure
@@ -494,8 +517,9 @@ module ActiveRecord
494
517
  # See: https://www.sqlite.org/lang_altertable.html
495
518
  # SQLite has an additional restriction on the ALTER TABLE statement
496
519
  def invalid_alter_table_type?(type, options)
497
- type.to_sym == :primary_key || options[:primary_key] ||
498
- options[:null] == false && options[:default].nil?
520
+ type == :primary_key || options[:primary_key] ||
521
+ options[:null] == false && options[:default].nil? ||
522
+ (type == :virtual && options[:stored])
499
523
  end
500
524
 
501
525
  def alter_table(
@@ -523,8 +547,8 @@ module ActiveRecord
523
547
  yield definition if block_given?
524
548
  end
525
549
 
526
- transaction do
527
- disable_referential_integrity do
550
+ disable_referential_integrity do
551
+ transaction do
528
552
  move_table(table_name, altered_table_name, options.merge(temporary: true))
529
553
  move_table(altered_table_name, table_name, &caller)
530
554
  end
@@ -551,12 +575,6 @@ module ActiveRecord
551
575
  options[:rename][column.name.to_sym] ||
552
576
  column.name) : column.name
553
577
 
554
- if column.has_default?
555
- type = lookup_cast_type_from_column(column)
556
- default = type.deserialize(column.default)
557
- default = -> { column.default_function } if default.nil?
558
- end
559
-
560
578
  column_options = {
561
579
  limit: column.limit,
562
580
  precision: column.precision,
@@ -566,19 +584,31 @@ module ActiveRecord
566
584
  primary_key: column_name == from_primary_key
567
585
  }
568
586
 
569
- unless column.auto_increment?
570
- column_options[:default] = default
587
+ if column.virtual?
588
+ column_options[:as] = column.default_function
589
+ column_options[:stored] = column.virtual_stored?
590
+ column_options[:type] = column.type
591
+ elsif column.has_default?
592
+ type = lookup_cast_type_from_column(column)
593
+ default = type.deserialize(column.default)
594
+ default = -> { column.default_function } if default.nil?
595
+
596
+ unless column.auto_increment?
597
+ column_options[:default] = default
598
+ end
571
599
  end
572
600
 
573
- column_type = column.bigint? ? :bigint : column.type
601
+ column_type = column.virtual? ? :virtual : (column.bigint? ? :bigint : column.type)
574
602
  @definition.column(column_name, column_type, **column_options)
575
603
  end
576
604
 
577
605
  yield @definition if block_given?
578
606
  end
579
607
  copy_table_indexes(from, to, options[:rename] || {})
608
+
609
+ columns_to_copy = @definition.columns.reject { |col| col.options.key?(:as) }.map(&:name)
580
610
  copy_table_contents(from, to,
581
- @definition.columns.map(&:name),
611
+ columns_to_copy,
582
612
  options[:rename] || {})
583
613
  end
584
614
 
@@ -643,32 +673,22 @@ module ActiveRecord
643
673
 
644
674
  COLLATE_REGEX = /.*"(\w+)".*collate\s+"(\w+)".*/i
645
675
  PRIMARY_KEY_AUTOINCREMENT_REGEX = /.*"(\w+)".+PRIMARY KEY AUTOINCREMENT/i
676
+ GENERATED_ALWAYS_AS_REGEX = /.*"(\w+)".+GENERATED ALWAYS AS \((.+)\) (?:STORED|VIRTUAL)/i
646
677
 
647
678
  def table_structure_with_collation(table_name, basic_structure)
648
679
  collation_hash = {}
649
680
  auto_increments = {}
650
- sql = <<~SQL
651
- SELECT sql FROM
652
- (SELECT * FROM sqlite_master UNION ALL
653
- SELECT * FROM sqlite_temp_master)
654
- WHERE type = 'table' AND name = #{quote(table_name)}
655
- SQL
656
-
657
- # Result will have following sample string
658
- # CREATE TABLE "users" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
659
- # "password_digest" varchar COLLATE "NOCASE");
660
- result = query_value(sql, "SCHEMA")
681
+ generated_columns = {}
661
682
 
662
- if result
663
- # Splitting with left parentheses and discarding the first part will return all
664
- # columns separated with comma(,).
665
- columns_string = result.split("(", 2).last
683
+ column_strings = table_structure_sql(table_name, basic_structure.map { |column| column["name"] })
666
684
 
667
- columns_string.split(",").each do |column_string|
685
+ if column_strings.any?
686
+ column_strings.each do |column_string|
668
687
  # This regex will match the column name and collation type and will save
669
688
  # the value in $1 and $2 respectively.
670
689
  collation_hash[$1] = $2 if COLLATE_REGEX =~ column_string
671
690
  auto_increments[$1] = true if PRIMARY_KEY_AUTOINCREMENT_REGEX =~ column_string
691
+ generated_columns[$1] = $2 if GENERATED_ALWAYS_AS_REGEX =~ column_string
672
692
  end
673
693
 
674
694
  basic_structure.map do |column|
@@ -682,6 +702,10 @@ module ActiveRecord
682
702
  column["auto_increment"] = true
683
703
  end
684
704
 
705
+ if generated_columns.has_key?(column_name)
706
+ column["dflt_value"] = generated_columns[column_name]
707
+ end
708
+
685
709
  column
686
710
  end
687
711
  else
@@ -689,6 +713,50 @@ module ActiveRecord
689
713
  end
690
714
  end
691
715
 
716
+ UNQUOTED_OPEN_PARENS_REGEX = /\((?![^'"]*['"][^'"]*$)/
717
+ FINAL_CLOSE_PARENS_REGEX = /\);*\z/
718
+
719
+ def table_structure_sql(table_name, column_names = nil)
720
+ unless column_names
721
+ column_info = table_info(table_name)
722
+ column_names = column_info.map { |column| column["name"] }
723
+ end
724
+
725
+ sql = <<~SQL
726
+ SELECT sql FROM
727
+ (SELECT * FROM sqlite_master UNION ALL
728
+ SELECT * FROM sqlite_temp_master)
729
+ WHERE type = 'table' AND name = #{quote(table_name)}
730
+ SQL
731
+
732
+ # Result will have following sample string
733
+ # CREATE TABLE "users" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
734
+ # "password_digest" varchar COLLATE "NOCASE",
735
+ # "o_id" integer,
736
+ # CONSTRAINT "fk_rails_78146ddd2e" FOREIGN KEY ("o_id") REFERENCES "os" ("id"));
737
+ result = query_value(sql, "SCHEMA")
738
+
739
+ return [] unless result
740
+
741
+ # Splitting with left parentheses and discarding the first part will return all
742
+ # columns separated with comma(,).
743
+ result.partition(UNQUOTED_OPEN_PARENS_REGEX)
744
+ .last
745
+ .sub(FINAL_CLOSE_PARENS_REGEX, "")
746
+ # column definitions can have a comma in them, so split on commas followed
747
+ # by a space and a column name in quotes or followed by the keyword CONSTRAINT
748
+ .split(/,(?=\s(?:CONSTRAINT|"(?:#{Regexp.union(column_names).source})"))/i)
749
+ .map(&:strip)
750
+ end
751
+
752
+ def table_info(table_name)
753
+ if supports_virtual_columns?
754
+ internal_exec_query("PRAGMA table_xinfo(#{quote_table_name(table_name)})", "SCHEMA")
755
+ else
756
+ internal_exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", "SCHEMA")
757
+ end
758
+ end
759
+
692
760
  def arel_visitor
693
761
  Arel::Visitors::SQLite.new(self)
694
762
  end
@@ -723,29 +791,16 @@ module ActiveRecord
723
791
  end
724
792
  end
725
793
 
726
- # Enforce foreign key constraints
727
- # https://www.sqlite.org/pragma.html#pragma_foreign_keys
728
- # https://www.sqlite.org/foreignkeys.html
729
- raw_execute("PRAGMA foreign_keys = ON", "SCHEMA")
730
- unless @memory_database
731
- # Journal mode WAL allows for greater concurrency (many readers + one writer)
732
- # https://www.sqlite.org/pragma.html#pragma_journal_mode
733
- raw_execute("PRAGMA journal_mode = WAL", "SCHEMA")
734
- # Set more relaxed level of database durability
735
- # 2 = "FULL" (sync on every write), 1 = "NORMAL" (sync every 1000 written pages) and 0 = "NONE"
736
- # https://www.sqlite.org/pragma.html#pragma_synchronous
737
- raw_execute("PRAGMA synchronous = NORMAL", "SCHEMA")
738
- # Set the global memory map so all processes can share some data
739
- # https://www.sqlite.org/pragma.html#pragma_mmap_size
740
- # https://www.sqlite.org/mmap.html
741
- raw_execute("PRAGMA mmap_size = #{128.megabytes}", "SCHEMA")
794
+ super
795
+
796
+ pragmas = @config.fetch(:pragmas, {}).stringify_keys
797
+ DEFAULT_PRAGMAS.merge(pragmas).each do |pragma, value|
798
+ if ::SQLite3::Pragmas.method_defined?("#{pragma}=")
799
+ @raw_connection.public_send("#{pragma}=", value)
800
+ else
801
+ warn "Unknown SQLite pragma: #{pragma}"
802
+ end
742
803
  end
743
- # Impose a limit on the WAL file to prevent unlimited growth
744
- # https://www.sqlite.org/pragma.html#pragma_journal_size_limit
745
- raw_execute("PRAGMA journal_size_limit = #{64.megabytes}", "SCHEMA")
746
- # Set the local connection cache to 2000 pages
747
- # https://www.sqlite.org/pragma.html#pragma_cache_size
748
- raw_execute("PRAGMA cache_size = 2000", "SCHEMA")
749
804
  end
750
805
  end
751
806
  ActiveSupport.run_load_hooks(:active_record_sqlite3adapter, SQLite3Adapter)
@@ -4,20 +4,12 @@ module ActiveRecord
4
4
  module ConnectionAdapters
5
5
  module Trilogy
6
6
  module DatabaseStatements
7
- def select_all(*, **) # :nodoc:
8
- result = super
9
- with_raw_connection do |conn|
10
- conn.next_result while conn.more_results_exist?
11
- end
12
- result
13
- end
14
-
15
- def internal_exec_query(sql, name = "SQL", binds = [], prepare: false, async: false) # :nodoc:
7
+ def internal_exec_query(sql, name = "SQL", binds = [], prepare: false, async: false, allow_retry: false) # :nodoc:
16
8
  sql = transform_query(sql)
17
9
  check_if_write_query(sql)
18
10
  mark_transaction_written_if_write(sql)
19
11
 
20
- result = raw_execute(sql, name, async: async)
12
+ result = raw_execute(sql, name, async: async, allow_retry: allow_retry)
21
13
  ActiveRecord::Result.new(result.fields, result.to_a)
22
14
  end
23
15
 
@@ -26,7 +18,8 @@ module ActiveRecord
26
18
  check_if_write_query(sql)
27
19
  mark_transaction_written_if_write(sql)
28
20
 
29
- raw_execute(to_sql(sql, binds), name)
21
+ sql, _binds = sql_for_insert(sql, pk, binds, returning)
22
+ raw_execute(sql, name)
30
23
  end
31
24
 
32
25
  def exec_delete(sql, name = nil, binds = []) # :nodoc:
@@ -42,19 +35,27 @@ module ActiveRecord
42
35
 
43
36
  private
44
37
  def raw_execute(sql, name, async: false, allow_retry: false, materialize_transactions: true)
45
- log(sql, name, async: async) do
38
+ log(sql, name, async: async) do |notification_payload|
46
39
  with_raw_connection(allow_retry: allow_retry, materialize_transactions: materialize_transactions) do |conn|
47
40
  sync_timezone_changes(conn)
48
41
  result = conn.query(sql)
42
+ while conn.more_results_exist?
43
+ conn.next_result
44
+ end
49
45
  verified!
50
46
  handle_warnings(sql)
47
+ notification_payload[:row_count] = result.count
51
48
  result
52
49
  end
53
50
  end
54
51
  end
55
52
 
56
53
  def last_inserted_id(result)
57
- result.last_insert_id
54
+ if supports_insert_returning?
55
+ super
56
+ else
57
+ result.last_insert_id
58
+ end
58
59
  end
59
60
 
60
61
  def sync_timezone_changes(conn)
@@ -71,7 +72,6 @@ module ActiveRecord
71
72
  combine_multi_statements(statements).each do |statement|
72
73
  with_raw_connection do |conn|
73
74
  raw_execute(statement, name)
74
- conn.next_result while conn.more_results_exist?
75
75
  end
76
76
  end
77
77
  end
@@ -90,7 +90,7 @@ module ActiveRecord
90
90
 
91
91
  yield
92
92
  ensure
93
- conn.set_server_option(::Trilogy::SET_SERVER_MULTI_STATEMENTS_OFF)
93
+ conn.set_server_option(::Trilogy::SET_SERVER_MULTI_STATEMENTS_OFF) if active?
94
94
  end
95
95
  end
96
96
  end
@@ -2,44 +2,17 @@
2
2
 
3
3
  require "active_record/connection_adapters/abstract_mysql_adapter"
4
4
 
5
- gem "trilogy", "~> 2.4"
5
+ gem "trilogy", "~> 2.7"
6
6
  require "trilogy"
7
7
 
8
8
  require "active_record/connection_adapters/trilogy/database_statements"
9
9
 
10
10
  module ActiveRecord
11
- module ConnectionHandling # :nodoc:
12
- def trilogy_adapter_class
13
- ConnectionAdapters::TrilogyAdapter
14
- end
15
-
16
- # Establishes a connection to the database that's used by all Active Record objects.
17
- def trilogy_connection(config)
18
- configuration = config.dup
19
-
20
- # Set FOUND_ROWS capability on the connection so UPDATE queries returns number of rows
21
- # matched rather than number of rows updated.
22
- configuration[:found_rows] = true
23
-
24
- options = [
25
- configuration[:host],
26
- configuration[:port],
27
- configuration[:database],
28
- configuration[:username],
29
- configuration[:password],
30
- configuration[:socket],
31
- 0
32
- ]
33
-
34
- trilogy_adapter_class.new nil, logger, options, configuration
35
- end
36
- end
37
11
  module ConnectionAdapters
38
12
  class TrilogyAdapter < AbstractMysqlAdapter
39
13
  ER_BAD_DB_ERROR = 1049
40
14
  ER_DBACCESS_DENIED_ERROR = 1044
41
15
  ER_ACCESS_DENIED_ERROR = 1045
42
- ER_SERVER_SHUTDOWN = 1053
43
16
 
44
17
  ADAPTER_NAME = "Trilogy"
45
18
 
@@ -99,16 +72,22 @@ module ActiveRecord
99
72
  end
100
73
  end
101
74
 
102
- def initialize(...)
103
- super
75
+ def initialize(config, *)
76
+ config = config.dup
77
+
78
+ # Trilogy ignores `socket` if `host is set. We want the opposite to allow
79
+ # configuring UNIX domain sockets via `DATABASE_URL`.
80
+ config.delete(:host) if config[:socket]
104
81
 
105
- if @config[:prepared_statements]
82
+ # Set FOUND_ROWS capability on the connection so UPDATE queries returns number of rows
83
+ # matched rather than number of rows updated.
84
+ config[:found_rows] = true
85
+
86
+ if config[:prepared_statements]
106
87
  raise ArgumentError, "Trilogy currently doesn't support prepared statements. Remove `prepared_statements: true` from your database configuration."
107
88
  end
108
89
 
109
- # Trilogy ignore `socket` if `host is set. We want the opposite to allow
110
- # configuring UNIX domain sockets via `DATABASE_URL`.
111
- @config.delete(:host) if @config[:socket]
90
+ super
112
91
  end
113
92
 
114
93
  TYPE_MAP = Type::TypeMap.new.tap { |m| initialize_type_map(m) }
@@ -137,14 +116,12 @@ module ActiveRecord
137
116
  true
138
117
  end
139
118
 
140
- def quote_string(string)
141
- with_raw_connection(allow_retry: true, materialize_transactions: false) do |conn|
142
- conn.escape(string)
143
- end
119
+ def connected?
120
+ !(@raw_connection.nil? || @raw_connection.closed?)
144
121
  end
145
122
 
146
123
  def active?
147
- !(@raw_connection.nil? || @raw_connection.closed?) && @lock.synchronize { @raw_connection&.ping } || false
124
+ connected? && @lock.synchronize { @raw_connection&.ping; verified! } || false
148
125
  rescue ::Trilogy::Error
149
126
  false
150
127
  end
@@ -206,7 +183,7 @@ module ActiveRecord
206
183
  end
207
184
 
208
185
  def full_version
209
- schema_cache.database_version.full_version_string
186
+ database_version.full_version_string
210
187
  end
211
188
 
212
189
  def get_full_version
@@ -219,18 +196,12 @@ module ActiveRecord
219
196
  if exception.is_a?(::Trilogy::TimeoutError) && !exception.error_code
220
197
  return ActiveRecord::AdapterTimeout.new(message, sql: sql, binds: binds, connection_pool: @pool)
221
198
  end
222
- error_code = exception.error_code if exception.respond_to?(:error_code)
223
-
224
- case error_code
225
- when ER_SERVER_SHUTDOWN
226
- return ConnectionFailed.new(message, connection_pool: @pool)
227
- end
228
199
 
229
200
  case exception
230
- when Errno::EPIPE, SocketError, IOError
201
+ when ::Trilogy::ConnectionClosed, ::Trilogy::EOFError
231
202
  return ConnectionFailed.new(message, connection_pool: @pool)
232
203
  when ::Trilogy::Error
233
- if /Connection reset by peer|TRILOGY_CLOSED_CONNECTION|TRILOGY_INVALID_SEQUENCE_ID|TRILOGY_UNEXPECTED_PACKET/.match?(exception.message)
204
+ if exception.is_a?(SystemCallError) || exception.message.include?("TRILOGY_INVALID_SEQUENCE_ID")
234
205
  return ConnectionFailed.new(message, connection_pool: @pool)
235
206
  end
236
207
  end