activerecord 7.2.4 → 8.1.4

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 (210) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1013 -878
  3. data/README.rdoc +1 -1
  4. data/lib/active_record/association_relation.rb +2 -1
  5. data/lib/active_record/associations/association.rb +69 -27
  6. data/lib/active_record/associations/association_scope.rb +1 -1
  7. data/lib/active_record/associations/belongs_to_association.rb +2 -2
  8. data/lib/active_record/associations/builder/association.rb +23 -11
  9. data/lib/active_record/associations/builder/belongs_to.rb +36 -8
  10. data/lib/active_record/associations/builder/collection_association.rb +7 -3
  11. data/lib/active_record/associations/builder/has_one.rb +1 -1
  12. data/lib/active_record/associations/builder/singular_association.rb +33 -5
  13. data/lib/active_record/associations/collection_association.rb +7 -3
  14. data/lib/active_record/associations/collection_proxy.rb +26 -4
  15. data/lib/active_record/associations/deprecation.rb +88 -0
  16. data/lib/active_record/associations/disable_joins_association_scope.rb +1 -1
  17. data/lib/active_record/associations/errors.rb +3 -0
  18. data/lib/active_record/associations/foreign_association.rb +3 -1
  19. data/lib/active_record/associations/has_many_through_association.rb +3 -2
  20. data/lib/active_record/associations/has_one_association.rb +1 -0
  21. data/lib/active_record/associations/join_dependency.rb +3 -2
  22. data/lib/active_record/associations/preloader/association.rb +3 -3
  23. data/lib/active_record/associations/preloader/batch.rb +7 -1
  24. data/lib/active_record/associations/preloader/branch.rb +1 -0
  25. data/lib/active_record/associations/singular_association.rb +8 -3
  26. data/lib/active_record/associations.rb +192 -24
  27. data/lib/active_record/asynchronous_queries_tracker.rb +28 -24
  28. data/lib/active_record/attribute_methods/primary_key.rb +4 -8
  29. data/lib/active_record/attribute_methods/query.rb +34 -0
  30. data/lib/active_record/attribute_methods/serialization.rb +18 -5
  31. data/lib/active_record/attribute_methods/time_zone_conversion.rb +12 -14
  32. data/lib/active_record/attributes.rb +3 -0
  33. data/lib/active_record/autosave_association.rb +69 -27
  34. data/lib/active_record/base.rb +1 -2
  35. data/lib/active_record/callbacks.rb +1 -2
  36. data/lib/active_record/coders/column_serializer.rb +1 -1
  37. data/lib/active_record/coders/json.rb +16 -6
  38. data/lib/active_record/coders/yaml_column.rb +2 -2
  39. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +35 -28
  40. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +16 -4
  41. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +68 -13
  42. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +429 -93
  43. data/lib/active_record/connection_adapters/abstract/database_statements.rb +148 -74
  44. data/lib/active_record/connection_adapters/abstract/query_cache.rb +27 -5
  45. data/lib/active_record/connection_adapters/abstract/quoting.rb +16 -25
  46. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +11 -7
  47. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +32 -35
  48. data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +2 -1
  49. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +124 -34
  50. data/lib/active_record/connection_adapters/abstract/transaction.rb +40 -8
  51. data/lib/active_record/connection_adapters/abstract_adapter.rb +129 -63
  52. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +132 -65
  53. data/lib/active_record/connection_adapters/column.rb +14 -4
  54. data/lib/active_record/connection_adapters/mysql/database_statements.rb +4 -4
  55. data/lib/active_record/connection_adapters/mysql/quoting.rb +0 -8
  56. data/lib/active_record/connection_adapters/mysql/schema_creation.rb +2 -0
  57. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +41 -10
  58. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +79 -66
  59. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +89 -94
  60. data/lib/active_record/connection_adapters/mysql2_adapter.rb +2 -10
  61. data/lib/active_record/connection_adapters/pool_config.rb +7 -7
  62. data/lib/active_record/connection_adapters/postgresql/column.rb +8 -2
  63. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +79 -61
  64. data/lib/active_record/connection_adapters/postgresql/oid/array.rb +3 -3
  65. data/lib/active_record/connection_adapters/postgresql/oid/point.rb +10 -0
  66. data/lib/active_record/connection_adapters/postgresql/oid/range.rb +46 -3
  67. data/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +1 -1
  68. data/lib/active_record/connection_adapters/postgresql/quoting.rb +21 -10
  69. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +2 -4
  70. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +9 -17
  71. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +14 -33
  72. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +71 -32
  73. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +156 -68
  74. data/lib/active_record/connection_adapters/postgresql_adapter.rb +78 -105
  75. data/lib/active_record/connection_adapters/schema_cache.rb +1 -3
  76. data/lib/active_record/connection_adapters/sqlite3/column.rb +8 -2
  77. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +90 -98
  78. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +0 -8
  79. data/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +0 -6
  80. data/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +27 -2
  81. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +15 -16
  82. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +104 -37
  83. data/lib/active_record/connection_adapters/statement_pool.rb +4 -2
  84. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +41 -66
  85. data/lib/active_record/connection_adapters/trilogy_adapter.rb +15 -18
  86. data/lib/active_record/connection_adapters.rb +1 -56
  87. data/lib/active_record/connection_handling.rb +25 -2
  88. data/lib/active_record/core.rb +33 -17
  89. data/lib/active_record/counter_cache.rb +35 -8
  90. data/lib/active_record/database_configurations/database_config.rb +9 -1
  91. data/lib/active_record/database_configurations/hash_config.rb +74 -9
  92. data/lib/active_record/database_configurations/url_config.rb +15 -3
  93. data/lib/active_record/database_configurations.rb +7 -3
  94. data/lib/active_record/delegated_type.rb +1 -1
  95. data/lib/active_record/disable_joins_association_relation.rb +5 -1
  96. data/lib/active_record/dynamic_matchers.rb +54 -69
  97. data/lib/active_record/encryption/config.rb +3 -1
  98. data/lib/active_record/encryption/encryptable_record.rb +8 -8
  99. data/lib/active_record/encryption/encrypted_attribute_type.rb +11 -2
  100. data/lib/active_record/encryption/encrypted_fixtures.rb +12 -7
  101. data/lib/active_record/encryption/encryptor.rb +28 -8
  102. data/lib/active_record/encryption/extended_deterministic_queries.rb +4 -2
  103. data/lib/active_record/encryption/scheme.rb +9 -2
  104. data/lib/active_record/enum.rb +33 -30
  105. data/lib/active_record/errors.rb +33 -9
  106. data/lib/active_record/explain.rb +1 -1
  107. data/lib/active_record/explain_registry.rb +51 -2
  108. data/lib/active_record/filter_attribute_handler.rb +76 -0
  109. data/lib/active_record/fixtures.rb +2 -4
  110. data/lib/active_record/future_result.rb +15 -9
  111. data/lib/active_record/gem_version.rb +2 -2
  112. data/lib/active_record/inheritance.rb +9 -4
  113. data/lib/active_record/insert_all.rb +14 -9
  114. data/lib/active_record/locking/optimistic.rb +8 -1
  115. data/lib/active_record/locking/pessimistic.rb +5 -0
  116. data/lib/active_record/log_subscriber.rb +31 -29
  117. data/lib/active_record/message_pack.rb +13 -0
  118. data/lib/active_record/middleware/shard_selector.rb +34 -17
  119. data/lib/active_record/migration/command_recorder.rb +56 -13
  120. data/lib/active_record/migration/compatibility.rb +37 -24
  121. data/lib/active_record/migration/default_schema_versions_formatter.rb +30 -0
  122. data/lib/active_record/migration.rb +48 -42
  123. data/lib/active_record/model_schema.rb +40 -15
  124. data/lib/active_record/nested_attributes.rb +14 -12
  125. data/lib/active_record/persistence.rb +163 -133
  126. data/lib/active_record/query_cache.rb +22 -15
  127. data/lib/active_record/query_logs.rb +100 -52
  128. data/lib/active_record/query_logs_formatter.rb +17 -28
  129. data/lib/active_record/querying.rb +21 -9
  130. data/lib/active_record/railtie.rb +35 -30
  131. data/lib/active_record/railties/controller_runtime.rb +11 -6
  132. data/lib/active_record/railties/databases.rake +26 -38
  133. data/lib/active_record/railties/job_checkpoints.rb +15 -0
  134. data/lib/active_record/railties/job_runtime.rb +10 -11
  135. data/lib/active_record/reflection.rb +54 -22
  136. data/lib/active_record/relation/batches/batch_enumerator.rb +4 -3
  137. data/lib/active_record/relation/batches.rb +147 -73
  138. data/lib/active_record/relation/calculations.rb +60 -43
  139. data/lib/active_record/relation/delegation.rb +25 -15
  140. data/lib/active_record/relation/finder_methods.rb +73 -43
  141. data/lib/active_record/relation/merger.rb +12 -10
  142. data/lib/active_record/relation/predicate_builder/array_handler.rb +3 -1
  143. data/lib/active_record/relation/predicate_builder/association_query_value.rb +9 -9
  144. data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +8 -8
  145. data/lib/active_record/relation/predicate_builder/relation_handler.rb +4 -3
  146. data/lib/active_record/relation/predicate_builder.rb +23 -8
  147. data/lib/active_record/relation/query_attribute.rb +3 -1
  148. data/lib/active_record/relation/query_methods.rb +200 -96
  149. data/lib/active_record/relation/spawn_methods.rb +7 -7
  150. data/lib/active_record/relation/where_clause.rb +2 -9
  151. data/lib/active_record/relation.rb +127 -87
  152. data/lib/active_record/result.rb +109 -24
  153. data/lib/active_record/runtime_registry.rb +42 -58
  154. data/lib/active_record/sanitization.rb +9 -6
  155. data/lib/active_record/schema.rb +1 -1
  156. data/lib/active_record/schema_dumper.rb +17 -10
  157. data/lib/active_record/schema_migration.rb +2 -1
  158. data/lib/active_record/scoping/named.rb +5 -2
  159. data/lib/active_record/scoping.rb +0 -1
  160. data/lib/active_record/signed_id.rb +44 -16
  161. data/lib/active_record/statement_cache.rb +24 -20
  162. data/lib/active_record/store.rb +53 -22
  163. data/lib/active_record/structured_event_subscriber.rb +85 -0
  164. data/lib/active_record/table_metadata.rb +6 -23
  165. data/lib/active_record/tasks/abstract_tasks.rb +83 -0
  166. data/lib/active_record/tasks/database_tasks.rb +85 -85
  167. data/lib/active_record/tasks/mysql_database_tasks.rb +4 -58
  168. data/lib/active_record/tasks/postgresql_database_tasks.rb +7 -40
  169. data/lib/active_record/tasks/sqlite_database_tasks.rb +16 -28
  170. data/lib/active_record/test_databases.rb +18 -4
  171. data/lib/active_record/test_fixtures.rb +52 -6
  172. data/lib/active_record/testing/query_assertions.rb +8 -2
  173. data/lib/active_record/timestamp.rb +4 -2
  174. data/lib/active_record/token_for.rb +1 -1
  175. data/lib/active_record/transaction.rb +2 -5
  176. data/lib/active_record/transactions.rb +56 -32
  177. data/lib/active_record/type/hash_lookup_type_map.rb +2 -1
  178. data/lib/active_record/type/internal/timezone.rb +7 -0
  179. data/lib/active_record/type/json.rb +13 -2
  180. data/lib/active_record/type/serialized.rb +16 -4
  181. data/lib/active_record/type/type_map.rb +1 -1
  182. data/lib/active_record/type_caster/connection.rb +2 -1
  183. data/lib/active_record/validations/associated.rb +1 -1
  184. data/lib/active_record/validations/uniqueness.rb +8 -8
  185. data/lib/active_record.rb +85 -50
  186. data/lib/arel/alias_predication.rb +2 -0
  187. data/lib/arel/collectors/bind.rb +2 -2
  188. data/lib/arel/collectors/sql_string.rb +1 -1
  189. data/lib/arel/collectors/substitute_binds.rb +2 -2
  190. data/lib/arel/crud.rb +6 -11
  191. data/lib/arel/nodes/binary.rb +1 -1
  192. data/lib/arel/nodes/count.rb +2 -2
  193. data/lib/arel/nodes/function.rb +4 -10
  194. data/lib/arel/nodes/named_function.rb +2 -2
  195. data/lib/arel/nodes/node.rb +2 -2
  196. data/lib/arel/nodes/sql_literal.rb +1 -1
  197. data/lib/arel/nodes.rb +0 -2
  198. data/lib/arel/predications.rb +1 -3
  199. data/lib/arel/select_manager.rb +7 -2
  200. data/lib/arel/table.rb +3 -7
  201. data/lib/arel/visitors/dot.rb +0 -3
  202. data/lib/arel/visitors/postgresql.rb +55 -0
  203. data/lib/arel/visitors/sqlite.rb +55 -8
  204. data/lib/arel/visitors/to_sql.rb +4 -22
  205. data/lib/arel.rb +3 -1
  206. data/lib/rails/generators/active_record/application_record/USAGE +1 -1
  207. metadata +15 -12
  208. data/lib/active_record/explain_subscriber.rb +0 -34
  209. data/lib/active_record/normalization.rb +0 -163
  210. data/lib/active_record/relation/record_fetch_warning.rb +0 -52
data/README.rdoc CHANGED
@@ -139,7 +139,7 @@ A short rundown of some of the major features:
139
139
 
140
140
  * Database agnostic schema management with Migrations.
141
141
 
142
- class AddSystemSettings < ActiveRecord::Migration[7.2]
142
+ class AddSystemSettings < ActiveRecord::Migration[8.1]
143
143
  def up
144
144
  create_table :system_settings do |t|
145
145
  t.string :name
@@ -17,7 +17,7 @@ module ActiveRecord
17
17
 
18
18
  %w(insert insert_all insert! insert_all! upsert upsert_all).each do |method|
19
19
  class_eval <<~RUBY, __FILE__, __LINE__ + 1
20
- def #{method}(attributes, **kwargs)
20
+ def #{method}(...)
21
21
  if @association.reflection.through_reflection?
22
22
  raise ArgumentError, "Bulk insert or upsert is currently not supported for has_many through association"
23
23
  end
@@ -43,6 +43,7 @@ module ActiveRecord
43
43
  def exec_queries
44
44
  super do |record|
45
45
  @association.set_inverse_instance_from_queries(record)
46
+ @association.set_strict_loading(record)
46
47
  yield record if block_given?
47
48
  end
48
49
  end
@@ -34,7 +34,7 @@ module ActiveRecord
34
34
  # the <tt>reflection</tt> object represents a <tt>:has_many</tt> macro.
35
35
  class Association # :nodoc:
36
36
  attr_accessor :owner
37
- attr_reader :target, :reflection, :disable_joins
37
+ attr_reader :reflection, :disable_joins
38
38
 
39
39
  delegate :options, to: :reflection
40
40
 
@@ -50,6 +50,13 @@ module ActiveRecord
50
50
  @skip_strict_loading = nil
51
51
  end
52
52
 
53
+ def target
54
+ if @target.is_a?(Promise)
55
+ @target = @target.value
56
+ end
57
+ @target
58
+ end
59
+
53
60
  # Resets the \loaded flag to +false+ and sets the \target to +nil+.
54
61
  def reset
55
62
  @loaded = false
@@ -113,6 +120,14 @@ module ActiveRecord
113
120
  @association_scope = nil
114
121
  end
115
122
 
123
+ def set_strict_loading(record)
124
+ if owner.strict_loading_n_plus_one_only? && reflection.macro == :has_many
125
+ record.strict_loading!
126
+ else
127
+ record.strict_loading!(false, mode: owner.strict_loading_mode)
128
+ end
129
+ end
130
+
116
131
  # Set the inverse association, if possible
117
132
  def set_inverse_instance(record)
118
133
  if inverse = inverse_association_for(record)
@@ -172,7 +187,7 @@ module ActiveRecord
172
187
  # ActiveRecord::RecordNotFound is rescued within the method, and it is
173
188
  # not reraised. The proxy is \reset and +nil+ is the return value.
174
189
  def load_target
175
- @target = find_target if (@stale_state && stale_target?) || find_target?
190
+ @target = find_target(async: false) if (@stale_state && stale_target?) || find_target?
176
191
 
177
192
  loaded! unless loaded?
178
193
  target
@@ -180,6 +195,13 @@ module ActiveRecord
180
195
  reset
181
196
  end
182
197
 
198
+ def async_load_target # :nodoc:
199
+ @target = find_target(async: true) if (@stale_state && stale_target?) || find_target?
200
+
201
+ loaded! unless loaded?
202
+ nil
203
+ end
204
+
183
205
  # We can't dump @reflection and @through_reflection since it contains the scope proc
184
206
  def marshal_dump
185
207
  ivars = (instance_variables - [:@reflection, :@through_reflection]).map { |name| [name, instance_variable_get(name)] }
@@ -210,7 +232,19 @@ module ActiveRecord
210
232
  _create_record(attributes, true, &block)
211
233
  end
212
234
 
213
- # Whether the association represent a single record
235
+ def violates_strict_loading?
236
+ return unless find_target?
237
+
238
+ return if @skip_strict_loading
239
+
240
+ return unless owner.validation_context.nil?
241
+
242
+ return reflection.strict_loading? if reflection.options.key?(:strict_loading)
243
+
244
+ owner.strict_loading? && !owner.strict_loading_n_plus_one_only?
245
+ end
246
+
247
+ # Whether the association represents a single record
214
248
  # or a collection of records.
215
249
  def collection?
216
250
  false
@@ -223,13 +257,19 @@ module ActiveRecord
223
257
  klass
224
258
  end
225
259
 
226
- def find_target
260
+ def find_target(async: false)
227
261
  if violates_strict_loading?
228
262
  Base.strict_loading_violation!(owner: owner.class, reflection: reflection)
229
263
  end
230
264
 
231
265
  scope = self.scope
232
- return scope.to_a if skip_statement_cache?(scope)
266
+ if skip_statement_cache?(scope)
267
+ if async
268
+ return scope.load_async.then(&:to_a)
269
+ else
270
+ return scope.to_a
271
+ end
272
+ end
233
273
 
234
274
  sc = reflection.association_scope_cache(klass, owner) do |params|
235
275
  as = AssociationScope.create { params.bind }
@@ -238,13 +278,9 @@ module ActiveRecord
238
278
 
239
279
  binds = AssociationScope.get_bind_values(owner, reflection.chain)
240
280
  klass.with_connection do |c|
241
- sc.execute(binds, c) do |record|
281
+ sc.execute(binds, c, async: async) do |record|
242
282
  set_inverse_instance(record)
243
- if owner.strict_loading_n_plus_one_only? && reflection.macro == :has_many
244
- record.strict_loading!
245
- else
246
- record.strict_loading!(false, mode: owner.strict_loading_mode)
247
- end
283
+ set_strict_loading(record)
248
284
  end
249
285
  end
250
286
  end
@@ -257,16 +293,6 @@ module ActiveRecord
257
293
  @skip_strict_loading = skip_strict_loading_was
258
294
  end
259
295
 
260
- def violates_strict_loading?
261
- return if @skip_strict_loading
262
-
263
- return unless owner.validation_context.nil?
264
-
265
- return reflection.strict_loading? if reflection.options.key?(:strict_loading)
266
-
267
- owner.strict_loading? && !owner.strict_loading_n_plus_one_only?
268
- end
269
-
270
296
  # The scope for this association.
271
297
  #
272
298
  # Note that the association_scope is merged into the target_scope only when the
@@ -385,12 +411,28 @@ module ActiveRecord
385
411
  end
386
412
 
387
413
  def matches_foreign_key?(record)
388
- if foreign_key_for?(record)
389
- record.read_attribute(reflection.foreign_key) == owner.id ||
390
- (foreign_key_for?(owner) && owner.read_attribute(reflection.foreign_key) == record.id)
391
- else
392
- owner.read_attribute(reflection.foreign_key) == record.id
393
- end
414
+ (foreign_key_for?(record) && record_foreign_key_matches_owner?(record)) ||
415
+ (foreign_key_for?(owner) && owner_foreign_key_matches_record?(record))
416
+ end
417
+
418
+ def record_foreign_key_matches_owner?(record)
419
+ foreign_key_values(record) == active_record_primary_key_values(owner)
420
+ end
421
+
422
+ def owner_foreign_key_matches_record?(record)
423
+ foreign_key_values(owner) == association_primary_key_values(record)
424
+ end
425
+
426
+ def foreign_key_values(record)
427
+ Array(reflection.foreign_key).map { |key| record.read_attribute(key) }
428
+ end
429
+
430
+ def active_record_primary_key_values(record)
431
+ Array(reflection.active_record_primary_key).map { |key| record.read_attribute(key) }
432
+ end
433
+
434
+ def association_primary_key_values(record)
435
+ Array(reflection.association_primary_key(record.class)).map { |key| record.read_attribute(key) }
394
436
  end
395
437
  end
396
438
  end
@@ -149,7 +149,7 @@ module ActiveRecord
149
149
  scope.includes_values |= item.includes_values
150
150
  end
151
151
 
152
- scope.unscope!(*item.unscope_values)
152
+ scope.unscope!(*item.table_name_qualified_unscope_values)
153
153
  scope.where_clause += item.where_clause
154
154
  scope.order_values = item.order_values | scope.order_values
155
155
  end
@@ -80,11 +80,11 @@ module ActiveRecord
80
80
  end
81
81
 
82
82
  def target_changed?
83
- owner.attribute_changed?(reflection.foreign_key) || (!foreign_key_present? && target&.new_record?)
83
+ Array(reflection.foreign_key).any? { |fk| owner.attribute_changed?(fk) } || (!foreign_key_present? && target&.new_record?)
84
84
  end
85
85
 
86
86
  def target_previously_changed?
87
- owner.attribute_previously_changed?(reflection.foreign_key)
87
+ Array(reflection.foreign_key).any? { |fk| owner.attribute_previously_changed?(fk) }
88
88
  end
89
89
 
90
90
  def saved_change_to_target?
@@ -19,7 +19,7 @@ module ActiveRecord::Associations::Builder # :nodoc:
19
19
  self.extensions = []
20
20
 
21
21
  VALID_OPTIONS = [
22
- :class_name, :anonymous_class, :primary_key, :foreign_key, :dependent, :validate, :inverse_of, :strict_loading, :query_constraints
22
+ :anonymous_class, :primary_key, :foreign_key, :dependent, :validate, :inverse_of, :strict_loading, :query_constraints, :deprecated
23
23
  ].freeze # :nodoc:
24
24
 
25
25
  def self.build(model, name, scope, options, &block)
@@ -30,10 +30,10 @@ module ActiveRecord::Associations::Builder # :nodoc:
30
30
  end
31
31
 
32
32
  reflection = create_reflection(model, name, scope, options, &block)
33
- define_accessors model, reflection
34
- define_callbacks model, reflection
35
- define_validations model, reflection
36
- define_change_tracking_methods model, reflection
33
+ define_accessors(model, reflection)
34
+ define_callbacks(model, reflection)
35
+ define_validations(model, reflection)
36
+ define_change_tracking_methods(model, reflection)
37
37
  reflection
38
38
  end
39
39
 
@@ -71,6 +71,7 @@ module ActiveRecord::Associations::Builder # :nodoc:
71
71
  end
72
72
 
73
73
  def self.define_extensions(model, name)
74
+ # noop
74
75
  end
75
76
 
76
77
  def self.define_callbacks(model, reflection)
@@ -81,7 +82,7 @@ module ActiveRecord::Associations::Builder # :nodoc:
81
82
  end
82
83
 
83
84
  Association.extensions.each do |extension|
84
- extension.build model, reflection
85
+ extension.build(model, reflection)
85
86
  end
86
87
  end
87
88
 
@@ -101,7 +102,9 @@ module ActiveRecord::Associations::Builder # :nodoc:
101
102
  def self.define_readers(mixin, name)
102
103
  mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
103
104
  def #{name}
104
- association(:#{name}).reader
105
+ association = association(:#{name})
106
+ deprecated_associations_api_guard(association, __method__)
107
+ association.reader
105
108
  end
106
109
  CODE
107
110
  end
@@ -109,7 +112,9 @@ module ActiveRecord::Associations::Builder # :nodoc:
109
112
  def self.define_writers(mixin, name)
110
113
  mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
111
114
  def #{name}=(value)
112
- association(:#{name}).writer(value)
115
+ association = association(:#{name})
116
+ deprecated_associations_api_guard(association, __method__)
117
+ association.writer(value)
113
118
  end
114
119
  CODE
115
120
  end
@@ -131,14 +136,21 @@ module ActiveRecord::Associations::Builder # :nodoc:
131
136
  err_message = "A valid destroy_association_async_job is required to use `dependent: :destroy_async` on associations"
132
137
  raise ActiveRecord::ConfigurationError, err_message
133
138
  end
134
- unless valid_dependent_options.include? dependent
139
+ unless valid_dependent_options.include?(dependent)
135
140
  raise ArgumentError, "The :dependent option must be one of #{valid_dependent_options}, but is :#{dependent}"
136
141
  end
137
142
  end
138
143
 
139
144
  def self.add_destroy_callbacks(model, reflection)
140
- name = reflection.name
141
- model.before_destroy(->(o) { o.association(name).handle_dependency })
145
+ if reflection.deprecated?
146
+ # If :dependent is set, destroying the record has a side effect that
147
+ # would no longer happen if the association is removed.
148
+ model.before_destroy do
149
+ report_deprecated_association(reflection, context: ":dependent has a side effect here")
150
+ end
151
+ end
152
+
153
+ model.before_destroy(->(o) { o.association(reflection.name).handle_dependency })
142
154
  end
143
155
 
144
156
  def self.add_after_commit_jobs_callback(model, dependent)
@@ -8,8 +8,9 @@ module ActiveRecord::Associations::Builder # :nodoc:
8
8
 
9
9
  def self.valid_options(options)
10
10
  valid = super + [:polymorphic, :counter_cache, :optional, :default]
11
- valid += [:foreign_type] if options[:polymorphic]
12
- valid += [:ensuring_owner_was] if options[:dependent] == :destroy_async
11
+ valid << :class_name unless options[:polymorphic]
12
+ valid << :foreign_type if options[:polymorphic]
13
+ valid << :ensuring_owner_was if options[:dependent] == :destroy_async
13
14
  valid
14
15
  end
15
16
 
@@ -42,7 +43,16 @@ module ActiveRecord::Associations::Builder # :nodoc:
42
43
  end
43
44
 
44
45
  def self.touch_record(o, changes, foreign_key, name, touch) # :nodoc:
45
- old_foreign_id = changes[foreign_key] && changes[foreign_key].first
46
+ old_foreign_id =
47
+ if foreign_key.is_a?(Array)
48
+ if foreign_key.any? { |fk| changes[fk] }
49
+ foreign_key.map do |fk|
50
+ changes[fk] ? changes[fk].first : o.read_attribute(fk)
51
+ end
52
+ end
53
+ elsif changes[foreign_key]
54
+ changes[foreign_key].first
55
+ end
46
56
 
47
57
  if old_foreign_id
48
58
  association = o.association(name)
@@ -55,7 +65,7 @@ module ActiveRecord::Associations::Builder # :nodoc:
55
65
  klass = association.klass
56
66
  end
57
67
  primary_key = reflection.association_primary_key(klass)
58
- old_record = klass.find_by(primary_key => old_foreign_id)
68
+ old_record = klass.find_by(primary_key => [old_foreign_id])
59
69
 
60
70
  if old_record
61
71
  if touch != true
@@ -107,6 +117,14 @@ module ActiveRecord::Associations::Builder # :nodoc:
107
117
  end
108
118
 
109
119
  def self.add_destroy_callbacks(model, reflection)
120
+ if reflection.deprecated?
121
+ # If :dependent is set, destroying the record has some side effect that
122
+ # would no longer happen if the association is removed.
123
+ model.before_destroy do
124
+ report_deprecated_association(reflection, context: ":dependent has a side effect here")
125
+ end
126
+ end
127
+
110
128
  model.after_destroy lambda { |o| o.association(reflection.name).handle_dependency }
111
129
  end
112
130
 
@@ -131,8 +149,14 @@ module ActiveRecord::Associations::Builder # :nodoc:
131
149
  foreign_key = reflection.foreign_key
132
150
  foreign_type = reflection.foreign_type
133
151
 
134
- record.read_attribute(foreign_key).nil? ||
135
- record.attribute_changed?(foreign_key) ||
152
+ fk_missing_or_changed = if foreign_key.is_a?(Array)
153
+ foreign_key.any? { |fk| record.read_attribute(fk).nil? || record.attribute_changed?(fk) }
154
+ else
155
+ record.read_attribute(foreign_key).nil? ||
156
+ record.attribute_changed?(foreign_key)
157
+ end
158
+
159
+ fk_missing_or_changed ||
136
160
  (reflection.polymorphic? && (record.read_attribute(foreign_type).nil? || record.attribute_changed?(foreign_type)))
137
161
  }
138
162
 
@@ -144,11 +168,15 @@ module ActiveRecord::Associations::Builder # :nodoc:
144
168
  def self.define_change_tracking_methods(model, reflection)
145
169
  model.generated_association_methods.class_eval <<-CODE, __FILE__, __LINE__ + 1
146
170
  def #{reflection.name}_changed?
147
- association(:#{reflection.name}).target_changed?
171
+ association = association(:#{reflection.name})
172
+ deprecated_associations_api_guard(association, __method__)
173
+ association.target_changed?
148
174
  end
149
175
 
150
176
  def #{reflection.name}_previously_changed?
151
- association(:#{reflection.name}).target_previously_changed?
177
+ association = association(:#{reflection.name})
178
+ deprecated_associations_api_guard(association, __method__)
179
+ association.target_previously_changed?
152
180
  end
153
181
  CODE
154
182
  end
@@ -7,7 +7,7 @@ module ActiveRecord::Associations::Builder # :nodoc:
7
7
  CALLBACKS = [:before_add, :after_add, :before_remove, :after_remove]
8
8
 
9
9
  def self.valid_options(options)
10
- super + [:before_add, :after_add, :before_remove, :after_remove, :extend]
10
+ super + [:class_name, :before_add, :after_add, :before_remove, :after_remove, :extend]
11
11
  end
12
12
 
13
13
  def self.define_callbacks(model, reflection)
@@ -60,7 +60,9 @@ module ActiveRecord::Associations::Builder # :nodoc:
60
60
 
61
61
  mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
62
62
  def #{name.to_s.singularize}_ids
63
- association(:#{name}).ids_reader
63
+ association = association(:#{name})
64
+ deprecated_associations_api_guard(association, __method__)
65
+ association.ids_reader
64
66
  end
65
67
  CODE
66
68
  end
@@ -70,7 +72,9 @@ module ActiveRecord::Associations::Builder # :nodoc:
70
72
 
71
73
  mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
72
74
  def #{name.to_s.singularize}_ids=(ids)
73
- association(:#{name}).ids_writer(ids)
75
+ association = association(:#{name})
76
+ deprecated_associations_api_guard(association, __method__)
77
+ association.ids_writer(ids)
74
78
  end
75
79
  CODE
76
80
  end
@@ -7,7 +7,7 @@ module ActiveRecord::Associations::Builder # :nodoc:
7
7
  end
8
8
 
9
9
  def self.valid_options(options)
10
- valid = super + [:as, :through]
10
+ valid = super + [:class_name, :as, :through]
11
11
  valid += [:foreign_type] if options[:as]
12
12
  valid += [:ensuring_owner_was] if options[:dependent] == :destroy_async
13
13
  valid += [:source, :source_type, :disable_joins] if options[:through]
@@ -17,11 +17,15 @@ module ActiveRecord::Associations::Builder # :nodoc:
17
17
 
18
18
  mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
19
19
  def reload_#{name}
20
- association(:#{name}).force_reload_reader
20
+ association = association(:#{name})
21
+ deprecated_associations_api_guard(association, __method__)
22
+ association.force_reload_reader
21
23
  end
22
24
 
23
25
  def reset_#{name}
24
- association(:#{name}).reset
26
+ association = association(:#{name})
27
+ deprecated_associations_api_guard(association, __method__)
28
+ association.reset
25
29
  end
26
30
  CODE
27
31
  end
@@ -30,19 +34,43 @@ module ActiveRecord::Associations::Builder # :nodoc:
30
34
  def self.define_constructors(mixin, name)
31
35
  mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
32
36
  def build_#{name}(*args, &block)
33
- association(:#{name}).build(*args, &block)
37
+ association = association(:#{name})
38
+ deprecated_associations_api_guard(association, __method__)
39
+ association.build(*args, &block)
34
40
  end
35
41
 
36
42
  def create_#{name}(*args, &block)
37
- association(:#{name}).create(*args, &block)
43
+ association = association(:#{name})
44
+ deprecated_associations_api_guard(association, __method__)
45
+ association.create(*args, &block)
38
46
  end
39
47
 
40
48
  def create_#{name}!(*args, &block)
41
- association(:#{name}).create!(*args, &block)
49
+ association = association(:#{name})
50
+ deprecated_associations_api_guard(association, __method__)
51
+ association.create!(*args, &block)
42
52
  end
43
53
  CODE
44
54
  end
45
55
 
56
+ def self.define_callbacks(model, reflection)
57
+ super
58
+
59
+ # If the record is saved or destroyed and `:touch` is set, the parent
60
+ # record gets a timestamp updated. We want to know about it, because
61
+ # deleting the association would change that side-effect and perhaps there
62
+ # is code relying on it.
63
+ if reflection.deprecated? && reflection.options[:touch]
64
+ model.before_save do
65
+ report_deprecated_association(reflection, context: ":touch has a side effect here")
66
+ end
67
+
68
+ model.before_destroy do
69
+ report_deprecated_association(reflection, context: ":touch has a side effect here")
70
+ end
71
+ end
72
+ end
73
+
46
74
  private_class_method :valid_options, :define_accessors, :define_constructors
47
75
  end
48
76
  end
@@ -61,15 +61,19 @@ module ActiveRecord
61
61
  # Implements the ids writer method, e.g. foo.item_ids= for Foo.has_many :items
62
62
  def ids_writer(ids)
63
63
  primary_key = reflection.association_primary_key
64
- pk_type = klass.type_for_attribute(primary_key)
65
64
  ids = Array(ids).compact_blank
66
- ids.map! { |id| pk_type.cast(id) }
67
65
 
68
66
  records = if klass.composite_primary_key?
67
+ pk_types = primary_key.map { |column| klass.type_for_attribute(column) }
68
+ ids.map! { |id| pk_types.zip(id).map! { |type, value| type.cast(value) } }
69
+
69
70
  klass.where(primary_key => ids).index_by do |record|
70
71
  primary_key.map { |primary_key| record._read_attribute(primary_key) }
71
72
  end
72
73
  else
74
+ pk_type = klass.type_for_attribute(primary_key)
75
+ ids.map! { |id| pk_type.cast(id) }
76
+
73
77
  klass.where(primary_key => ids).index_by do |record|
74
78
  record._read_attribute(primary_key)
75
79
  end
@@ -94,7 +98,7 @@ module ActiveRecord
94
98
  def find(*args)
95
99
  if options[:inverse_of] && loaded?
96
100
  args_flatten = args.flatten
97
- model = scope.klass
101
+ model = scope.model
98
102
 
99
103
  if args_flatten.blank?
100
104
  error_message = "Couldn't find #{model.name} without an ID"
@@ -110,7 +110,7 @@ module ActiveRecord
110
110
  # # ]
111
111
 
112
112
  # Finds an object in the collection responding to the +id+. Uses the same
113
- # rules as ActiveRecord::FinderMethods.find. Returns ActiveRecord::RecordNotFound
113
+ # rules as ActiveRecord::FinderMethods.find. Raises ActiveRecord::RecordNotFound
114
114
  # error if the object cannot be found.
115
115
  #
116
116
  # class Person < ActiveRecord::Base
@@ -726,6 +726,10 @@ module ActiveRecord
726
726
  end
727
727
 
728
728
  def pluck(*column_names)
729
+ if proxy_association.violates_strict_loading?
730
+ Base.strict_loading_violation!(owner: proxy_association.owner.class, reflection: proxy_association.reflection)
731
+ end
732
+
729
733
  null_scope? ? scope.pluck(*column_names) : super
730
734
  end
731
735
 
@@ -1125,14 +1129,32 @@ module ActiveRecord
1125
1129
  super
1126
1130
  end
1127
1131
 
1132
+ %w(insert insert_all insert! insert_all! upsert upsert_all).each do |method|
1133
+ class_eval <<~RUBY, __FILE__, __LINE__ + 1
1134
+ def #{method}(...)
1135
+ if @association&.target&.any? { |r| r.new_record? }
1136
+ association_name = @association.reflection.name
1137
+ ActiveRecord.deprecator.warn(<<~MSG)
1138
+ Using #{method} on association \#{association_name} with unpersisted records
1139
+ is deprecated and will be removed in Rails 8.2.
1140
+ The unpersisted records will be lost after this operation.
1141
+ Please either persist your records first or store them separately before
1142
+ calling #{method}.
1143
+ MSG
1144
+ scope.#{method}(...)
1145
+ else
1146
+ scope.#{method}(...).tap { reset }
1147
+ end
1148
+ end
1149
+ RUBY
1150
+ end
1151
+
1128
1152
  delegate_methods = [
1129
1153
  QueryMethods,
1130
1154
  SpawnMethods,
1131
1155
  ].flat_map { |klass|
1132
1156
  klass.public_instance_methods(false)
1133
- } - self.public_instance_methods(false) - [:select] + [
1134
- :scoping, :values, :insert, :insert_all, :insert!, :insert_all!, :upsert, :upsert_all, :load_async
1135
- ]
1157
+ } - self.public_instance_methods(false) - [ :select ] + [ :scoping, :values, :load_async ]
1136
1158
 
1137
1159
  delegate(*delegate_methods, to: :scope)
1138
1160
 
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/notifications"
4
+ require "active_support/core_ext/array/conversions"
5
+
6
+ module ActiveRecord::Associations::Deprecation # :nodoc:
7
+ EVENT = "deprecated_association.active_record"
8
+ private_constant :EVENT
9
+
10
+ MODES = [:warn, :raise, :notify].freeze
11
+ private_constant :MODES
12
+
13
+ class << self
14
+ attr_reader :mode, :backtrace
15
+
16
+ def mode=(value) # private setter
17
+ unless MODES.include?(value)
18
+ raise ArgumentError, "invalid deprecated associations mode #{value.inspect} (valid modes are #{MODES.map(&:inspect).to_sentence})"
19
+ end
20
+
21
+ @mode = value
22
+ end
23
+
24
+ def backtrace=(value)
25
+ @backtrace = !!value
26
+ end
27
+
28
+ def guard(reflection)
29
+ report(reflection, context: yield) if reflection.deprecated?
30
+
31
+ if reflection.through_reflection?
32
+ reflection.deprecated_nested_reflections.each do |deprecated_nested_reflection|
33
+ context = "referenced as nested association of the through #{reflection.active_record}##{reflection.name}"
34
+ report(deprecated_nested_reflection, context: context)
35
+ end
36
+ end
37
+ end
38
+
39
+ def report(reflection, context:)
40
+ reflection = user_facing_reflection(reflection)
41
+
42
+ message = +"The association #{reflection.active_record}##{reflection.name} is deprecated, #{context}"
43
+ message << " (#{backtrace_cleaner.first_clean_frame})"
44
+
45
+ case @mode
46
+ when :warn
47
+ message = [message, *clean_frames].join("\n\t") if @backtrace
48
+ ActiveRecord::Base.logger&.warn(message)
49
+ when :raise
50
+ error = ActiveRecord::DeprecatedAssociationError.new(message)
51
+ if set_backtrace_supports_array_of_locations?
52
+ error.set_backtrace(clean_locations)
53
+ else
54
+ error.set_backtrace(clean_frames)
55
+ end
56
+ raise error
57
+ else
58
+ payload = { reflection: reflection, message: message, location: backtrace_cleaner.first_clean_location }
59
+ payload[:backtrace] = clean_locations if @backtrace
60
+ ActiveSupport::Notifications.instrument(EVENT, payload)
61
+ end
62
+ end
63
+
64
+ private
65
+ def backtrace_cleaner
66
+ ActiveRecord::LogSubscriber.backtrace_cleaner
67
+ end
68
+
69
+ def clean_frames
70
+ backtrace_cleaner.clean(caller)
71
+ end
72
+
73
+ def clean_locations
74
+ backtrace_cleaner.clean_locations(caller_locations)
75
+ end
76
+
77
+ def set_backtrace_supports_array_of_locations?
78
+ @backtrace_supports_array_of_locations ||= Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.4.0")
79
+ end
80
+
81
+ def user_facing_reflection(reflection)
82
+ reflection.active_record.reflect_on_association(reflection.name)
83
+ end
84
+ end
85
+
86
+ self.mode = :warn
87
+ self.backtrace = false
88
+ end