activerecord 7.2.2.1 → 8.0.5

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 (156) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +523 -677
  3. data/README.rdoc +2 -2
  4. data/lib/active_record/association_relation.rb +1 -0
  5. data/lib/active_record/associations/alias_tracker.rb +6 -4
  6. data/lib/active_record/associations/association.rb +34 -10
  7. data/lib/active_record/associations/belongs_to_association.rb +18 -2
  8. data/lib/active_record/associations/builder/association.rb +7 -6
  9. data/lib/active_record/associations/collection_association.rb +10 -8
  10. data/lib/active_record/associations/disable_joins_association_scope.rb +1 -1
  11. data/lib/active_record/associations/has_many_through_association.rb +3 -2
  12. data/lib/active_record/associations/join_dependency/join_association.rb +25 -27
  13. data/lib/active_record/associations/join_dependency.rb +2 -2
  14. data/lib/active_record/associations/preloader/association.rb +2 -2
  15. data/lib/active_record/associations/preloader/batch.rb +7 -1
  16. data/lib/active_record/associations/singular_association.rb +8 -3
  17. data/lib/active_record/associations.rb +34 -4
  18. data/lib/active_record/asynchronous_queries_tracker.rb +28 -24
  19. data/lib/active_record/attribute_methods/primary_key.rb +4 -8
  20. data/lib/active_record/attribute_methods/query.rb +34 -0
  21. data/lib/active_record/attribute_methods/serialization.rb +1 -1
  22. data/lib/active_record/attribute_methods/time_zone_conversion.rb +2 -12
  23. data/lib/active_record/attribute_methods.rb +24 -19
  24. data/lib/active_record/attributes.rb +37 -26
  25. data/lib/active_record/autosave_association.rb +91 -39
  26. data/lib/active_record/base.rb +2 -2
  27. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +34 -25
  28. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +0 -1
  29. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +0 -1
  30. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +55 -47
  31. data/lib/active_record/connection_adapters/abstract/database_statements.rb +91 -44
  32. data/lib/active_record/connection_adapters/abstract/query_cache.rb +25 -8
  33. data/lib/active_record/connection_adapters/abstract/quoting.rb +1 -1
  34. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +4 -5
  35. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +12 -3
  36. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +40 -10
  37. data/lib/active_record/connection_adapters/abstract/transaction.rb +15 -5
  38. data/lib/active_record/connection_adapters/abstract_adapter.rb +92 -70
  39. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +35 -46
  40. data/lib/active_record/connection_adapters/mysql/quoting.rb +7 -9
  41. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +2 -8
  42. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +50 -45
  43. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +84 -94
  44. data/lib/active_record/connection_adapters/mysql2_adapter.rb +9 -9
  45. data/lib/active_record/connection_adapters/pool_config.rb +7 -7
  46. data/lib/active_record/connection_adapters/postgresql/column.rb +4 -2
  47. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +72 -43
  48. data/lib/active_record/connection_adapters/postgresql/oid/array.rb +1 -1
  49. data/lib/active_record/connection_adapters/postgresql/oid/point.rb +10 -0
  50. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +2 -4
  51. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +1 -11
  52. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +20 -24
  53. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +3 -2
  54. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +60 -17
  55. data/lib/active_record/connection_adapters/postgresql_adapter.rb +61 -99
  56. data/lib/active_record/connection_adapters/schema_cache.rb +1 -3
  57. data/lib/active_record/connection_adapters/sqlite3/column.rb +8 -2
  58. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +80 -100
  59. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +13 -0
  60. data/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +0 -6
  61. data/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +27 -2
  62. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +10 -1
  63. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +63 -17
  64. data/lib/active_record/connection_adapters/statement_pool.rb +4 -2
  65. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +37 -67
  66. data/lib/active_record/connection_adapters/trilogy_adapter.rb +1 -18
  67. data/lib/active_record/connection_adapters.rb +0 -56
  68. data/lib/active_record/connection_handling.rb +35 -9
  69. data/lib/active_record/core.rb +57 -22
  70. data/lib/active_record/counter_cache.rb +1 -1
  71. data/lib/active_record/database_configurations/connection_url_resolver.rb +3 -1
  72. data/lib/active_record/database_configurations/database_config.rb +4 -0
  73. data/lib/active_record/database_configurations/hash_config.rb +16 -2
  74. data/lib/active_record/delegated_type.rb +18 -18
  75. data/lib/active_record/encryption/config.rb +3 -1
  76. data/lib/active_record/encryption/encryptable_record.rb +5 -5
  77. data/lib/active_record/encryption/encrypted_attribute_type.rb +11 -2
  78. data/lib/active_record/encryption/encryptor.rb +37 -28
  79. data/lib/active_record/encryption/extended_deterministic_queries.rb +4 -2
  80. data/lib/active_record/encryption/scheme.rb +8 -1
  81. data/lib/active_record/enum.rb +22 -34
  82. data/lib/active_record/errors.rb +16 -8
  83. data/lib/active_record/fixture_set/table_row.rb +19 -2
  84. data/lib/active_record/fixtures.rb +0 -2
  85. data/lib/active_record/future_result.rb +15 -9
  86. data/lib/active_record/gem_version.rb +4 -4
  87. data/lib/active_record/insert_all.rb +3 -3
  88. data/lib/active_record/locking/optimistic.rb +1 -1
  89. data/lib/active_record/log_subscriber.rb +5 -11
  90. data/lib/active_record/migration/command_recorder.rb +32 -12
  91. data/lib/active_record/migration/compatibility.rb +5 -2
  92. data/lib/active_record/migration.rb +40 -43
  93. data/lib/active_record/model_schema.rb +3 -4
  94. data/lib/active_record/nested_attributes.rb +4 -6
  95. data/lib/active_record/persistence.rb +128 -130
  96. data/lib/active_record/query_logs.rb +106 -50
  97. data/lib/active_record/query_logs_formatter.rb +17 -28
  98. data/lib/active_record/querying.rb +12 -12
  99. data/lib/active_record/railtie.rb +4 -28
  100. data/lib/active_record/railties/databases.rake +11 -34
  101. data/lib/active_record/reflection.rb +18 -21
  102. data/lib/active_record/relation/batches/batch_enumerator.rb +4 -3
  103. data/lib/active_record/relation/batches.rb +132 -72
  104. data/lib/active_record/relation/calculations.rb +71 -65
  105. data/lib/active_record/relation/delegation.rb +25 -14
  106. data/lib/active_record/relation/finder_methods.rb +32 -31
  107. data/lib/active_record/relation/merger.rb +8 -8
  108. data/lib/active_record/relation/predicate_builder/array_handler.rb +3 -1
  109. data/lib/active_record/relation/predicate_builder/association_query_value.rb +2 -0
  110. data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +1 -1
  111. data/lib/active_record/relation/predicate_builder/relation_handler.rb +4 -3
  112. data/lib/active_record/relation/predicate_builder.rb +13 -0
  113. data/lib/active_record/relation/query_attribute.rb +1 -1
  114. data/lib/active_record/relation/query_methods.rb +121 -70
  115. data/lib/active_record/relation/spawn_methods.rb +7 -7
  116. data/lib/active_record/relation/where_clause.rb +9 -3
  117. data/lib/active_record/relation.rb +95 -67
  118. data/lib/active_record/result.rb +66 -4
  119. data/lib/active_record/sanitization.rb +7 -6
  120. data/lib/active_record/schema_dumper.rb +34 -11
  121. data/lib/active_record/schema_migration.rb +2 -1
  122. data/lib/active_record/scoping/named.rb +5 -2
  123. data/lib/active_record/secure_token.rb +3 -3
  124. data/lib/active_record/signed_id.rb +7 -6
  125. data/lib/active_record/statement_cache.rb +14 -14
  126. data/lib/active_record/store.rb +7 -3
  127. data/lib/active_record/table_metadata.rb +1 -3
  128. data/lib/active_record/tasks/database_tasks.rb +69 -60
  129. data/lib/active_record/tasks/mysql_database_tasks.rb +0 -2
  130. data/lib/active_record/tasks/postgresql_database_tasks.rb +9 -1
  131. data/lib/active_record/tasks/sqlite_database_tasks.rb +2 -2
  132. data/lib/active_record/test_databases.rb +1 -1
  133. data/lib/active_record/test_fixtures.rb +12 -0
  134. data/lib/active_record/token_for.rb +1 -1
  135. data/lib/active_record/transactions.rb +8 -7
  136. data/lib/active_record/type/serialized.rb +5 -0
  137. data/lib/active_record/validations/uniqueness.rb +8 -8
  138. data/lib/active_record.rb +22 -49
  139. data/lib/arel/collectors/bind.rb +2 -2
  140. data/lib/arel/collectors/sql_string.rb +1 -1
  141. data/lib/arel/collectors/substitute_binds.rb +2 -2
  142. data/lib/arel/crud.rb +2 -0
  143. data/lib/arel/delete_manager.rb +5 -0
  144. data/lib/arel/nodes/binary.rb +1 -1
  145. data/lib/arel/nodes/delete_statement.rb +4 -2
  146. data/lib/arel/nodes/node.rb +1 -1
  147. data/lib/arel/nodes/sql_literal.rb +1 -1
  148. data/lib/arel/nodes/update_statement.rb +4 -2
  149. data/lib/arel/predications.rb +1 -3
  150. data/lib/arel/select_manager.rb +6 -2
  151. data/lib/arel/table.rb +3 -7
  152. data/lib/arel/update_manager.rb +5 -0
  153. data/lib/arel/visitors/dot.rb +2 -0
  154. data/lib/arel/visitors/to_sql.rb +3 -1
  155. metadata +11 -15
  156. 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.0]
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
@@ -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)] }
@@ -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
@@ -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
 
@@ -131,7 +132,7 @@ module ActiveRecord::Associations::Builder # :nodoc:
131
132
  err_message = "A valid destroy_association_async_job is required to use `dependent: :destroy_async` on associations"
132
133
  raise ActiveRecord::ConfigurationError, err_message
133
134
  end
134
- unless valid_dependent_options.include? dependent
135
+ unless valid_dependent_options.include?(dependent)
135
136
  raise ArgumentError, "The :dependent option must be one of #{valid_dependent_options}, but is :#{dependent}"
136
137
  end
137
138
  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
 
@@ -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
@@ -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
@@ -38,41 +38,39 @@ module ActiveRecord
38
38
  chain << [reflection, table]
39
39
  end
40
40
 
41
- base_klass.with_connection do |connection|
42
- # The chain starts with the target table, but we want to end with it here (makes
43
- # more sense in this context), so we reverse
44
- chain.reverse_each do |reflection, table|
45
- klass = reflection.klass
41
+ # The chain starts with the target table, but we want to end with it here (makes
42
+ # more sense in this context), so we reverse
43
+ chain.reverse_each do |reflection, table|
44
+ klass = reflection.klass
46
45
 
47
- scope = reflection.join_scope(table, foreign_table, foreign_klass)
46
+ scope = reflection.join_scope(table, foreign_table, foreign_klass)
48
47
 
49
- unless scope.references_values.empty?
50
- associations = scope.eager_load_values | scope.includes_values
48
+ unless scope.references_values.empty?
49
+ associations = scope.eager_load_values | scope.includes_values
51
50
 
52
- unless associations.empty?
53
- scope.joins! scope.construct_join_dependency(associations, Arel::Nodes::OuterJoin)
54
- end
51
+ unless associations.empty?
52
+ scope.joins! scope.construct_join_dependency(associations, Arel::Nodes::OuterJoin)
55
53
  end
54
+ end
56
55
 
57
- arel = scope.arel(alias_tracker.aliases)
58
- nodes = arel.constraints.first
56
+ arel = scope.arel(alias_tracker.aliases)
57
+ nodes = arel.constraints.first
59
58
 
60
- if nodes.is_a?(Arel::Nodes::And)
61
- others = nodes.children.extract! do |node|
62
- !Arel.fetch_attribute(node) { |attr| attr.relation.name == table.name }
63
- end
59
+ if nodes.is_a?(Arel::Nodes::And)
60
+ others = nodes.children.extract! do |node|
61
+ !Arel.fetch_attribute(node) { |attr| attr.relation.name == table.name }
64
62
  end
63
+ end
65
64
 
66
- joins << join_type.new(table, Arel::Nodes::On.new(nodes))
65
+ joins << join_type.new(table, Arel::Nodes::On.new(nodes))
67
66
 
68
- if others && !others.empty?
69
- joins.concat arel.join_sources
70
- append_constraints(connection, joins.last, others)
71
- end
72
-
73
- # The current table in this iteration becomes the foreign table in the next
74
- foreign_table, foreign_klass = table, klass
67
+ if others && !others.empty?
68
+ joins.concat arel.join_sources
69
+ append_constraints(joins.last, others)
75
70
  end
71
+
72
+ # The current table in this iteration becomes the foreign table in the next
73
+ foreign_table, foreign_klass = table, klass
76
74
  end
77
75
 
78
76
  joins
@@ -91,10 +89,10 @@ module ActiveRecord
91
89
  end
92
90
 
93
91
  private
94
- def append_constraints(connection, join, constraints)
92
+ def append_constraints(join, constraints)
95
93
  if join.is_a?(Arel::Nodes::StringJoin)
96
94
  join_string = Arel::Nodes::And.new(constraints.unshift join.left)
97
- join.left = Arel.sql(connection.visitor.compile(join_string))
95
+ join.left = join_string
98
96
  else
99
97
  right = join.right
100
98
  right.expr = Arel::Nodes::And.new(constraints.unshift right.expr)
@@ -103,7 +103,7 @@ module ActiveRecord
103
103
  end
104
104
 
105
105
  def instantiate(result_set, strict_loading_value, &block)
106
- primary_key = aliases.column_alias(join_root, join_root.primary_key)
106
+ primary_key = Array(join_root.primary_key).map { |column| aliases.column_alias(join_root, column) }
107
107
 
108
108
  seen = Hash.new { |i, parent|
109
109
  i[parent] = Hash.new { |j, child_class|
@@ -141,7 +141,7 @@ module ActiveRecord
141
141
 
142
142
  message_bus.instrument("instantiation.active_record", payload) do
143
143
  result_set.each { |row_hash|
144
- parent_key = primary_key ? row_hash[primary_key] : row_hash
144
+ parent_key = primary_key.empty? ? row_hash : row_hash.values_at(*primary_key)
145
145
  parent = parents[parent_key] ||= join_root.instantiate(row_hash, column_aliases, column_types, &block)
146
146
  construct(parent, join_root, row_hash, seen, model_cache, strict_loading_value)
147
147
  }
@@ -17,12 +17,12 @@ module ActiveRecord
17
17
  def eql?(other)
18
18
  association_key_name == other.association_key_name &&
19
19
  scope.table_name == other.scope.table_name &&
20
- scope.connection_specification_name == other.scope.connection_specification_name &&
20
+ scope.model.connection_specification_name == other.scope.model.connection_specification_name &&
21
21
  scope.values_for_queries == other.scope.values_for_queries
22
22
  end
23
23
 
24
24
  def hash
25
- [association_key_name, scope.table_name, scope.connection_specification_name, scope.values_for_queries].hash
25
+ [association_key_name, scope.model.table_name, scope.model.connection_specification_name, scope.values_for_queries].hash
26
26
  end
27
27
 
28
28
  def records_for(loaders)
@@ -38,7 +38,13 @@ module ActiveRecord
38
38
  attr_reader :loaders
39
39
 
40
40
  def group_and_load_similar(loaders)
41
- loaders.grep_v(ThroughAssociation).group_by(&:loader_query).each_pair do |query, similar_loaders|
41
+ non_through = loaders.grep_v(ThroughAssociation)
42
+
43
+ grouped = non_through.group_by do |loader|
44
+ [loader.loader_query, loader.klass]
45
+ end
46
+
47
+ grouped.each do |(query, _klass), similar_loaders|
42
48
  query.load_records_in_batch(similar_loaders)
43
49
  end
44
50
  end
@@ -18,6 +18,7 @@ module ActiveRecord
18
18
  def reset
19
19
  super
20
20
  @target = nil
21
+ @future_target = nil
21
22
  end
22
23
 
23
24
  # Implements the writer method, e.g. foo.bar= for Foo.belongs_to :bar
@@ -43,11 +44,15 @@ module ActiveRecord
43
44
  super.except!(*Array(klass.primary_key))
44
45
  end
45
46
 
46
- def find_target
47
+ def find_target(async: false)
47
48
  if disable_joins
48
- scope.first
49
+ if async
50
+ scope.load_async.then(&:first)
51
+ else
52
+ scope.first
53
+ end
49
54
  else
50
- super.first
55
+ super.then(&:first)
51
56
  end
52
57
  end
53
58
 
@@ -379,21 +379,43 @@ module ActiveRecord
379
379
  # after_add: :congratulate_client,
380
380
  # after_remove: :log_after_remove
381
381
  #
382
- # def congratulate_client(record)
382
+ # def congratulate_client(client)
383
383
  # # ...
384
384
  # end
385
385
  #
386
- # def log_after_remove(record)
386
+ # def log_after_remove(client)
387
387
  # # ...
388
388
  # end
389
389
  # end
390
390
  #
391
+ # Callbacks can be defined in three ways:
392
+ #
393
+ # 1. A symbol that references a method defined on the class with the
394
+ # associated collection. For example, <tt>after_add: :congratulate_client</tt>
395
+ # invokes <tt>Firm#congratulate_client(client)</tt>.
396
+ # 2. A callable with a signature that accepts both the record with the
397
+ # associated collection and the record being added or removed. For
398
+ # example, <tt>after_add: ->(firm, client) { ... }</tt>.
399
+ # 3. An object that responds to the callback name. For example, passing
400
+ # <tt>after_add: CallbackObject.new</tt> invokes <tt>CallbackObject#after_add(firm,
401
+ # client)</tt>.
402
+ #
391
403
  # It's possible to stack callbacks by passing them as an array. Example:
392
404
  #
405
+ # class CallbackObject
406
+ # def after_add(firm, client)
407
+ # firm.log << "after_adding #{client.id}"
408
+ # end
409
+ # end
410
+ #
393
411
  # class Firm < ActiveRecord::Base
394
412
  # has_many :clients,
395
413
  # dependent: :destroy,
396
- # after_add: [:congratulate_client, -> (firm, record) { firm.log << "after_adding#{record.id}" }],
414
+ # after_add: [
415
+ # :congratulate_client,
416
+ # -> (firm, client) { firm.log << "after_adding #{client.id}" },
417
+ # CallbackObject.new
418
+ # ],
397
419
  # after_remove: :log_after_remove
398
420
  # end
399
421
  #
@@ -1255,6 +1277,14 @@ module ActiveRecord
1255
1277
  # persisted new records placed at the end.
1256
1278
  # When set to +:nested_attributes_order+, the index is based on the record order received by
1257
1279
  # nested attributes setter, when accepts_nested_attributes_for is used.
1280
+ # [:before_add]
1281
+ # Defines an {association callback}[rdoc-ref:Associations::ClassMethods@Association+callbacks] that gets triggered <b>before an object is added</b> to the association collection.
1282
+ # [:after_add]
1283
+ # Defines an {association callback}[rdoc-ref:Associations::ClassMethods@Association+callbacks] that gets triggered <b>after an object is added</b> to the association collection.
1284
+ # [:before_remove]
1285
+ # Defines an {association callback}[rdoc-ref:Associations::ClassMethods@Association+callbacks] that gets triggered <b>before an object is removed</b> from the association collection.
1286
+ # [:after_remove]
1287
+ # Defines an {association callback}[rdoc-ref:Associations::ClassMethods@Association+callbacks] that gets triggered <b>after an object is removed</b> from the association collection.
1258
1288
  #
1259
1289
  # Option examples:
1260
1290
  # has_many :comments, -> { order("posted_on") }
@@ -1678,7 +1708,7 @@ module ActiveRecord
1678
1708
  # The join table should not have a primary key or a model associated with it. You must manually generate the
1679
1709
  # join table with a migration such as this:
1680
1710
  #
1681
- # class CreateDevelopersProjectsJoinTable < ActiveRecord::Migration[7.2]
1711
+ # class CreateDevelopersProjectsJoinTable < ActiveRecord::Migration[8.0]
1682
1712
  # def change
1683
1713
  # create_join_table :developers, :projects
1684
1714
  # end
@@ -1,29 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "concurrent/atomic/atomic_boolean"
4
+ require "concurrent/atomic/read_write_lock"
5
+
3
6
  module ActiveRecord
4
7
  class AsynchronousQueriesTracker # :nodoc:
5
- module NullSession # :nodoc:
6
- class << self
7
- def active?
8
- true
9
- end
10
-
11
- def finalize
12
- end
13
- end
14
- end
15
-
16
8
  class Session # :nodoc:
17
9
  def initialize
18
- @active = true
10
+ @active = Concurrent::AtomicBoolean.new(true)
11
+ @lock = Concurrent::ReadWriteLock.new
19
12
  end
20
13
 
21
14
  def active?
22
- @active
15
+ @active.true?
23
16
  end
24
17
 
25
- def finalize
26
- @active = false
18
+ def synchronize(&block)
19
+ @lock.with_read_lock(&block)
20
+ end
21
+
22
+ def finalize(wait = false)
23
+ @active.make_false
24
+ if wait
25
+ # Wait until all thread with a read lock are done
26
+ @lock.with_write_lock { }
27
+ end
27
28
  end
28
29
  end
29
30
 
@@ -33,7 +34,7 @@ module ActiveRecord
33
34
  end
34
35
 
35
36
  def run
36
- ActiveRecord::Base.asynchronous_queries_tracker.start_session
37
+ ActiveRecord::Base.asynchronous_queries_tracker.tap(&:start_session)
37
38
  end
38
39
 
39
40
  def complete(asynchronous_queries_tracker)
@@ -41,20 +42,23 @@ module ActiveRecord
41
42
  end
42
43
  end
43
44
 
44
- attr_reader :current_session
45
-
46
45
  def initialize
47
- @current_session = NullSession
46
+ @stack = []
47
+ end
48
+
49
+ def current_session
50
+ @stack.last or raise ActiveRecordError, "Can't perform asynchronous queries without a query session"
48
51
  end
49
52
 
50
53
  def start_session
51
- @current_session = Session.new
52
- self
54
+ session = Session.new
55
+ @stack << session
53
56
  end
54
57
 
55
- def finalize_session
56
- @current_session.finalize
57
- @current_session = NullSession
58
+ def finalize_session(wait = false)
59
+ session = @stack.pop
60
+ session&.finalize(wait)
61
+ self
58
62
  end
59
63
  end
60
64
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "set"
4
-
5
3
  module ActiveRecord
6
4
  module AttributeMethods
7
5
  # = Active Record Attribute Methods Primary Key
@@ -89,10 +87,9 @@ module ActiveRecord
89
87
  @composite_primary_key
90
88
  end
91
89
 
92
- # Returns a quoted version of the primary key name, used to construct
93
- # SQL statements.
90
+ # Returns a quoted version of the primary key name.
94
91
  def quoted_primary_key
95
- @quoted_primary_key ||= adapter_class.quote_column_name(primary_key)
92
+ adapter_class.quote_column_name(primary_key)
96
93
  end
97
94
 
98
95
  def reset_primary_key # :nodoc:
@@ -132,13 +129,13 @@ module ActiveRecord
132
129
  # Project.primary_key # => "foo_id"
133
130
  def primary_key=(value)
134
131
  @primary_key = if value.is_a?(Array)
135
- @composite_primary_key = true
136
132
  include CompositePrimaryKey
137
133
  @primary_key = value.map { |v| -v.to_s }.freeze
138
134
  elsif value
139
135
  -value.to_s
140
136
  end
141
- @quoted_primary_key = nil
137
+
138
+ @composite_primary_key = value.is_a?(Array)
142
139
  @attributes_builder = nil
143
140
  end
144
141
 
@@ -148,7 +145,6 @@ module ActiveRecord
148
145
  base.class_eval do
149
146
  @primary_key = PRIMARY_KEY_NOT_SET
150
147
  @composite_primary_key = false
151
- @quoted_primary_key = nil
152
148
  @attributes_builder = nil
153
149
  end
154
150
  end