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
@@ -7,6 +7,67 @@ module ActiveRecord
7
7
  module ConnectionAdapters # :nodoc:
8
8
  # = Active Record Connection Adapters \Quoting
9
9
  module Quoting
10
+ extend ActiveSupport::Concern
11
+
12
+ module ClassMethods # :nodoc:
13
+ # Regexp for column names (with or without a table name prefix).
14
+ # Matches the following:
15
+ #
16
+ # "#{table_name}.#{column_name}"
17
+ # "#{column_name}"
18
+ def column_name_matcher
19
+ /
20
+ \A
21
+ (
22
+ (?:
23
+ # table_name.column_name | function(one or no argument)
24
+ ((?:\w+\.)?\w+ | \w+\((?:|\g<2>)\))
25
+ )
26
+ (?:(?:\s+AS)?\s+\w+)?
27
+ )
28
+ (?:\s*,\s*\g<1>)*
29
+ \z
30
+ /ix
31
+ end
32
+
33
+ # Regexp for column names with order (with or without a table name prefix,
34
+ # with or without various order modifiers). Matches the following:
35
+ #
36
+ # "#{table_name}.#{column_name}"
37
+ # "#{table_name}.#{column_name} #{direction}"
38
+ # "#{table_name}.#{column_name} #{direction} NULLS FIRST"
39
+ # "#{table_name}.#{column_name} NULLS LAST"
40
+ # "#{column_name}"
41
+ # "#{column_name} #{direction}"
42
+ # "#{column_name} #{direction} NULLS FIRST"
43
+ # "#{column_name} NULLS LAST"
44
+ def column_name_with_order_matcher
45
+ /
46
+ \A
47
+ (
48
+ (?:
49
+ # table_name.column_name | function(one or no argument)
50
+ ((?:\w+\.)?\w+ | \w+\((?:|\g<2>)\))
51
+ )
52
+ (?:\s+ASC|\s+DESC)?
53
+ (?:\s+NULLS\s+(?:FIRST|LAST))?
54
+ )
55
+ (?:\s*,\s*\g<1>)*
56
+ \z
57
+ /ix
58
+ end
59
+
60
+ # Quotes the column name. Must be implemented by subclasses
61
+ def quote_column_name(column_name)
62
+ raise NotImplementedError
63
+ end
64
+
65
+ # Quotes the table name. Defaults to column name quoting.
66
+ def quote_table_name(table_name)
67
+ quote_column_name(table_name)
68
+ end
69
+ end
70
+
10
71
  # Quotes the column value to help prevent
11
72
  # {SQL injection attacks}[https://en.wikipedia.org/wiki/SQL_injection].
12
73
  def quote(value)
@@ -23,9 +84,6 @@ module ActiveRecord
23
84
  when Type::Time::Value then "'#{quoted_time(value)}'"
24
85
  when Date, Time then "'#{quoted_date(value)}'"
25
86
  when Class then "'#{value}'"
26
- when ActiveSupport::Duration
27
- warn_quote_duration_deprecated
28
- value.to_s
29
87
  else raise TypeError, "can't quote #{value.class.name}"
30
88
  end
31
89
  end
@@ -48,21 +106,6 @@ module ActiveRecord
48
106
  end
49
107
  end
50
108
 
51
- # Quote a value to be used as a bound parameter of unknown type. For example,
52
- # MySQL might perform dangerous castings when comparing a string to a number,
53
- # so this method will cast numbers to string.
54
- #
55
- # Deprecated: Consider `Arel.sql("... ? ...", value)` or
56
- # +sanitize_sql+ instead.
57
- def quote_bound_value(value)
58
- ActiveRecord.deprecator.warn(<<~MSG.squish)
59
- #quote_bound_value is deprecated and will be removed in Rails 7.2.
60
- Consider Arel.sql(".. ? ..", value) or #sanitize_sql instead.
61
- MSG
62
-
63
- quote(cast_bound_value(value))
64
- end
65
-
66
109
  # Cast a value to be used as a bound parameter of unknown type. For example,
67
110
  # MySQL might perform dangerous castings when comparing a string to a number,
68
111
  # so this method will cast numbers to string.
@@ -89,14 +132,14 @@ module ActiveRecord
89
132
  s.gsub("\\", '\&\&').gsub("'", "''") # ' (for ruby-mode)
90
133
  end
91
134
 
92
- # Quotes the column name. Defaults to no quoting.
135
+ # Quotes the column name.
93
136
  def quote_column_name(column_name)
94
- column_name.to_s
137
+ self.class.quote_column_name(column_name)
95
138
  end
96
139
 
97
- # Quotes the table name. Defaults to column name quoting.
140
+ # Quotes the table name.
98
141
  def quote_table_name(table_name)
99
- quote_column_name(table_name)
142
+ self.class.quote_table_name(table_name)
100
143
  end
101
144
 
102
145
  # Override to return the quoted table name for assignment. Defaults to
@@ -177,59 +220,6 @@ module ActiveRecord
177
220
  comment
178
221
  end
179
222
 
180
- def column_name_matcher # :nodoc:
181
- COLUMN_NAME
182
- end
183
-
184
- def column_name_with_order_matcher # :nodoc:
185
- COLUMN_NAME_WITH_ORDER
186
- end
187
-
188
- # Regexp for column names (with or without a table name prefix).
189
- # Matches the following:
190
- #
191
- # "#{table_name}.#{column_name}"
192
- # "#{column_name}"
193
- COLUMN_NAME = /
194
- \A
195
- (
196
- (?:
197
- # table_name.column_name | function(one or no argument)
198
- ((?:\w+\.)?\w+ | \w+\((?:|\g<2>)\))
199
- )
200
- (?:(?:\s+AS)?\s+\w+)?
201
- )
202
- (?:\s*,\s*\g<1>)*
203
- \z
204
- /ix
205
-
206
- # Regexp for column names with order (with or without a table name prefix,
207
- # with or without various order modifiers). Matches the following:
208
- #
209
- # "#{table_name}.#{column_name}"
210
- # "#{table_name}.#{column_name} #{direction}"
211
- # "#{table_name}.#{column_name} #{direction} NULLS FIRST"
212
- # "#{table_name}.#{column_name} NULLS LAST"
213
- # "#{column_name}"
214
- # "#{column_name} #{direction}"
215
- # "#{column_name} #{direction} NULLS FIRST"
216
- # "#{column_name} NULLS LAST"
217
- COLUMN_NAME_WITH_ORDER = /
218
- \A
219
- (
220
- (?:
221
- # table_name.column_name | function(one or no argument)
222
- ((?:\w+\.)?\w+ | \w+\((?:|\g<2>)\))
223
- )
224
- (?:\s+ASC|\s+DESC)?
225
- (?:\s+NULLS\s+(?:FIRST|LAST))?
226
- )
227
- (?:\s*,\s*\g<1>)*
228
- \z
229
- /ix
230
-
231
- private_constant :COLUMN_NAME, :COLUMN_NAME_WITH_ORDER
232
-
233
223
  private
234
224
  def type_casted_binds(binds)
235
225
  binds.map do |value|
@@ -244,22 +234,6 @@ module ActiveRecord
244
234
  def lookup_cast_type(sql_type)
245
235
  type_map.lookup(sql_type)
246
236
  end
247
-
248
- def warn_quote_duration_deprecated
249
- ActiveRecord.deprecator.warn(<<~MSG)
250
- Using ActiveSupport::Duration as an interpolated bind parameter in a SQL
251
- string template is deprecated. To avoid this warning, you should explicitly
252
- convert the duration to a more specific database type. For example, if you
253
- want to use a duration as an integer number of seconds:
254
- ```
255
- Record.where("duration = ?", 1.hour.to_i)
256
- ```
257
- If you want to use a duration as an ISO 8601 string:
258
- ```
259
- Record.where("duration = ?", 1.hour.iso8601)
260
- ```
261
- MSG
262
- end
263
237
  end
264
238
  end
265
239
  end
@@ -160,6 +160,8 @@ module ActiveRecord
160
160
  end
161
161
 
162
162
  def defined_for?(to_table: nil, validate: nil, **options)
163
+ options = options.slice(*self.options.keys)
164
+
163
165
  (to_table.nil? || to_table.to_s == self.to_table) &&
164
166
  (validate.nil? || validate == self.options.fetch(:validate, validate)) &&
165
167
  options.all? { |k, v| Array(self.options[k]).map(&:to_s) == Array(v).map(&:to_s) }
@@ -186,6 +188,8 @@ module ActiveRecord
186
188
  end
187
189
 
188
190
  def defined_for?(name:, expression: nil, validate: nil, **options)
191
+ options = options.slice(*self.options.keys)
192
+
189
193
  self.name == name.to_s &&
190
194
  (validate.nil? || validate == self.options.fetch(:validate, validate)) &&
191
195
  options.all? { |k, v| self.options[k].to_s == v.to_s }
@@ -348,7 +352,7 @@ module ActiveRecord
348
352
  # Inside migration files, the +t+ object in {create_table}[rdoc-ref:SchemaStatements#create_table]
349
353
  # is actually of this type:
350
354
  #
351
- # class SomeMigration < ActiveRecord::Migration[7.1]
355
+ # class SomeMigration < ActiveRecord::Migration[7.2]
352
356
  # def up
353
357
  # create_table :foo do |t|
354
358
  # puts t.class # => "ActiveRecord::ConnectionAdapters::TableDefinition"
@@ -431,7 +435,7 @@ module ActiveRecord
431
435
  #
432
436
  # == Examples
433
437
  #
434
- # # Assuming +td+ is an instance of TableDefinition
438
+ # # Assuming `td` is an instance of TableDefinition
435
439
  # td.column(:granted, :boolean, index: true)
436
440
  #
437
441
  # == Short-hand examples
@@ -186,6 +186,9 @@ module ActiveRecord
186
186
  # Join tables for {ActiveRecord::Base.has_and_belongs_to_many}[rdoc-ref:Associations::ClassMethods#has_and_belongs_to_many] should set it to false.
187
187
  #
188
188
  # A Symbol can be used to specify the type of the generated primary key column.
189
+ #
190
+ # A Hash can be used to specify the generated primary key column creation options.
191
+ # See {add_column}[rdoc-ref:ConnectionAdapters::SchemaStatements#add_column] for available options.
189
192
  # [<tt>:primary_key</tt>]
190
193
  # The name of the primary key, if one is to be added automatically.
191
194
  # Defaults to +id+. If <tt>:id</tt> is false, then this option is ignored.
@@ -293,6 +296,11 @@ module ActiveRecord
293
296
  def create_table(table_name, id: :primary_key, primary_key: nil, force: nil, **options, &block)
294
297
  validate_create_table_options!(options)
295
298
  validate_table_length!(table_name) unless options[:_uses_legacy_table_name]
299
+
300
+ if force && options.key?(:if_not_exists)
301
+ raise ArgumentError, "Options `:force` and `:if_not_exists` cannot be used simultaneously."
302
+ end
303
+
296
304
  td = build_create_table_definition(table_name, id: id, primary_key: primary_key, force: force, **options, &block)
297
305
 
298
306
  if force
@@ -677,7 +685,7 @@ module ActiveRecord
677
685
  #
678
686
  # If the options provided include an +if_exists+ key, it will be used to check if the
679
687
  # column does not exist. This will silently ignore the migration rather than raising
680
- # if the column was already used.
688
+ # if the column was already removed.
681
689
  #
682
690
  # remove_column(:suppliers, :qualification, if_exists: true)
683
691
  def remove_column(table_name, column_name, type = nil, **options)
@@ -876,9 +884,12 @@ module ActiveRecord
876
884
  # ====== Creating an index with a specific algorithm
877
885
  #
878
886
  # add_index(:developers, :name, algorithm: :concurrently)
879
- # # CREATE INDEX CONCURRENTLY developers_on_name on developers (name)
887
+ # # CREATE INDEX CONCURRENTLY developers_on_name on developers (name) -- PostgreSQL
880
888
  #
881
- # Note: only supported by PostgreSQL.
889
+ # add_index(:developers, :name, algorithm: :inplace)
890
+ # # CREATE INDEX `index_developers_on_name` ON `developers` (`name`) ALGORITHM = INPLACE -- MySQL
891
+ #
892
+ # Note: only supported by PostgreSQL and MySQL.
882
893
  #
883
894
  # Concurrently adding an index is not supported in a transaction.
884
895
  #
@@ -1318,7 +1329,7 @@ module ActiveRecord
1318
1329
  end
1319
1330
 
1320
1331
  def dump_schema_information # :nodoc:
1321
- versions = schema_migration.versions
1332
+ versions = pool.schema_migration.versions
1322
1333
  insert_versions_sql(versions) if versions.any?
1323
1334
  end
1324
1335
 
@@ -1328,8 +1339,9 @@ module ActiveRecord
1328
1339
 
1329
1340
  def assume_migrated_upto_version(version)
1330
1341
  version = version.to_i
1331
- sm_table = quote_table_name(schema_migration.table_name)
1342
+ sm_table = quote_table_name(pool.schema_migration.table_name)
1332
1343
 
1344
+ migration_context = pool.migration_context
1333
1345
  migrated = migration_context.get_all_versions
1334
1346
  versions = migration_context.migrations.map(&:version)
1335
1347
 
@@ -1839,7 +1851,7 @@ module ActiveRecord
1839
1851
  end
1840
1852
 
1841
1853
  def insert_versions_sql(versions)
1842
- sm_table = quote_table_name(schema_migration.table_name)
1854
+ sm_table = quote_table_name(pool.schema_migration.table_name)
1843
1855
 
1844
1856
  if versions.is_a?(Array)
1845
1857
  sql = +"INSERT INTO #{sm_table} (version) VALUES\n"
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_support/core_ext/digest"
4
+
3
5
  module ActiveRecord
4
6
  module ConnectionAdapters
5
7
  # = Active Record Connection Adapters Transaction State
@@ -89,7 +91,9 @@ module ActiveRecord
89
91
  raise InstrumentationAlreadyStartedError.new("Called start on an already started transaction") if @started
90
92
  @started = true
91
93
 
92
- @payload = @base_payload.dup
94
+ ActiveSupport::Notifications.instrument("start_transaction.active_record", @base_payload)
95
+
96
+ @payload = @base_payload.dup # We dup because the payload for a given event is mutated later to add the outcome.
93
97
  @handle = ActiveSupport::Notifications.instrumenter.build_handle("transaction.active_record", @payload)
94
98
  @handle.start
95
99
  end
@@ -104,7 +108,6 @@ module ActiveRecord
104
108
  end
105
109
 
106
110
  class NullTransaction # :nodoc:
107
- def initialize; end
108
111
  def state; end
109
112
  def closed?; true; end
110
113
  def open?; false; end
@@ -115,17 +118,43 @@ module ActiveRecord
115
118
  def dirty!; end
116
119
  def invalidated?; false; end
117
120
  def invalidate!; end
121
+ def materialized?; false; end
122
+ def before_commit; yield; end
123
+ def after_commit; yield; end
124
+ def after_rollback; end
125
+ def user_transaction; ActiveRecord::Transaction::NULL_TRANSACTION; end
118
126
  end
119
127
 
120
128
  class Transaction # :nodoc:
121
- attr_reader :connection, :state, :savepoint_name, :isolation_level
129
+ class Callback # :nodoc:
130
+ def initialize(event, callback)
131
+ @event = event
132
+ @callback = callback
133
+ end
134
+
135
+ def before_commit
136
+ @callback.call if @event == :before_commit
137
+ end
138
+
139
+ def after_commit
140
+ @callback.call if @event == :after_commit
141
+ end
142
+
143
+ def after_rollback
144
+ @callback.call if @event == :after_rollback
145
+ end
146
+ end
147
+
148
+ attr_reader :connection, :state, :savepoint_name, :isolation_level, :user_transaction
122
149
  attr_accessor :written
123
150
 
124
151
  delegate :invalidate!, :invalidated?, to: :@state
125
152
 
126
153
  def initialize(connection, isolation: nil, joinable: true, run_commit_callbacks: false)
154
+ super()
127
155
  @connection = connection
128
156
  @state = TransactionState.new
157
+ @callbacks = nil
129
158
  @records = nil
130
159
  @isolation_level = isolation
131
160
  @materialized = false
@@ -133,7 +162,8 @@ module ActiveRecord
133
162
  @run_commit_callbacks = run_commit_callbacks
134
163
  @lazy_enrollment_records = nil
135
164
  @dirty = false
136
- @instrumenter = TransactionInstrumenter.new(connection: connection)
165
+ @user_transaction = joinable ? ActiveRecord::Transaction.new(self) : ActiveRecord::Transaction::NULL_TRANSACTION
166
+ @instrumenter = TransactionInstrumenter.new(connection: connection, transaction: @user_transaction)
137
167
  end
138
168
 
139
169
  def dirty!
@@ -144,6 +174,14 @@ module ActiveRecord
144
174
  @dirty
145
175
  end
146
176
 
177
+ def open?
178
+ true
179
+ end
180
+
181
+ def closed?
182
+ false
183
+ end
184
+
147
185
  def add_record(record, ensure_finalize = true)
148
186
  @records ||= []
149
187
  if ensure_finalize
@@ -154,6 +192,30 @@ module ActiveRecord
154
192
  end
155
193
  end
156
194
 
195
+ def before_commit(&block)
196
+ if @state.finalized?
197
+ raise ActiveRecordError, "Cannot register callbacks on a finalized transaction"
198
+ end
199
+
200
+ (@callbacks ||= []) << Callback.new(:before_commit, block)
201
+ end
202
+
203
+ def after_commit(&block)
204
+ if @state.finalized?
205
+ raise ActiveRecordError, "Cannot register callbacks on a finalized transaction"
206
+ end
207
+
208
+ (@callbacks ||= []) << Callback.new(:after_commit, block)
209
+ end
210
+
211
+ def after_rollback(&block)
212
+ if @state.finalized?
213
+ raise ActiveRecordError, "Cannot register callbacks on a finalized transaction"
214
+ end
215
+
216
+ (@callbacks ||= []) << Callback.new(:after_rollback, block)
217
+ end
218
+
157
219
  def records
158
220
  if @lazy_enrollment_records
159
221
  @records.concat @lazy_enrollment_records.values
@@ -190,66 +252,85 @@ module ActiveRecord
190
252
  end
191
253
 
192
254
  def rollback_records
193
- return unless records
194
-
195
- ite = unique_records
255
+ if records
256
+ begin
257
+ ite = unique_records
196
258
 
197
- instances_to_run_callbacks_on = prepare_instances_to_run_callbacks_on(ite)
259
+ instances_to_run_callbacks_on = prepare_instances_to_run_callbacks_on(ite)
198
260
 
199
- run_action_on_records(ite, instances_to_run_callbacks_on) do |record, should_run_callbacks|
200
- record.rolledback!(force_restore_state: full_rollback?, should_run_callbacks: should_run_callbacks)
201
- end
202
- ensure
203
- ite&.each do |i|
204
- i.rolledback!(force_restore_state: full_rollback?, should_run_callbacks: false)
261
+ run_action_on_records(ite, instances_to_run_callbacks_on) do |record, should_run_callbacks|
262
+ record.rolledback!(force_restore_state: full_rollback?, should_run_callbacks: should_run_callbacks)
263
+ end
264
+ ensure
265
+ ite&.each do |i|
266
+ i.rolledback!(force_restore_state: full_rollback?, should_run_callbacks: false)
267
+ end
268
+ end
205
269
  end
270
+
271
+ @callbacks&.each(&:after_rollback)
206
272
  end
207
273
 
208
274
  def before_commit_records
209
- return unless records
210
-
211
275
  if @run_commit_callbacks
212
- if ActiveRecord.before_committed_on_all_records
213
- ite = unique_records
276
+ if records
277
+ if ActiveRecord.before_committed_on_all_records
278
+ ite = unique_records
214
279
 
215
- instances_to_run_callbacks_on = records.each_with_object({}) do |record, candidates|
216
- candidates[record] = record
217
- end
280
+ instances_to_run_callbacks_on = records.each_with_object({}) do |record, candidates|
281
+ candidates[record] = record
282
+ end
218
283
 
219
- run_action_on_records(ite, instances_to_run_callbacks_on) do |record, should_run_callbacks|
220
- record.before_committed! if should_run_callbacks
284
+ run_action_on_records(ite, instances_to_run_callbacks_on) do |record, should_run_callbacks|
285
+ record.before_committed! if should_run_callbacks
286
+ end
287
+ else
288
+ records.uniq.each(&:before_committed!)
221
289
  end
222
- else
223
- records.uniq.each(&:before_committed!)
224
290
  end
291
+
292
+ @callbacks&.each(&:before_commit)
225
293
  end
294
+ # Note: When @run_commit_callbacks is false #commit_records takes care of appending
295
+ # remaining callbacks to the parent transaction
226
296
  end
227
297
 
228
298
  def commit_records
229
- return unless records
230
-
231
- ite = unique_records
299
+ if records
300
+ begin
301
+ ite = unique_records
232
302
 
233
- if @run_commit_callbacks
234
- instances_to_run_callbacks_on = prepare_instances_to_run_callbacks_on(ite)
303
+ if @run_commit_callbacks
304
+ instances_to_run_callbacks_on = prepare_instances_to_run_callbacks_on(ite)
235
305
 
236
- run_action_on_records(ite, instances_to_run_callbacks_on) do |record, should_run_callbacks|
237
- record.committed!(should_run_callbacks: should_run_callbacks)
238
- end
239
- else
240
- while record = ite.shift
241
- # if not running callbacks, only adds the record to the parent transaction
242
- connection.add_transaction_record(record)
306
+ run_action_on_records(ite, instances_to_run_callbacks_on) do |record, should_run_callbacks|
307
+ record.committed!(should_run_callbacks: should_run_callbacks)
308
+ end
309
+ else
310
+ while record = ite.shift
311
+ # if not running callbacks, only adds the record to the parent transaction
312
+ connection.add_transaction_record(record)
313
+ end
314
+ end
315
+ ensure
316
+ ite&.each { |i| i.committed!(should_run_callbacks: false) }
243
317
  end
244
318
  end
245
- ensure
246
- ite&.each { |i| i.committed!(should_run_callbacks: false) }
319
+
320
+ if @run_commit_callbacks
321
+ @callbacks&.each(&:after_commit)
322
+ elsif @callbacks
323
+ connection.current_transaction.append_callbacks(@callbacks)
324
+ end
247
325
  end
248
326
 
249
327
  def full_rollback?; true; end
250
328
  def joinable?; @joinable; end
251
- def closed?; false; end
252
- def open?; !closed?; end
329
+
330
+ protected
331
+ def append_callbacks(callbacks) # :nodoc:
332
+ (@callbacks ||= []).concat(callbacks)
333
+ end
253
334
 
254
335
  private
255
336
  def unique_records
@@ -349,7 +430,7 @@ module ActiveRecord
349
430
 
350
431
  def rollback
351
432
  unless @state.invalidated?
352
- connection.rollback_to_savepoint(savepoint_name) if materialized?
433
+ connection.rollback_to_savepoint(savepoint_name) if materialized? && connection.active?
353
434
  end
354
435
  @state.rollback!
355
436
  @instrumenter.finish(:rollback) if materialized?
@@ -532,9 +613,7 @@ module ActiveRecord
532
613
  @connection.lock.synchronize do
533
614
  transaction = begin_transaction(isolation: isolation, joinable: joinable)
534
615
  begin
535
- ret = yield
536
- completed = true
537
- ret
616
+ yield transaction.user_transaction
538
617
  rescue Exception => error
539
618
  rollback_transaction
540
619
  after_failure_actions(transaction, error)
@@ -542,24 +621,8 @@ module ActiveRecord
542
621
  raise
543
622
  ensure
544
623
  unless error
545
- # In 7.1 we enforce timeout >= 0.4.0 which no longer use throw, so we can
546
- # go back to the original behavior of committing on non-local return.
547
- # If users are using throw, we assume it's not an error case.
548
- completed = true if ActiveRecord.commit_transaction_on_non_local_return
549
-
550
624
  if Thread.current.status == "aborting"
551
625
  rollback_transaction
552
- elsif !completed && transaction.written
553
- ActiveRecord.deprecator.warn(<<~EOW)
554
- A transaction is being rolled back because the transaction block was
555
- exited using `return`, `break` or `throw`.
556
- In Rails 7.2 this transaction will be committed instead.
557
- To opt-in to the new behavior now and suppress this warning
558
- you can set:
559
-
560
- Rails.application.config.active_record.commit_transaction_on_non_local_return = true
561
- EOW
562
- rollback_transaction
563
626
  else
564
627
  begin
565
628
  commit_transaction
@@ -590,7 +653,7 @@ module ActiveRecord
590
653
  end
591
654
 
592
655
  private
593
- NULL_TRANSACTION = NullTransaction.new
656
+ NULL_TRANSACTION = NullTransaction.new.freeze
594
657
 
595
658
  # Deallocate invalidated prepared statements outside of the transaction
596
659
  def after_failure_actions(transaction, error)