activerecord 8.0.0 → 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 (202) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1381 -241
  3. data/README.rdoc +2 -2
  4. data/lib/active_record/association_relation.rb +1 -1
  5. data/lib/active_record/associations/alias_tracker.rb +6 -4
  6. data/lib/active_record/associations/association.rb +35 -17
  7. data/lib/active_record/associations/association_scope.rb +1 -1
  8. data/lib/active_record/associations/belongs_to_association.rb +20 -4
  9. data/lib/active_record/associations/builder/association.rb +16 -5
  10. data/lib/active_record/associations/builder/belongs_to.rb +36 -8
  11. data/lib/active_record/associations/builder/collection_association.rb +7 -3
  12. data/lib/active_record/associations/builder/has_one.rb +1 -1
  13. data/lib/active_record/associations/builder/singular_association.rb +33 -5
  14. data/lib/active_record/associations/collection_association.rb +9 -5
  15. data/lib/active_record/associations/collection_proxy.rb +26 -4
  16. data/lib/active_record/associations/deprecation.rb +88 -0
  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_one_association.rb +1 -0
  20. data/lib/active_record/associations/join_dependency/join_association.rb +25 -27
  21. data/lib/active_record/associations/join_dependency.rb +5 -4
  22. data/lib/active_record/associations/preloader/association.rb +1 -1
  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.rb +159 -21
  26. data/lib/active_record/attribute_methods/primary_key.rb +2 -1
  27. data/lib/active_record/attribute_methods/query.rb +34 -0
  28. data/lib/active_record/attribute_methods/serialization.rb +19 -6
  29. data/lib/active_record/attribute_methods/time_zone_conversion.rb +10 -2
  30. data/lib/active_record/attribute_methods.rb +24 -17
  31. data/lib/active_record/attributes.rb +40 -26
  32. data/lib/active_record/autosave_association.rb +22 -12
  33. data/lib/active_record/base.rb +3 -4
  34. data/lib/active_record/callbacks.rb +1 -2
  35. data/lib/active_record/coders/column_serializer.rb +1 -1
  36. data/lib/active_record/coders/json.rb +16 -6
  37. data/lib/active_record/coders/yaml_column.rb +2 -2
  38. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +19 -18
  39. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +16 -3
  40. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +68 -12
  41. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +474 -112
  42. data/lib/active_record/connection_adapters/abstract/database_statements.rb +68 -41
  43. data/lib/active_record/connection_adapters/abstract/query_cache.rb +36 -9
  44. data/lib/active_record/connection_adapters/abstract/quoting.rb +15 -24
  45. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +7 -2
  46. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +31 -35
  47. data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +2 -1
  48. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +94 -28
  49. data/lib/active_record/connection_adapters/abstract/transaction.rb +25 -3
  50. data/lib/active_record/connection_adapters/abstract_adapter.rb +166 -64
  51. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +133 -40
  52. data/lib/active_record/connection_adapters/column.rb +14 -4
  53. data/lib/active_record/connection_adapters/mysql/database_statements.rb +4 -4
  54. data/lib/active_record/connection_adapters/mysql/quoting.rb +7 -1
  55. data/lib/active_record/connection_adapters/mysql/schema_creation.rb +2 -0
  56. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +42 -5
  57. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +40 -25
  58. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +66 -15
  59. data/lib/active_record/connection_adapters/mysql2_adapter.rb +9 -3
  60. data/lib/active_record/connection_adapters/pool_config.rb +7 -7
  61. data/lib/active_record/connection_adapters/postgresql/column.rb +8 -2
  62. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +29 -33
  63. data/lib/active_record/connection_adapters/postgresql/oid/array.rb +2 -2
  64. data/lib/active_record/connection_adapters/postgresql/oid/range.rb +46 -3
  65. data/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +1 -1
  66. data/lib/active_record/connection_adapters/postgresql/quoting.rb +21 -10
  67. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +1 -1
  68. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +8 -6
  69. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +22 -33
  70. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +70 -32
  71. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +99 -54
  72. data/lib/active_record/connection_adapters/postgresql_adapter.rb +40 -12
  73. data/lib/active_record/connection_adapters/sqlite3/column.rb +8 -2
  74. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +39 -23
  75. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +13 -8
  76. data/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +15 -3
  77. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +7 -14
  78. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +68 -37
  79. data/lib/active_record/connection_adapters/statement_pool.rb +4 -2
  80. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +8 -3
  81. data/lib/active_record/connection_adapters/trilogy_adapter.rb +16 -2
  82. data/lib/active_record/connection_adapters.rb +1 -0
  83. data/lib/active_record/connection_handling.rb +15 -10
  84. data/lib/active_record/core.rb +44 -12
  85. data/lib/active_record/counter_cache.rb +36 -9
  86. data/lib/active_record/database_configurations/connection_url_resolver.rb +3 -1
  87. data/lib/active_record/database_configurations/database_config.rb +5 -1
  88. data/lib/active_record/database_configurations/hash_config.rb +66 -9
  89. data/lib/active_record/database_configurations/url_config.rb +15 -3
  90. data/lib/active_record/database_configurations.rb +7 -3
  91. data/lib/active_record/delegated_type.rb +19 -19
  92. data/lib/active_record/disable_joins_association_relation.rb +5 -1
  93. data/lib/active_record/dynamic_matchers.rb +54 -69
  94. data/lib/active_record/encryption/encryptable_record.rb +5 -5
  95. data/lib/active_record/encryption/encrypted_attribute_type.rb +2 -2
  96. data/lib/active_record/encryption/encrypted_fixtures.rb +12 -7
  97. data/lib/active_record/encryption/encryptor.rb +39 -25
  98. data/lib/active_record/encryption/scheme.rb +1 -1
  99. data/lib/active_record/enum.rb +37 -20
  100. data/lib/active_record/errors.rb +23 -7
  101. data/lib/active_record/explain.rb +1 -1
  102. data/lib/active_record/explain_registry.rb +51 -2
  103. data/lib/active_record/filter_attribute_handler.rb +76 -0
  104. data/lib/active_record/fixture_set/table_row.rb +19 -2
  105. data/lib/active_record/fixtures.rb +2 -2
  106. data/lib/active_record/future_result.rb +5 -3
  107. data/lib/active_record/gem_version.rb +2 -2
  108. data/lib/active_record/inheritance.rb +9 -4
  109. data/lib/active_record/insert_all.rb +14 -9
  110. data/lib/active_record/locking/optimistic.rb +7 -0
  111. data/lib/active_record/locking/pessimistic.rb +5 -0
  112. data/lib/active_record/log_subscriber.rb +30 -22
  113. data/lib/active_record/message_pack.rb +13 -0
  114. data/lib/active_record/middleware/shard_selector.rb +34 -17
  115. data/lib/active_record/migration/command_recorder.rb +32 -6
  116. data/lib/active_record/migration/compatibility.rb +34 -24
  117. data/lib/active_record/migration/default_schema_versions_formatter.rb +30 -0
  118. data/lib/active_record/migration.rb +31 -21
  119. data/lib/active_record/model_schema.rb +38 -12
  120. data/lib/active_record/nested_attributes.rb +10 -6
  121. data/lib/active_record/persistence.rb +35 -3
  122. data/lib/active_record/query_cache.rb +22 -15
  123. data/lib/active_record/query_logs.rb +7 -7
  124. data/lib/active_record/querying.rb +17 -5
  125. data/lib/active_record/railtie.rb +35 -6
  126. data/lib/active_record/railties/controller_runtime.rb +11 -6
  127. data/lib/active_record/railties/databases.rake +24 -20
  128. data/lib/active_record/railties/job_checkpoints.rb +15 -0
  129. data/lib/active_record/railties/job_runtime.rb +10 -11
  130. data/lib/active_record/reflection.rb +36 -1
  131. data/lib/active_record/relation/batches.rb +25 -11
  132. data/lib/active_record/relation/calculations.rb +62 -41
  133. data/lib/active_record/relation/delegation.rb +0 -1
  134. data/lib/active_record/relation/finder_methods.rb +76 -45
  135. data/lib/active_record/relation/merger.rb +4 -2
  136. data/lib/active_record/relation/predicate_builder/array_handler.rb +3 -1
  137. data/lib/active_record/relation/predicate_builder/association_query_value.rb +11 -9
  138. data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +7 -7
  139. data/lib/active_record/relation/predicate_builder.rb +10 -8
  140. data/lib/active_record/relation/query_attribute.rb +4 -2
  141. data/lib/active_record/relation/query_methods.rb +106 -45
  142. data/lib/active_record/relation/spawn_methods.rb +6 -6
  143. data/lib/active_record/relation/where_clause.rb +10 -11
  144. data/lib/active_record/relation.rb +63 -31
  145. data/lib/active_record/result.rb +44 -21
  146. data/lib/active_record/runtime_registry.rb +42 -58
  147. data/lib/active_record/sanitization.rb +2 -0
  148. data/lib/active_record/schema.rb +1 -1
  149. data/lib/active_record/schema_dumper.rb +41 -21
  150. data/lib/active_record/scoping.rb +0 -1
  151. data/lib/active_record/secure_token.rb +3 -3
  152. data/lib/active_record/signed_id.rb +48 -19
  153. data/lib/active_record/statement_cache.rb +16 -12
  154. data/lib/active_record/store.rb +46 -19
  155. data/lib/active_record/structured_event_subscriber.rb +85 -0
  156. data/lib/active_record/table_metadata.rb +5 -20
  157. data/lib/active_record/tasks/abstract_tasks.rb +83 -0
  158. data/lib/active_record/tasks/database_tasks.rb +44 -45
  159. data/lib/active_record/tasks/mysql_database_tasks.rb +4 -56
  160. data/lib/active_record/tasks/postgresql_database_tasks.rb +14 -40
  161. data/lib/active_record/tasks/sqlite_database_tasks.rb +14 -26
  162. data/lib/active_record/test_databases.rb +18 -4
  163. data/lib/active_record/test_fixtures.rb +40 -6
  164. data/lib/active_record/testing/query_assertions.rb +8 -2
  165. data/lib/active_record/timestamp.rb +4 -2
  166. data/lib/active_record/transaction.rb +2 -5
  167. data/lib/active_record/transactions.rb +58 -32
  168. data/lib/active_record/type/hash_lookup_type_map.rb +2 -1
  169. data/lib/active_record/type/internal/timezone.rb +7 -0
  170. data/lib/active_record/type/json.rb +13 -2
  171. data/lib/active_record/type/serialized.rb +16 -4
  172. data/lib/active_record/type/type_map.rb +1 -1
  173. data/lib/active_record/type_caster/connection.rb +2 -1
  174. data/lib/active_record/validations/associated.rb +1 -1
  175. data/lib/active_record.rb +72 -7
  176. data/lib/arel/alias_predication.rb +2 -0
  177. data/lib/arel/collectors/bind.rb +1 -1
  178. data/lib/arel/collectors/sql_string.rb +1 -1
  179. data/lib/arel/collectors/substitute_binds.rb +2 -2
  180. data/lib/arel/crud.rb +8 -11
  181. data/lib/arel/delete_manager.rb +5 -0
  182. data/lib/arel/nodes/binary.rb +1 -1
  183. data/lib/arel/nodes/count.rb +2 -2
  184. data/lib/arel/nodes/delete_statement.rb +4 -2
  185. data/lib/arel/nodes/function.rb +4 -10
  186. data/lib/arel/nodes/named_function.rb +2 -2
  187. data/lib/arel/nodes/node.rb +2 -2
  188. data/lib/arel/nodes/sql_literal.rb +1 -1
  189. data/lib/arel/nodes/update_statement.rb +4 -2
  190. data/lib/arel/nodes.rb +0 -2
  191. data/lib/arel/predications.rb +1 -3
  192. data/lib/arel/select_manager.rb +13 -4
  193. data/lib/arel/update_manager.rb +5 -0
  194. data/lib/arel/visitors/dot.rb +2 -3
  195. data/lib/arel/visitors/postgresql.rb +55 -0
  196. data/lib/arel/visitors/sqlite.rb +55 -8
  197. data/lib/arel/visitors/to_sql.rb +7 -23
  198. data/lib/arel.rb +3 -1
  199. data/lib/rails/generators/active_record/application_record/USAGE +1 -1
  200. metadata +16 -15
  201. data/lib/active_record/explain_subscriber.rb +0 -34
  202. data/lib/active_record/normalization.rb +0 -163
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[8.0]
142
+ class AddSystemSettings < ActiveRecord::Migration[8.1]
143
143
  def up
144
144
  create_table :system_settings do |t|
145
145
  t.string :name
@@ -214,6 +214,6 @@ Bug reports for the Ruby on \Rails project can be filed here:
214
214
 
215
215
  * https://github.com/rails/rails/issues
216
216
 
217
- Feature requests should be discussed on the rails-core mailing list here:
217
+ Feature requests should be discussed on the rubyonrails-core forum here:
218
218
 
219
219
  * https://discuss.rubyonrails.org/c/rubyonrails-core
@@ -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
@@ -26,16 +26,18 @@ module ActiveRecord
26
26
  end
27
27
 
28
28
  def self.initial_count_for(connection, name, table_joins)
29
- quoted_name = nil
29
+ quoted_name_escaped = nil
30
+ name_escaped = nil
30
31
 
31
32
  counts = table_joins.map do |join|
32
33
  if join.is_a?(Arel::Nodes::StringJoin)
33
- # quoted_name should be case ignored as some database adapters (Oracle) return quoted name in uppercase
34
- quoted_name ||= connection.quote_table_name(name)
34
+ # quoted_name_escaped should be case ignored as some database adapters (Oracle) return quoted name in uppercase
35
+ quoted_name_escaped ||= Regexp.escape(connection.quote_table_name(name))
36
+ name_escaped ||= Regexp.escape(name)
35
37
 
36
38
  # Table names + table aliases
37
39
  join.left.scan(
38
- /JOIN(?:\s+\w+)?\s+(?:\S+\s+)?(?:#{quoted_name}|#{name})\sON/i
40
+ /JOIN(?:\s+\w+)?\s+(?:\S+\s+)?(?:#{quoted_name_escaped}|#{name_escaped})\sON/i
39
41
  ).size
40
42
  elsif join.is_a?(Arel::Nodes::Join)
41
43
  join.left.name == name ? 1 : 0
@@ -232,7 +232,19 @@ module ActiveRecord
232
232
  _create_record(attributes, true, &block)
233
233
  end
234
234
 
235
- # 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
236
248
  # or a collection of records.
237
249
  def collection?
238
250
  false
@@ -281,16 +293,6 @@ module ActiveRecord
281
293
  @skip_strict_loading = skip_strict_loading_was
282
294
  end
283
295
 
284
- def violates_strict_loading?
285
- return if @skip_strict_loading
286
-
287
- return unless owner.validation_context.nil?
288
-
289
- return reflection.strict_loading? if reflection.options.key?(:strict_loading)
290
-
291
- owner.strict_loading? && !owner.strict_loading_n_plus_one_only?
292
- end
293
-
294
296
  # The scope for this association.
295
297
  #
296
298
  # Note that the association_scope is merged into the target_scope only when the
@@ -409,12 +411,28 @@ module ActiveRecord
409
411
  end
410
412
 
411
413
  def matches_foreign_key?(record)
412
- if foreign_key_for?(record)
413
- record.read_attribute(reflection.foreign_key) == owner.id ||
414
- (foreign_key_for?(owner) && owner.read_attribute(reflection.foreign_key) == record.id)
415
- else
416
- owner.read_attribute(reflection.foreign_key) == record.id
417
- 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) }
418
436
  end
419
437
  end
420
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
@@ -19,10 +19,16 @@ module ActiveRecord
19
19
  id = owner.public_send(reflection.foreign_key)
20
20
  end
21
21
 
22
+ association_class = if reflection.polymorphic?
23
+ owner.public_send(reflection.foreign_type)
24
+ else
25
+ reflection.klass
26
+ end
27
+
22
28
  enqueue_destroy_association(
23
29
  owner_model_name: owner.class.to_s,
24
30
  owner_id: owner.id,
25
- association_class: reflection.klass.to_s,
31
+ association_class: association_class.to_s,
26
32
  association_ids: [id],
27
33
  association_primary_key_column: primary_key_column,
28
34
  ensuring_owner_was_method: options.fetch(:ensuring_owner_was, nil)
@@ -74,11 +80,11 @@ module ActiveRecord
74
80
  end
75
81
 
76
82
  def target_changed?
77
- 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?)
78
84
  end
79
85
 
80
86
  def target_previously_changed?
81
- owner.attribute_previously_changed?(reflection.foreign_key)
87
+ Array(reflection.foreign_key).any? { |fk| owner.attribute_previously_changed?(fk) }
82
88
  end
83
89
 
84
90
  def saved_change_to_target?
@@ -129,7 +135,9 @@ module ActiveRecord
129
135
  target_key_values = record ? Array(primary_key(record.class)).map { |key| record._read_attribute(key) } : []
130
136
 
131
137
  if force || reflection_fk.map { |fk| owner._read_attribute(fk) } != target_key_values
138
+ owner_pk = Array(owner.class.primary_key)
132
139
  reflection_fk.each_with_index do |key, index|
140
+ next if record.nil? && owner_pk.include?(key)
133
141
  owner[key] = target_key_values[index]
134
142
  end
135
143
  end
@@ -156,7 +164,15 @@ module ActiveRecord
156
164
  end
157
165
 
158
166
  def stale_state
159
- owner._read_attribute(reflection.foreign_key) { |n| owner.send(:missing_attribute, n, caller) }
167
+ foreign_key = reflection.foreign_key
168
+ if foreign_key.is_a?(Array)
169
+ attributes = foreign_key.map do |fk|
170
+ owner._read_attribute(fk) { |n| owner.send(:missing_attribute, n, caller) }
171
+ end
172
+ attributes if attributes.any?
173
+ else
174
+ owner._read_attribute(foreign_key) { |n| owner.send(:missing_attribute, n, caller) }
175
+ end
160
176
  end
161
177
  end
162
178
  end
@@ -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)
@@ -102,7 +102,9 @@ module ActiveRecord::Associations::Builder # :nodoc:
102
102
  def self.define_readers(mixin, name)
103
103
  mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
104
104
  def #{name}
105
- association(:#{name}).reader
105
+ association = association(:#{name})
106
+ deprecated_associations_api_guard(association, __method__)
107
+ association.reader
106
108
  end
107
109
  CODE
108
110
  end
@@ -110,7 +112,9 @@ module ActiveRecord::Associations::Builder # :nodoc:
110
112
  def self.define_writers(mixin, name)
111
113
  mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
112
114
  def #{name}=(value)
113
- association(:#{name}).writer(value)
115
+ association = association(:#{name})
116
+ deprecated_associations_api_guard(association, __method__)
117
+ association.writer(value)
114
118
  end
115
119
  CODE
116
120
  end
@@ -138,8 +142,15 @@ module ActiveRecord::Associations::Builder # :nodoc:
138
142
  end
139
143
 
140
144
  def self.add_destroy_callbacks(model, reflection)
141
- name = reflection.name
142
- 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 })
143
154
  end
144
155
 
145
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
@@ -259,10 +263,10 @@ module ActiveRecord
259
263
  klass = reflection.klass
260
264
  return false unless record.is_a?(klass)
261
265
 
262
- if record.new_record?
263
- include_in_memory?(record)
264
- elsif loaded?
266
+ if loaded?
265
267
  target.include?(record)
268
+ elsif record.new_record?
269
+ include_in_memory?(record)
266
270
  else
267
271
  record_id = klass.composite_primary_key? ? klass.primary_key.zip(record.id).to_h : record.id
268
272
  scope.exists?(record_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
@@ -262,4 +262,7 @@ module ActiveRecord
262
262
  end
263
263
  end
264
264
  end
265
+
266
+ class DeprecatedAssociationError < ActiveRecordError
267
+ end
265
268
  end
@@ -4,7 +4,9 @@ module ActiveRecord::Associations
4
4
  module ForeignAssociation # :nodoc:
5
5
  def foreign_key_present?
6
6
  if reflection.klass.primary_key
7
- owner.attribute_present?(reflection.active_record_primary_key)
7
+ Array(reflection.active_record_primary_key).all? do |key|
8
+ owner.attribute_present?(key)
9
+ end
8
10
  else
9
11
  false
10
12
  end
@@ -120,6 +120,7 @@ module ActiveRecord
120
120
  Array(reflection.foreign_key).each do |foreign_key_column|
121
121
  record[foreign_key_column] = nil unless foreign_key_column.in?(Array(record.class.primary_key))
122
122
  end
123
+ record[reflection.type] = nil if reflection.type.present?
123
124
  end
124
125
 
125
126
  def transaction_if(value, &block)