activerecord 7.2.2.1 → 8.1.2

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 (206) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +564 -753
  3. data/README.rdoc +2 -2
  4. data/lib/active_record/association_relation.rb +2 -1
  5. data/lib/active_record/associations/alias_tracker.rb +6 -4
  6. data/lib/active_record/associations/association.rb +35 -11
  7. data/lib/active_record/associations/belongs_to_association.rb +18 -2
  8. data/lib/active_record/associations/builder/association.rb +23 -11
  9. data/lib/active_record/associations/builder/belongs_to.rb +17 -4
  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 +10 -8
  14. data/lib/active_record/associations/collection_proxy.rb +22 -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/has_many_through_association.rb +3 -2
  19. data/lib/active_record/associations/join_dependency/join_association.rb +25 -27
  20. data/lib/active_record/associations/join_dependency.rb +4 -2
  21. data/lib/active_record/associations/preloader/association.rb +2 -2
  22. data/lib/active_record/associations/preloader/batch.rb +7 -1
  23. data/lib/active_record/associations/preloader/branch.rb +1 -0
  24. data/lib/active_record/associations/singular_association.rb +8 -3
  25. data/lib/active_record/associations.rb +192 -24
  26. data/lib/active_record/asynchronous_queries_tracker.rb +28 -24
  27. data/lib/active_record/attribute_methods/primary_key.rb +4 -8
  28. data/lib/active_record/attribute_methods/query.rb +34 -0
  29. data/lib/active_record/attribute_methods/serialization.rb +17 -4
  30. data/lib/active_record/attribute_methods/time_zone_conversion.rb +12 -14
  31. data/lib/active_record/attribute_methods.rb +24 -19
  32. data/lib/active_record/attributes.rb +40 -26
  33. data/lib/active_record/autosave_association.rb +91 -39
  34. data/lib/active_record/base.rb +3 -4
  35. data/lib/active_record/coders/json.rb +14 -5
  36. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +35 -28
  37. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +16 -4
  38. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +51 -13
  39. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +458 -117
  40. data/lib/active_record/connection_adapters/abstract/database_statements.rb +136 -74
  41. data/lib/active_record/connection_adapters/abstract/query_cache.rb +44 -11
  42. data/lib/active_record/connection_adapters/abstract/quoting.rb +16 -25
  43. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +11 -7
  44. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +37 -36
  45. data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +2 -1
  46. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +122 -29
  47. data/lib/active_record/connection_adapters/abstract/transaction.rb +40 -8
  48. data/lib/active_record/connection_adapters/abstract_adapter.rb +175 -87
  49. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +77 -58
  50. data/lib/active_record/connection_adapters/column.rb +17 -4
  51. data/lib/active_record/connection_adapters/mysql/database_statements.rb +4 -4
  52. data/lib/active_record/connection_adapters/mysql/quoting.rb +7 -9
  53. data/lib/active_record/connection_adapters/mysql/schema_creation.rb +2 -0
  54. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +41 -10
  55. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +73 -46
  56. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +89 -94
  57. data/lib/active_record/connection_adapters/mysql2_adapter.rb +10 -11
  58. data/lib/active_record/connection_adapters/pool_config.rb +7 -7
  59. data/lib/active_record/connection_adapters/postgresql/column.rb +4 -0
  60. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +76 -45
  61. data/lib/active_record/connection_adapters/postgresql/oid/array.rb +3 -3
  62. data/lib/active_record/connection_adapters/postgresql/oid/point.rb +10 -0
  63. data/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +1 -1
  64. data/lib/active_record/connection_adapters/postgresql/quoting.rb +21 -10
  65. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +2 -4
  66. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +9 -17
  67. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +28 -45
  68. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +69 -32
  69. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +140 -64
  70. data/lib/active_record/connection_adapters/postgresql_adapter.rb +83 -105
  71. data/lib/active_record/connection_adapters/schema_cache.rb +3 -5
  72. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +90 -98
  73. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +13 -8
  74. data/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +0 -6
  75. data/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +27 -2
  76. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +13 -13
  77. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +112 -42
  78. data/lib/active_record/connection_adapters/statement_pool.rb +4 -2
  79. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +38 -67
  80. data/lib/active_record/connection_adapters/trilogy_adapter.rb +2 -19
  81. data/lib/active_record/connection_adapters.rb +1 -56
  82. data/lib/active_record/connection_handling.rb +37 -10
  83. data/lib/active_record/core.rb +61 -25
  84. data/lib/active_record/counter_cache.rb +34 -9
  85. data/lib/active_record/database_configurations/connection_url_resolver.rb +3 -1
  86. data/lib/active_record/database_configurations/database_config.rb +9 -1
  87. data/lib/active_record/database_configurations/hash_config.rb +67 -9
  88. data/lib/active_record/database_configurations/url_config.rb +13 -3
  89. data/lib/active_record/database_configurations.rb +7 -3
  90. data/lib/active_record/delegated_type.rb +19 -19
  91. data/lib/active_record/dynamic_matchers.rb +54 -69
  92. data/lib/active_record/encryption/config.rb +3 -1
  93. data/lib/active_record/encryption/encryptable_record.rb +9 -9
  94. data/lib/active_record/encryption/encrypted_attribute_type.rb +12 -3
  95. data/lib/active_record/encryption/encryptor.rb +49 -28
  96. data/lib/active_record/encryption/extended_deterministic_queries.rb +4 -2
  97. data/lib/active_record/encryption/scheme.rb +9 -2
  98. data/lib/active_record/enum.rb +46 -42
  99. data/lib/active_record/errors.rb +36 -12
  100. data/lib/active_record/explain.rb +1 -1
  101. data/lib/active_record/explain_registry.rb +51 -2
  102. data/lib/active_record/filter_attribute_handler.rb +73 -0
  103. data/lib/active_record/fixture_set/table_row.rb +19 -2
  104. data/lib/active_record/fixtures.rb +2 -4
  105. data/lib/active_record/future_result.rb +13 -9
  106. data/lib/active_record/gem_version.rb +3 -3
  107. data/lib/active_record/inheritance.rb +1 -1
  108. data/lib/active_record/insert_all.rb +12 -7
  109. data/lib/active_record/locking/optimistic.rb +8 -1
  110. data/lib/active_record/locking/pessimistic.rb +5 -0
  111. data/lib/active_record/log_subscriber.rb +3 -13
  112. data/lib/active_record/middleware/shard_selector.rb +34 -17
  113. data/lib/active_record/migration/command_recorder.rb +44 -11
  114. data/lib/active_record/migration/compatibility.rb +37 -24
  115. data/lib/active_record/migration/default_schema_versions_formatter.rb +30 -0
  116. data/lib/active_record/migration.rb +50 -43
  117. data/lib/active_record/model_schema.rb +38 -13
  118. data/lib/active_record/nested_attributes.rb +6 -6
  119. data/lib/active_record/persistence.rb +162 -133
  120. data/lib/active_record/query_cache.rb +22 -15
  121. data/lib/active_record/query_logs.rb +104 -52
  122. data/lib/active_record/query_logs_formatter.rb +17 -28
  123. data/lib/active_record/querying.rb +12 -12
  124. data/lib/active_record/railtie.rb +37 -32
  125. data/lib/active_record/railties/controller_runtime.rb +11 -6
  126. data/lib/active_record/railties/databases.rake +26 -37
  127. data/lib/active_record/railties/job_checkpoints.rb +15 -0
  128. data/lib/active_record/railties/job_runtime.rb +10 -11
  129. data/lib/active_record/reflection.rb +53 -21
  130. data/lib/active_record/relation/batches/batch_enumerator.rb +4 -3
  131. data/lib/active_record/relation/batches.rb +147 -73
  132. data/lib/active_record/relation/calculations.rb +80 -63
  133. data/lib/active_record/relation/delegation.rb +25 -15
  134. data/lib/active_record/relation/finder_methods.rb +54 -37
  135. data/lib/active_record/relation/merger.rb +8 -8
  136. data/lib/active_record/relation/predicate_builder/association_query_value.rb +11 -9
  137. data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +8 -8
  138. data/lib/active_record/relation/predicate_builder/relation_handler.rb +4 -3
  139. data/lib/active_record/relation/predicate_builder.rb +22 -7
  140. data/lib/active_record/relation/query_attribute.rb +4 -2
  141. data/lib/active_record/relation/query_methods.rb +156 -95
  142. data/lib/active_record/relation/spawn_methods.rb +7 -7
  143. data/lib/active_record/relation/where_clause.rb +10 -11
  144. data/lib/active_record/relation.rb +122 -80
  145. data/lib/active_record/result.rb +109 -24
  146. data/lib/active_record/runtime_registry.rb +42 -58
  147. data/lib/active_record/sanitization.rb +9 -6
  148. data/lib/active_record/schema_dumper.rb +47 -22
  149. data/lib/active_record/schema_migration.rb +2 -1
  150. data/lib/active_record/scoping/named.rb +5 -2
  151. data/lib/active_record/scoping.rb +0 -1
  152. data/lib/active_record/secure_token.rb +3 -3
  153. data/lib/active_record/signed_id.rb +47 -18
  154. data/lib/active_record/statement_cache.rb +24 -20
  155. data/lib/active_record/store.rb +51 -22
  156. data/lib/active_record/structured_event_subscriber.rb +85 -0
  157. data/lib/active_record/table_metadata.rb +6 -23
  158. data/lib/active_record/tasks/abstract_tasks.rb +76 -0
  159. data/lib/active_record/tasks/database_tasks.rb +85 -85
  160. data/lib/active_record/tasks/mysql_database_tasks.rb +3 -42
  161. data/lib/active_record/tasks/postgresql_database_tasks.rb +14 -40
  162. data/lib/active_record/tasks/sqlite_database_tasks.rb +16 -28
  163. data/lib/active_record/test_databases.rb +14 -4
  164. data/lib/active_record/test_fixtures.rb +39 -2
  165. data/lib/active_record/testing/query_assertions.rb +8 -2
  166. data/lib/active_record/timestamp.rb +4 -2
  167. data/lib/active_record/token_for.rb +1 -1
  168. data/lib/active_record/transaction.rb +2 -5
  169. data/lib/active_record/transactions.rb +39 -16
  170. data/lib/active_record/type/hash_lookup_type_map.rb +2 -1
  171. data/lib/active_record/type/internal/timezone.rb +7 -0
  172. data/lib/active_record/type/json.rb +15 -2
  173. data/lib/active_record/type/serialized.rb +11 -4
  174. data/lib/active_record/type/type_map.rb +1 -1
  175. data/lib/active_record/type_caster/connection.rb +2 -1
  176. data/lib/active_record/validations/associated.rb +1 -1
  177. data/lib/active_record/validations/uniqueness.rb +8 -8
  178. data/lib/active_record.rb +85 -50
  179. data/lib/arel/alias_predication.rb +2 -0
  180. data/lib/arel/collectors/bind.rb +2 -2
  181. data/lib/arel/collectors/sql_string.rb +1 -1
  182. data/lib/arel/collectors/substitute_binds.rb +2 -2
  183. data/lib/arel/crud.rb +8 -11
  184. data/lib/arel/delete_manager.rb +5 -0
  185. data/lib/arel/nodes/binary.rb +1 -1
  186. data/lib/arel/nodes/count.rb +2 -2
  187. data/lib/arel/nodes/delete_statement.rb +4 -2
  188. data/lib/arel/nodes/function.rb +4 -10
  189. data/lib/arel/nodes/named_function.rb +2 -2
  190. data/lib/arel/nodes/node.rb +2 -2
  191. data/lib/arel/nodes/sql_literal.rb +1 -1
  192. data/lib/arel/nodes/update_statement.rb +4 -2
  193. data/lib/arel/nodes.rb +0 -2
  194. data/lib/arel/select_manager.rb +13 -4
  195. data/lib/arel/table.rb +3 -7
  196. data/lib/arel/update_manager.rb +5 -0
  197. data/lib/arel/visitors/dot.rb +2 -3
  198. data/lib/arel/visitors/postgresql.rb +55 -0
  199. data/lib/arel/visitors/sqlite.rb +55 -8
  200. data/lib/arel/visitors/to_sql.rb +6 -22
  201. data/lib/arel.rb +3 -1
  202. data/lib/rails/generators/active_record/application_record/USAGE +1 -1
  203. metadata +17 -17
  204. data/lib/active_record/explain_subscriber.rb +0 -34
  205. data/lib/active_record/normalization.rb +0 -163
  206. 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
@@ -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
@@ -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
@@ -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
@@ -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,7 @@ module ActiveRecord
210
232
  _create_record(attributes, true, &block)
211
233
  end
212
234
 
213
- # Whether the association represent a single record
235
+ # Whether the association represents a single record
214
236
  # or a collection of records.
215
237
  def collection?
216
238
  false
@@ -223,13 +245,19 @@ module ActiveRecord
223
245
  klass
224
246
  end
225
247
 
226
- def find_target
248
+ def find_target(async: false)
227
249
  if violates_strict_loading?
228
250
  Base.strict_loading_violation!(owner: owner.class, reflection: reflection)
229
251
  end
230
252
 
231
253
  scope = self.scope
232
- return scope.to_a if skip_statement_cache?(scope)
254
+ if skip_statement_cache?(scope)
255
+ if async
256
+ return scope.load_async.then(&:to_a)
257
+ else
258
+ return scope.to_a
259
+ end
260
+ end
233
261
 
234
262
  sc = reflection.association_scope_cache(klass, owner) do |params|
235
263
  as = AssociationScope.create { params.bind }
@@ -238,13 +266,9 @@ module ActiveRecord
238
266
 
239
267
  binds = AssociationScope.get_bind_values(owner, reflection.chain)
240
268
  klass.with_connection do |c|
241
- sc.execute(binds, c) do |record|
269
+ sc.execute(binds, c, async: async) do |record|
242
270
  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
271
+ set_strict_loading(record)
248
272
  end
249
273
  end
250
274
  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)
@@ -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)
@@ -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
 
@@ -107,6 +108,14 @@ module ActiveRecord::Associations::Builder # :nodoc:
107
108
  end
108
109
 
109
110
  def self.add_destroy_callbacks(model, reflection)
111
+ if reflection.deprecated?
112
+ # If :dependent is set, destroying the record has some side effect that
113
+ # would no longer happen if the association is removed.
114
+ model.before_destroy do
115
+ report_deprecated_association(reflection, context: ":dependent has a side effect here")
116
+ end
117
+ end
118
+
110
119
  model.after_destroy lambda { |o| o.association(reflection.name).handle_dependency }
111
120
  end
112
121
 
@@ -144,11 +153,15 @@ module ActiveRecord::Associations::Builder # :nodoc:
144
153
  def self.define_change_tracking_methods(model, reflection)
145
154
  model.generated_association_methods.class_eval <<-CODE, __FILE__, __LINE__ + 1
146
155
  def #{reflection.name}_changed?
147
- association(:#{reflection.name}).target_changed?
156
+ association = association(:#{reflection.name})
157
+ deprecated_associations_api_guard(association, __method__)
158
+ association.target_changed?
148
159
  end
149
160
 
150
161
  def #{reflection.name}_previously_changed?
151
- association(:#{reflection.name}).target_previously_changed?
162
+ association = association(:#{reflection.name})
163
+ deprecated_associations_api_guard(association, __method__)
164
+ association.target_previously_changed?
152
165
  end
153
166
  CODE
154
167
  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
@@ -94,7 +94,7 @@ module ActiveRecord
94
94
  def find(*args)
95
95
  if options[:inverse_of] && loaded?
96
96
  args_flatten = args.flatten
97
- model = scope.klass
97
+ model = scope.model
98
98
 
99
99
  if args_flatten.blank?
100
100
  error_message = "Couldn't find #{model.name} without an ID"
@@ -256,14 +256,16 @@ module ActiveRecord
256
256
  end
257
257
 
258
258
  def include?(record)
259
- if record.is_a?(reflection.klass)
260
- if record.new_record?
261
- include_in_memory?(record)
262
- else
263
- loaded? ? target.include?(record) : scope.exists?(record.id)
264
- end
259
+ klass = reflection.klass
260
+ return false unless record.is_a?(klass)
261
+
262
+ if loaded?
263
+ target.include?(record)
264
+ elsif record.new_record?
265
+ include_in_memory?(record)
265
266
  else
266
- false
267
+ record_id = klass.composite_primary_key? ? klass.primary_key.zip(record.id).to_h : record.id
268
+ scope.exists?(record_id)
267
269
  end
268
270
  end
269
271
 
@@ -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
@@ -1125,14 +1125,32 @@ module ActiveRecord
1125
1125
  super
1126
1126
  end
1127
1127
 
1128
+ %w(insert insert_all insert! insert_all! upsert upsert_all).each do |method|
1129
+ class_eval <<~RUBY, __FILE__, __LINE__ + 1
1130
+ def #{method}(...)
1131
+ if @association&.target&.any? { |r| r.new_record? }
1132
+ association_name = @association.reflection.name
1133
+ ActiveRecord.deprecator.warn(<<~MSG)
1134
+ Using #{method} on association \#{association_name} with unpersisted records
1135
+ is deprecated and will be removed in Rails 8.2.
1136
+ The unpersisted records will be lost after this operation.
1137
+ Please either persist your records first or store them separately before
1138
+ calling #{method}.
1139
+ MSG
1140
+ scope.#{method}(...)
1141
+ else
1142
+ scope.#{method}(...).tap { reset }
1143
+ end
1144
+ end
1145
+ RUBY
1146
+ end
1147
+
1128
1148
  delegate_methods = [
1129
1149
  QueryMethods,
1130
1150
  SpawnMethods,
1131
1151
  ].flat_map { |klass|
1132
1152
  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
- ]
1153
+ } - self.public_instance_methods(false) - [ :select ] + [ :scoping, :values, :load_async ]
1136
1154
 
1137
1155
  delegate(*delegate_methods, to: :scope)
1138
1156
 
@@ -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
@@ -47,7 +47,7 @@ module ActiveRecord
47
47
  end
48
48
 
49
49
  if scope.order_values.empty? && ordered
50
- split_scope = DisableJoinsAssociationRelation.create(scope.klass, key, join_ids)
50
+ split_scope = DisableJoinsAssociationRelation.create(scope.model, key, join_ids)
51
51
  split_scope.where_clause += scope.where_clause
52
52
  split_scope
53
53
  else
@@ -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
@@ -146,7 +146,7 @@ module ActiveRecord
146
146
 
147
147
  case method
148
148
  when :destroy
149
- if scope.klass.primary_key
149
+ if scope.model.primary_key
150
150
  count = scope.destroy_all.count(&:destroyed?)
151
151
  else
152
152
  scope.each(&:_run_destroy_callbacks)
@@ -222,7 +222,8 @@ module ActiveRecord
222
222
  end
223
223
  end
224
224
 
225
- def find_target
225
+ def find_target(async: false)
226
+ raise NotImplementedError, "No async loading for HasManyThroughAssociation yet" if async
226
227
  return [] unless target_reflection_has_associated_record?
227
228
  return scope.to_a if disable_joins
228
229
  super