activerecord 7.1.5.1 → 7.2.0.beta1

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 (183) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +515 -2445
  3. data/README.rdoc +15 -15
  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 +25 -19
  7. data/lib/active_record/associations/association.rb +9 -8
  8. data/lib/active_record/associations/belongs_to_association.rb +14 -7
  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 +6 -4
  15. data/lib/active_record/associations/collection_proxy.rb +14 -1
  16. data/lib/active_record/associations/has_many_association.rb +1 -1
  17. data/lib/active_record/associations/join_dependency/join_association.rb +29 -28
  18. data/lib/active_record/associations/join_dependency.rb +5 -5
  19. data/lib/active_record/associations/nested_error.rb +47 -0
  20. data/lib/active_record/associations/preloader/association.rb +2 -1
  21. data/lib/active_record/associations/preloader/branch.rb +7 -1
  22. data/lib/active_record/associations/preloader/through_association.rb +1 -3
  23. data/lib/active_record/associations/singular_association.rb +6 -0
  24. data/lib/active_record/associations/through_association.rb +1 -1
  25. data/lib/active_record/associations.rb +33 -16
  26. data/lib/active_record/attribute_assignment.rb +1 -11
  27. data/lib/active_record/attribute_methods/composite_primary_key.rb +84 -0
  28. data/lib/active_record/attribute_methods/dirty.rb +1 -1
  29. data/lib/active_record/attribute_methods/primary_key.rb +23 -55
  30. data/lib/active_record/attribute_methods/read.rb +4 -16
  31. data/lib/active_record/attribute_methods/serialization.rb +4 -24
  32. data/lib/active_record/attribute_methods/time_zone_conversion.rb +7 -10
  33. data/lib/active_record/attribute_methods/write.rb +3 -3
  34. data/lib/active_record/attribute_methods.rb +60 -71
  35. data/lib/active_record/attributes.rb +55 -42
  36. data/lib/active_record/autosave_association.rb +13 -32
  37. data/lib/active_record/base.rb +2 -3
  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 +248 -65
  41. data/lib/active_record/connection_adapters/abstract/database_statements.rb +34 -17
  42. data/lib/active_record/connection_adapters/abstract/query_cache.rb +159 -74
  43. data/lib/active_record/connection_adapters/abstract/quoting.rb +65 -91
  44. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +1 -1
  45. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +14 -5
  46. data/lib/active_record/connection_adapters/abstract/transaction.rb +60 -57
  47. data/lib/active_record/connection_adapters/abstract_adapter.rb +18 -46
  48. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +32 -6
  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 +7 -1
  52. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +11 -5
  53. data/lib/active_record/connection_adapters/mysql2_adapter.rb +5 -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/cidr.rb +1 -1
  57. data/lib/active_record/connection_adapters/postgresql/oid/interval.rb +1 -1
  58. data/lib/active_record/connection_adapters/postgresql/oid/uuid.rb +14 -4
  59. data/lib/active_record/connection_adapters/postgresql/quoting.rb +58 -58
  60. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +20 -0
  61. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +15 -13
  62. data/lib/active_record/connection_adapters/postgresql_adapter.rb +26 -21
  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 +44 -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 +25 -2
  71. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +107 -75
  72. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +12 -6
  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 +56 -41
  76. data/lib/active_record/core.rb +53 -37
  77. data/lib/active_record/counter_cache.rb +18 -9
  78. data/lib/active_record/database_configurations/connection_url_resolver.rb +8 -3
  79. data/lib/active_record/database_configurations/database_config.rb +15 -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 +24 -0
  84. data/lib/active_record/dynamic_matchers.rb +2 -2
  85. data/lib/active_record/encryption/encryptable_record.rb +2 -2
  86. data/lib/active_record/encryption/encrypted_attribute_type.rb +22 -2
  87. data/lib/active_record/encryption/encryptor.rb +17 -2
  88. data/lib/active_record/encryption/message_pack_message_serializer.rb +76 -0
  89. data/lib/active_record/encryption/message_serializer.rb +4 -0
  90. data/lib/active_record/encryption/null_encryptor.rb +4 -0
  91. data/lib/active_record/encryption/read_only_null_encryptor.rb +4 -0
  92. data/lib/active_record/encryption.rb +0 -2
  93. data/lib/active_record/enum.rb +10 -1
  94. data/lib/active_record/errors.rb +16 -11
  95. data/lib/active_record/explain.rb +13 -24
  96. data/lib/active_record/fixtures.rb +37 -31
  97. data/lib/active_record/future_result.rb +8 -4
  98. data/lib/active_record/gem_version.rb +3 -3
  99. data/lib/active_record/inheritance.rb +4 -2
  100. data/lib/active_record/insert_all.rb +18 -15
  101. data/lib/active_record/integration.rb +4 -1
  102. data/lib/active_record/internal_metadata.rb +48 -34
  103. data/lib/active_record/locking/optimistic.rb +7 -6
  104. data/lib/active_record/log_subscriber.rb +0 -21
  105. data/lib/active_record/marshalling.rb +1 -4
  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 +85 -76
  112. data/lib/active_record/model_schema.rb +28 -68
  113. data/lib/active_record/nested_attributes.rb +13 -16
  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 +18 -6
  117. data/lib/active_record/query_logs.rb +15 -0
  118. data/lib/active_record/querying.rb +21 -9
  119. data/lib/active_record/railtie.rb +50 -62
  120. data/lib/active_record/railties/controller_runtime.rb +13 -4
  121. data/lib/active_record/railties/databases.rake +41 -44
  122. data/lib/active_record/reflection.rb +90 -35
  123. data/lib/active_record/relation/batches/batch_enumerator.rb +15 -2
  124. data/lib/active_record/relation/batches.rb +3 -3
  125. data/lib/active_record/relation/calculations.rb +94 -61
  126. data/lib/active_record/relation/delegation.rb +8 -11
  127. data/lib/active_record/relation/finder_methods.rb +16 -2
  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.rb +3 -3
  131. data/lib/active_record/relation/query_methods.rb +196 -57
  132. data/lib/active_record/relation/record_fetch_warning.rb +3 -0
  133. data/lib/active_record/relation/spawn_methods.rb +2 -18
  134. data/lib/active_record/relation/where_clause.rb +7 -19
  135. data/lib/active_record/relation.rb +496 -72
  136. data/lib/active_record/result.rb +31 -44
  137. data/lib/active_record/runtime_registry.rb +39 -0
  138. data/lib/active_record/sanitization.rb +24 -19
  139. data/lib/active_record/schema.rb +8 -6
  140. data/lib/active_record/schema_dumper.rb +19 -9
  141. data/lib/active_record/schema_migration.rb +30 -14
  142. data/lib/active_record/signed_id.rb +11 -1
  143. data/lib/active_record/statement_cache.rb +7 -7
  144. data/lib/active_record/table_metadata.rb +1 -10
  145. data/lib/active_record/tasks/database_tasks.rb +76 -70
  146. data/lib/active_record/tasks/mysql_database_tasks.rb +1 -1
  147. data/lib/active_record/tasks/postgresql_database_tasks.rb +1 -1
  148. data/lib/active_record/tasks/sqlite_database_tasks.rb +2 -1
  149. data/lib/active_record/test_fixtures.rb +81 -91
  150. data/lib/active_record/testing/query_assertions.rb +121 -0
  151. data/lib/active_record/timestamp.rb +1 -1
  152. data/lib/active_record/token_for.rb +22 -12
  153. data/lib/active_record/touch_later.rb +1 -1
  154. data/lib/active_record/transaction.rb +68 -0
  155. data/lib/active_record/transactions.rb +43 -14
  156. data/lib/active_record/translation.rb +0 -2
  157. data/lib/active_record/type/serialized.rb +1 -3
  158. data/lib/active_record/type_caster/connection.rb +4 -4
  159. data/lib/active_record/validations/associated.rb +9 -3
  160. data/lib/active_record/validations/uniqueness.rb +14 -10
  161. data/lib/active_record/validations.rb +4 -1
  162. data/lib/active_record.rb +149 -40
  163. data/lib/arel/alias_predication.rb +1 -1
  164. data/lib/arel/collectors/bind.rb +2 -0
  165. data/lib/arel/collectors/composite.rb +7 -0
  166. data/lib/arel/collectors/sql_string.rb +1 -1
  167. data/lib/arel/collectors/substitute_binds.rb +1 -1
  168. data/lib/arel/nodes/binary.rb +0 -6
  169. data/lib/arel/nodes/bound_sql_literal.rb +9 -5
  170. data/lib/arel/nodes/{and.rb → nary.rb} +5 -2
  171. data/lib/arel/nodes/node.rb +4 -3
  172. data/lib/arel/nodes/sql_literal.rb +7 -0
  173. data/lib/arel/nodes.rb +2 -2
  174. data/lib/arel/predications.rb +1 -1
  175. data/lib/arel/select_manager.rb +1 -1
  176. data/lib/arel/tree_manager.rb +3 -2
  177. data/lib/arel/update_manager.rb +2 -1
  178. data/lib/arel/visitors/dot.rb +1 -0
  179. data/lib/arel/visitors/mysql.rb +9 -4
  180. data/lib/arel/visitors/postgresql.rb +1 -12
  181. data/lib/arel/visitors/to_sql.rb +29 -16
  182. data/lib/arel.rb +7 -3
  183. metadata +20 -15
@@ -5,6 +5,18 @@ module ActiveRecord
5
5
  module SQLite3
6
6
  class SchemaCreation < SchemaCreation # :nodoc:
7
7
  private
8
+ def visit_AddForeignKey(o)
9
+ super.dup.tap do |sql|
10
+ sql << " DEFERRABLE INITIALLY #{o.options[:deferrable].to_s.upcase}" if o.deferrable
11
+ end
12
+ end
13
+
14
+ def visit_ForeignKeyDefinition(o)
15
+ super.dup.tap do |sql|
16
+ sql << " DEFERRABLE INITIALLY #{o.deferrable.to_s.upcase}" if o.deferrable
17
+ end
18
+ end
19
+
8
20
  def supports_index_using?
9
21
  false
10
22
  end
@@ -13,6 +25,16 @@ module ActiveRecord
13
25
  if options[:collation]
14
26
  sql << " COLLATE \"#{options[:collation]}\""
15
27
  end
28
+
29
+ if as = options[:as]
30
+ sql << " GENERATED ALWAYS AS (#{as})"
31
+
32
+ if options[:stored]
33
+ sql << " STORED"
34
+ else
35
+ sql << " VIRTUAL"
36
+ end
37
+ end
16
38
  super
17
39
  end
18
40
  end
@@ -16,10 +16,23 @@ module ActiveRecord
16
16
  end
17
17
  alias :belongs_to :references
18
18
 
19
+ def new_column_definition(name, type, **options) # :nodoc:
20
+ case type
21
+ when :virtual
22
+ type = options[:type]
23
+ end
24
+
25
+ super
26
+ end
27
+
19
28
  private
20
29
  def integer_like_primary_key_type(type, options)
21
30
  :primary_key
22
31
  end
32
+
33
+ def valid_column_definition_options
34
+ super + [:as, :type, :stored]
35
+ end
23
36
  end
24
37
  end
25
38
  end
@@ -12,6 +12,22 @@ module ActiveRecord
12
12
  def explicit_primary_key_default?(column)
13
13
  column.bigint?
14
14
  end
15
+
16
+ def prepare_column_options(column)
17
+ spec = super
18
+
19
+ if @connection.supports_virtual_columns? && column.virtual?
20
+ spec[:as] = extract_expression_for_virtual_column(column)
21
+ spec[:stored] = column.virtual_stored?
22
+ spec = { type: schema_type(column).inspect }.merge!(spec)
23
+ end
24
+
25
+ spec
26
+ end
27
+
28
+ def extract_expression_for_virtual_column(column)
29
+ column.default_function.inspect
30
+ end
15
31
  end
16
32
  end
17
33
  end
@@ -53,6 +53,8 @@ module ActiveRecord
53
53
  end
54
54
 
55
55
  def add_foreign_key(from_table, to_table, **options)
56
+ assert_valid_deferrable(options[:deferrable])
57
+
56
58
  alter_table(from_table) do |definition|
57
59
  to_table = strip_table_name_prefix_and_suffix(to_table)
58
60
  definition.foreign_key(to_table, **options)
@@ -137,7 +139,14 @@ module ActiveRecord
137
139
 
138
140
  type_metadata = fetch_type_metadata(field["type"])
139
141
  default_value = extract_value_from_default(default)
140
- default_function = extract_default_function(default_value, default)
142
+ generated_type = extract_generated_type(field)
143
+
144
+ if generated_type.present?
145
+ default_function = default
146
+ else
147
+ default_function = extract_default_function(default_value, default)
148
+ end
149
+
141
150
  rowid = is_column_the_rowid?(field, definitions)
142
151
 
143
152
  Column.new(
@@ -148,7 +157,8 @@ module ActiveRecord
148
157
  default_function,
149
158
  collation: field["collation"],
150
159
  auto_increment: field["auto_increment"],
151
- rowid: rowid
160
+ rowid: rowid,
161
+ generated_type: generated_type
152
162
  )
153
163
  end
154
164
 
@@ -185,6 +195,19 @@ module ActiveRecord
185
195
  scope[:type] = type if type
186
196
  scope
187
197
  end
198
+
199
+ def assert_valid_deferrable(deferrable)
200
+ return if !deferrable || %i(immediate deferred).include?(deferrable)
201
+
202
+ raise ArgumentError, "deferrable must be `:immediate` or `:deferred`, got: `#{deferrable.inspect}`"
203
+ end
204
+
205
+ def extract_generated_type(field)
206
+ case field["hidden"]
207
+ when 2 then :virtual
208
+ when 3 then :stored
209
+ end
210
+ end
188
211
  end
189
212
  end
190
213
  end
@@ -15,16 +15,6 @@ 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
  #
@@ -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,14 +191,16 @@ 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"
201
196
  end
202
197
 
203
- def return_value_after_insert?(column) # :nodoc:
204
- column.auto_populated?
198
+ def connected?
199
+ !(@raw_connection.nil? || @raw_connection.closed?)
205
200
  end
206
201
 
202
+ alias_method :active?, :connected?
203
+
207
204
  alias :reset! :reconnect!
208
205
 
209
206
  # Disconnects from the database if already connected. Otherwise, this
@@ -236,6 +233,10 @@ module ActiveRecord
236
233
  true
237
234
  end
238
235
 
236
+ def supports_deferrable_constraints?
237
+ true
238
+ end
239
+
239
240
  # REFERENTIAL INTEGRITY ====================================
240
241
 
241
242
  def disable_referential_integrity # :nodoc:
@@ -258,7 +259,7 @@ module ActiveRecord
258
259
 
259
260
  unless result.blank?
260
261
  tables = result.map { |row| row["table"] }
261
- raise ActiveRecord::StatementInvalid.new("Foreign key violations found: #{tables.join(", ")}", sql: sql)
262
+ raise ActiveRecord::StatementInvalid.new("Foreign key violations found: #{tables.join(", ")}", sql: sql, connection_pool: @pool)
262
263
  end
263
264
  end
264
265
 
@@ -290,6 +291,7 @@ module ActiveRecord
290
291
  end
291
292
 
292
293
  def add_column(table_name, column_name, type, **options) # :nodoc:
294
+ type = type.to_sym
293
295
  if invalid_alter_table_type?(type, options)
294
296
  alter_table(table_name) do |definition|
295
297
  definition.column(column_name, type, **options)
@@ -365,15 +367,31 @@ module ActiveRecord
365
367
  end
366
368
  alias :add_belongs_to :add_reference
367
369
 
370
+ FK_REGEX = /.*FOREIGN KEY\s+\("(\w+)"\)\s+REFERENCES\s+"(\w+)"\s+\("(\w+)"\)/
371
+ DEFERRABLE_REGEX = /DEFERRABLE INITIALLY (\w+)/
368
372
  def foreign_keys(table_name)
369
373
  # SQLite returns 1 row for each column of composite foreign keys.
370
374
  fk_info = internal_exec_query("PRAGMA foreign_key_list(#{quote(table_name)})", "SCHEMA")
375
+ # Deferred or immediate foreign keys can only be seen in the CREATE TABLE sql
376
+ fk_defs = table_structure_sql(table_name)
377
+ .select do |column_string|
378
+ column_string.start_with?("CONSTRAINT") &&
379
+ column_string.include?("FOREIGN KEY")
380
+ end
381
+ .to_h do |fk_string|
382
+ _, from, table, to = fk_string.match(FK_REGEX).to_a
383
+ _, mode = fk_string.match(DEFERRABLE_REGEX).to_a
384
+ deferred = mode&.downcase&.to_sym || false
385
+ [[table, from, to], deferred]
386
+ end
387
+
371
388
  grouped_fk = fk_info.group_by { |row| row["id"] }.values.each { |group| group.sort_by! { |row| row["seq"] } }
372
389
  grouped_fk.map do |group|
373
390
  row = group.first
374
391
  options = {
375
392
  on_delete: extract_foreign_key_action(row["on_delete"]),
376
- on_update: extract_foreign_key_action(row["on_update"])
393
+ on_update: extract_foreign_key_action(row["on_update"]),
394
+ deferrable: fk_defs[[row["table"], row["from"], row["to"]]]
377
395
  }
378
396
 
379
397
  if group.one?
@@ -454,8 +472,12 @@ module ActiveRecord
454
472
  end
455
473
 
456
474
  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?
475
+ structure = if supports_virtual_columns?
476
+ internal_exec_query("PRAGMA table_xinfo(#{quote_table_name(table_name)})", "SCHEMA")
477
+ else
478
+ internal_exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", "SCHEMA")
479
+ end
480
+ raise ActiveRecord::StatementInvalid.new("Could not find table '#{table_name}'", connection_pool: @pool) if structure.empty?
459
481
  table_structure_with_collation(table_name, structure)
460
482
  end
461
483
  alias column_definitions table_structure
@@ -494,8 +516,9 @@ module ActiveRecord
494
516
  # See: https://www.sqlite.org/lang_altertable.html
495
517
  # SQLite has an additional restriction on the ALTER TABLE statement
496
518
  def invalid_alter_table_type?(type, options)
497
- type.to_sym == :primary_key || options[:primary_key] ||
498
- options[:null] == false && options[:default].nil?
519
+ type == :primary_key || options[:primary_key] ||
520
+ options[:null] == false && options[:default].nil? ||
521
+ (type == :virtual && options[:stored])
499
522
  end
500
523
 
501
524
  def alter_table(
@@ -551,12 +574,6 @@ module ActiveRecord
551
574
  options[:rename][column.name.to_sym] ||
552
575
  column.name) : column.name
553
576
 
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
577
  column_options = {
561
578
  limit: column.limit,
562
579
  precision: column.precision,
@@ -566,19 +583,31 @@ module ActiveRecord
566
583
  primary_key: column_name == from_primary_key
567
584
  }
568
585
 
569
- unless column.auto_increment?
570
- column_options[:default] = default
586
+ if column.virtual?
587
+ column_options[:as] = column.default_function
588
+ column_options[:stored] = column.virtual_stored?
589
+ column_options[:type] = column.type
590
+ elsif column.has_default?
591
+ type = lookup_cast_type_from_column(column)
592
+ default = type.deserialize(column.default)
593
+ default = -> { column.default_function } if default.nil?
594
+
595
+ unless column.auto_increment?
596
+ column_options[:default] = default
597
+ end
571
598
  end
572
599
 
573
- column_type = column.bigint? ? :bigint : column.type
600
+ column_type = column.virtual? ? :virtual : (column.bigint? ? :bigint : column.type)
574
601
  @definition.column(column_name, column_type, **column_options)
575
602
  end
576
603
 
577
604
  yield @definition if block_given?
578
605
  end
579
606
  copy_table_indexes(from, to, options[:rename] || {})
607
+
608
+ columns_to_copy = @definition.columns.reject { |col| col.options.key?(:as) }.map(&:name)
580
609
  copy_table_contents(from, to,
581
- @definition.columns.map(&:name),
610
+ columns_to_copy,
582
611
  options[:rename] || {})
583
612
  end
584
613
 
@@ -643,32 +672,22 @@ module ActiveRecord
643
672
 
644
673
  COLLATE_REGEX = /.*"(\w+)".*collate\s+"(\w+)".*/i
645
674
  PRIMARY_KEY_AUTOINCREMENT_REGEX = /.*"(\w+)".+PRIMARY KEY AUTOINCREMENT/i
675
+ GENERATED_ALWAYS_AS_REGEX = /.*"(\w+)".+GENERATED ALWAYS AS \((.+)\) (?:STORED|VIRTUAL)/i
646
676
 
647
677
  def table_structure_with_collation(table_name, basic_structure)
648
678
  collation_hash = {}
649
679
  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")
680
+ generated_columns = {}
661
681
 
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
682
+ column_strings = table_structure_sql(table_name)
666
683
 
667
- columns_string.split(",").each do |column_string|
684
+ if column_strings.any?
685
+ column_strings.each do |column_string|
668
686
  # This regex will match the column name and collation type and will save
669
687
  # the value in $1 and $2 respectively.
670
688
  collation_hash[$1] = $2 if COLLATE_REGEX =~ column_string
671
689
  auto_increments[$1] = true if PRIMARY_KEY_AUTOINCREMENT_REGEX =~ column_string
690
+ generated_columns[$1] = $2 if GENERATED_ALWAYS_AS_REGEX =~ column_string
672
691
  end
673
692
 
674
693
  basic_structure.map do |column|
@@ -682,6 +701,10 @@ module ActiveRecord
682
701
  column["auto_increment"] = true
683
702
  end
684
703
 
704
+ if generated_columns.has_key?(column_name)
705
+ column["dflt_value"] = generated_columns[column_name]
706
+ end
707
+
685
708
  column
686
709
  end
687
710
  else
@@ -689,6 +712,28 @@ module ActiveRecord
689
712
  end
690
713
  end
691
714
 
715
+ def table_structure_sql(table_name)
716
+ sql = <<~SQL
717
+ SELECT sql FROM
718
+ (SELECT * FROM sqlite_master UNION ALL
719
+ SELECT * FROM sqlite_temp_master)
720
+ WHERE type = 'table' AND name = #{quote(table_name)}
721
+ SQL
722
+
723
+ # Result will have following sample string
724
+ # CREATE TABLE "users" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
725
+ # "password_digest" varchar COLLATE "NOCASE");
726
+ result = query_value(sql, "SCHEMA")
727
+
728
+ return [] unless result
729
+
730
+ # Splitting with left parentheses and discarding the first part will return all
731
+ # columns separated with comma(,).
732
+ columns_string = result.split("(", 2).last
733
+
734
+ columns_string.split(",").map(&:strip)
735
+ end
736
+
692
737
  def arel_visitor
693
738
  Arel::Visitors::SQLite.new(self)
694
739
  end
@@ -723,29 +768,16 @@ module ActiveRecord
723
768
  end
724
769
  end
725
770
 
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")
771
+ super
772
+
773
+ pragmas = @config.fetch(:pragmas, {}).stringify_keys
774
+ DEFAULT_PRAGMAS.merge(pragmas).each do |pragma, value|
775
+ if ::SQLite3::Pragmas.method_defined?("#{pragma}=")
776
+ @raw_connection.public_send("#{pragma}=", value)
777
+ else
778
+ warn "Unknown SQLite pragma: #{pragma}"
779
+ end
742
780
  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
781
  end
750
782
  end
751
783
  ActiveSupport.run_load_hooks(:active_record_sqlite3adapter, SQLite3Adapter)
@@ -12,12 +12,12 @@ module ActiveRecord
12
12
  result
13
13
  end
14
14
 
15
- def internal_exec_query(sql, name = "SQL", binds = [], prepare: false, async: false) # :nodoc:
15
+ def internal_exec_query(sql, name = "SQL", binds = [], prepare: false, async: false, allow_retry: false) # :nodoc:
16
16
  sql = transform_query(sql)
17
17
  check_if_write_query(sql)
18
18
  mark_transaction_written_if_write(sql)
19
19
 
20
- result = raw_execute(sql, name, async: async)
20
+ result = raw_execute(sql, name, async: async, allow_retry: allow_retry)
21
21
  ActiveRecord::Result.new(result.fields, result.to_a)
22
22
  end
23
23
 
@@ -26,7 +26,8 @@ module ActiveRecord
26
26
  check_if_write_query(sql)
27
27
  mark_transaction_written_if_write(sql)
28
28
 
29
- raw_execute(to_sql(sql, binds), name)
29
+ sql, _binds = sql_for_insert(sql, pk, binds, returning)
30
+ raw_execute(sql, name)
30
31
  end
31
32
 
32
33
  def exec_delete(sql, name = nil, binds = []) # :nodoc:
@@ -42,19 +43,24 @@ module ActiveRecord
42
43
 
43
44
  private
44
45
  def raw_execute(sql, name, async: false, allow_retry: false, materialize_transactions: true)
45
- log(sql, name, async: async) do
46
+ log(sql, name, async: async) do |notification_payload|
46
47
  with_raw_connection(allow_retry: allow_retry, materialize_transactions: materialize_transactions) do |conn|
47
48
  sync_timezone_changes(conn)
48
49
  result = conn.query(sql)
49
50
  verified!
50
51
  handle_warnings(sql)
52
+ notification_payload[:row_count] = result.count
51
53
  result
52
54
  end
53
55
  end
54
56
  end
55
57
 
56
58
  def last_inserted_id(result)
57
- result.last_insert_id
59
+ if supports_insert_returning?
60
+ super
61
+ else
62
+ result.last_insert_id
63
+ end
58
64
  end
59
65
 
60
66
  def sync_timezone_changes(conn)
@@ -90,7 +96,7 @@ module ActiveRecord
90
96
 
91
97
  yield
92
98
  ensure
93
- conn.set_server_option(::Trilogy::SET_SERVER_MULTI_STATEMENTS_OFF)
99
+ conn.set_server_option(::Trilogy::SET_SERVER_MULTI_STATEMENTS_OFF) if active?
94
100
  end
95
101
  end
96
102
  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 } || 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