activerecord 7.2.3 → 8.0.0.beta1

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.

Potentially problematic release.


This version of activerecord might be problematic. Click here for more details.

Files changed (132) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +192 -1261
  3. data/README.rdoc +2 -2
  4. data/lib/active_record/associations/alias_tracker.rb +4 -6
  5. data/lib/active_record/associations/association.rb +25 -5
  6. data/lib/active_record/associations/belongs_to_association.rb +2 -18
  7. data/lib/active_record/associations/builder/association.rb +7 -6
  8. data/lib/active_record/associations/collection_association.rb +4 -4
  9. data/lib/active_record/associations/disable_joins_association_scope.rb +1 -1
  10. data/lib/active_record/associations/has_many_through_association.rb +4 -9
  11. data/lib/active_record/associations/join_dependency/join_association.rb +27 -25
  12. data/lib/active_record/associations/preloader/association.rb +2 -2
  13. data/lib/active_record/associations/singular_association.rb +8 -3
  14. data/lib/active_record/associations.rb +50 -32
  15. data/lib/active_record/asynchronous_queries_tracker.rb +28 -24
  16. data/lib/active_record/attribute_methods/serialization.rb +1 -1
  17. data/lib/active_record/attribute_methods.rb +19 -24
  18. data/lib/active_record/attributes.rb +26 -37
  19. data/lib/active_record/autosave_association.rb +81 -49
  20. data/lib/active_record/base.rb +2 -2
  21. data/lib/active_record/callbacks.rb +1 -1
  22. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +16 -10
  23. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +0 -1
  24. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +0 -1
  25. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +31 -75
  26. data/lib/active_record/connection_adapters/abstract/database_statements.rb +90 -43
  27. data/lib/active_record/connection_adapters/abstract/query_cache.rb +14 -19
  28. data/lib/active_record/connection_adapters/abstract/quoting.rb +1 -1
  29. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +2 -6
  30. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +27 -9
  31. data/lib/active_record/connection_adapters/abstract/transaction.rb +15 -5
  32. data/lib/active_record/connection_adapters/abstract_adapter.rb +27 -57
  33. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +28 -58
  34. data/lib/active_record/connection_adapters/mysql/quoting.rb +1 -15
  35. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +2 -8
  36. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +43 -45
  37. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +42 -98
  38. data/lib/active_record/connection_adapters/mysql2_adapter.rb +2 -16
  39. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +64 -42
  40. data/lib/active_record/connection_adapters/postgresql/oid/point.rb +10 -0
  41. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +0 -1
  42. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +12 -14
  43. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +1 -1
  44. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +51 -9
  45. data/lib/active_record/connection_adapters/postgresql_adapter.rb +44 -101
  46. data/lib/active_record/connection_adapters/schema_cache.rb +1 -3
  47. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +76 -100
  48. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +0 -13
  49. data/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +0 -6
  50. data/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +13 -0
  51. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +8 -2
  52. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +60 -22
  53. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +37 -67
  54. data/lib/active_record/connection_adapters/trilogy_adapter.rb +1 -18
  55. data/lib/active_record/connection_handling.rb +29 -11
  56. data/lib/active_record/core.rb +15 -60
  57. data/lib/active_record/counter_cache.rb +1 -1
  58. data/lib/active_record/database_configurations/connection_url_resolver.rb +1 -3
  59. data/lib/active_record/delegated_type.rb +18 -18
  60. data/lib/active_record/encryption/config.rb +3 -1
  61. data/lib/active_record/encryption/encryptable_record.rb +5 -5
  62. data/lib/active_record/encryption/encrypted_attribute_type.rb +11 -2
  63. data/lib/active_record/encryption/encryptor.rb +35 -29
  64. data/lib/active_record/encryption/extended_deterministic_queries.rb +4 -2
  65. data/lib/active_record/encryption/scheme.rb +8 -1
  66. data/lib/active_record/enum.rb +12 -13
  67. data/lib/active_record/errors.rb +16 -8
  68. data/lib/active_record/fixture_set/table_row.rb +2 -19
  69. data/lib/active_record/fixtures.rb +0 -1
  70. data/lib/active_record/future_result.rb +14 -10
  71. data/lib/active_record/gem_version.rb +4 -4
  72. data/lib/active_record/insert_all.rb +1 -1
  73. data/lib/active_record/marshalling.rb +1 -4
  74. data/lib/active_record/migration/command_recorder.rb +22 -5
  75. data/lib/active_record/migration/compatibility.rb +5 -2
  76. data/lib/active_record/migration.rb +36 -35
  77. data/lib/active_record/model_schema.rb +1 -1
  78. data/lib/active_record/nested_attributes.rb +4 -6
  79. data/lib/active_record/persistence.rb +128 -130
  80. data/lib/active_record/query_cache.rb +5 -4
  81. data/lib/active_record/query_logs.rb +98 -44
  82. data/lib/active_record/query_logs_formatter.rb +17 -28
  83. data/lib/active_record/querying.rb +10 -10
  84. data/lib/active_record/railtie.rb +5 -6
  85. data/lib/active_record/railties/databases.rake +1 -2
  86. data/lib/active_record/reflection.rb +9 -7
  87. data/lib/active_record/relation/batches/batch_enumerator.rb +4 -3
  88. data/lib/active_record/relation/batches.rb +132 -72
  89. data/lib/active_record/relation/calculations.rb +55 -55
  90. data/lib/active_record/relation/delegation.rb +25 -14
  91. data/lib/active_record/relation/finder_methods.rb +31 -32
  92. data/lib/active_record/relation/merger.rb +8 -8
  93. data/lib/active_record/relation/predicate_builder/association_query_value.rb +0 -2
  94. data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +1 -1
  95. data/lib/active_record/relation/predicate_builder/relation_handler.rb +4 -3
  96. data/lib/active_record/relation/predicate_builder.rb +5 -0
  97. data/lib/active_record/relation/query_attribute.rb +1 -1
  98. data/lib/active_record/relation/query_methods.rb +90 -91
  99. data/lib/active_record/relation/record_fetch_warning.rb +2 -2
  100. data/lib/active_record/relation/spawn_methods.rb +1 -1
  101. data/lib/active_record/relation/where_clause.rb +2 -8
  102. data/lib/active_record/relation.rb +77 -76
  103. data/lib/active_record/result.rb +68 -7
  104. data/lib/active_record/sanitization.rb +7 -6
  105. data/lib/active_record/schema_dumper.rb +16 -29
  106. data/lib/active_record/schema_migration.rb +2 -1
  107. data/lib/active_record/scoping/named.rb +5 -2
  108. data/lib/active_record/secure_token.rb +3 -3
  109. data/lib/active_record/signed_id.rb +6 -7
  110. data/lib/active_record/statement_cache.rb +12 -12
  111. data/lib/active_record/store.rb +7 -3
  112. data/lib/active_record/tasks/database_tasks.rb +24 -15
  113. data/lib/active_record/tasks/mysql_database_tasks.rb +0 -2
  114. data/lib/active_record/tasks/postgresql_database_tasks.rb +0 -7
  115. data/lib/active_record/tasks/sqlite_database_tasks.rb +2 -2
  116. data/lib/active_record/test_fixtures.rb +12 -0
  117. data/lib/active_record/testing/query_assertions.rb +2 -2
  118. data/lib/active_record/token_for.rb +1 -1
  119. data/lib/active_record/transactions.rb +1 -3
  120. data/lib/active_record/validations/uniqueness.rb +8 -8
  121. data/lib/active_record.rb +16 -1
  122. data/lib/arel/collectors/bind.rb +1 -1
  123. data/lib/arel/crud.rb +0 -2
  124. data/lib/arel/delete_manager.rb +0 -5
  125. data/lib/arel/nodes/delete_statement.rb +2 -4
  126. data/lib/arel/nodes/update_statement.rb +2 -4
  127. data/lib/arel/select_manager.rb +2 -6
  128. data/lib/arel/update_manager.rb +0 -5
  129. data/lib/arel/visitors/dot.rb +0 -2
  130. data/lib/arel/visitors/sqlite.rb +0 -25
  131. data/lib/arel/visitors/to_sql.rb +1 -3
  132. metadata +14 -11
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 rubyonrails-core forum here:
217
+ Feature requests should be discussed on the rails-core mailing list here:
218
218
 
219
219
  * https://discuss.rubyonrails.org/c/rubyonrails-core
@@ -26,18 +26,16 @@ module ActiveRecord
26
26
  end
27
27
 
28
28
  def self.initial_count_for(connection, name, table_joins)
29
- quoted_name_escaped = nil
30
- name_escaped = nil
29
+ quoted_name = nil
31
30
 
32
31
  counts = table_joins.map do |join|
33
32
  if join.is_a?(Arel::Nodes::StringJoin)
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)
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)
37
35
 
38
36
  # Table names + table aliases
39
37
  join.left.scan(
40
- /JOIN(?:\s+\w+)?\s+(?:\S+\s+)?(?:#{quoted_name_escaped}|#{name_escaped})\sON/i
38
+ /JOIN(?:\s+\w+)?\s+(?:\S+\s+)?(?:#{quoted_name}|#{name})\sON/i
41
39
  ).size
42
40
  elsif join.is_a?(Arel::Nodes::Join)
43
41
  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
@@ -172,7 +179,7 @@ module ActiveRecord
172
179
  # ActiveRecord::RecordNotFound is rescued within the method, and it is
173
180
  # not reraised. The proxy is \reset and +nil+ is the return value.
174
181
  def load_target
175
- @target = find_target if (@stale_state && stale_target?) || find_target?
182
+ @target = find_target(async: false) if (@stale_state && stale_target?) || find_target?
176
183
 
177
184
  loaded! unless loaded?
178
185
  target
@@ -180,6 +187,13 @@ module ActiveRecord
180
187
  reset
181
188
  end
182
189
 
190
+ def async_load_target # :nodoc:
191
+ @target = find_target(async: true) if (@stale_state && stale_target?) || find_target?
192
+
193
+ loaded! unless loaded?
194
+ nil
195
+ end
196
+
183
197
  # We can't dump @reflection and @through_reflection since it contains the scope proc
184
198
  def marshal_dump
185
199
  ivars = (instance_variables - [:@reflection, :@through_reflection]).map { |name| [name, instance_variable_get(name)] }
@@ -223,13 +237,19 @@ module ActiveRecord
223
237
  klass
224
238
  end
225
239
 
226
- def find_target
240
+ def find_target(async: false)
227
241
  if violates_strict_loading?
228
242
  Base.strict_loading_violation!(owner: owner.class, reflection: reflection)
229
243
  end
230
244
 
231
245
  scope = self.scope
232
- return scope.to_a if skip_statement_cache?(scope)
246
+ if skip_statement_cache?(scope)
247
+ if async
248
+ return scope.load_async.then(&:to_a)
249
+ else
250
+ return scope.to_a
251
+ end
252
+ end
233
253
 
234
254
  sc = reflection.association_scope_cache(klass, owner) do |params|
235
255
  as = AssociationScope.create { params.bind }
@@ -238,7 +258,7 @@ module ActiveRecord
238
258
 
239
259
  binds = AssociationScope.get_bind_values(owner, reflection.chain)
240
260
  klass.with_connection do |c|
241
- sc.execute(binds, c) do |record|
261
+ sc.execute(binds, c, async: async) do |record|
242
262
  set_inverse_instance(record)
243
263
  if owner.strict_loading_n_plus_one_only? && reflection.macro == :has_many
244
264
  record.strict_loading!
@@ -19,16 +19,10 @@ 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
-
28
22
  enqueue_destroy_association(
29
23
  owner_model_name: owner.class.to_s,
30
24
  owner_id: owner.id,
31
- association_class: association_class.to_s,
25
+ association_class: reflection.klass.to_s,
32
26
  association_ids: [id],
33
27
  association_primary_key_column: primary_key_column,
34
28
  ensuring_owner_was_method: options.fetch(:ensuring_owner_was, nil)
@@ -135,9 +129,7 @@ module ActiveRecord
135
129
  target_key_values = record ? Array(primary_key(record.class)).map { |key| record._read_attribute(key) } : []
136
130
 
137
131
  if force || reflection_fk.map { |fk| owner._read_attribute(fk) } != target_key_values
138
- owner_pk = Array(owner.class.primary_key)
139
132
  reflection_fk.each_with_index do |key, index|
140
- next if record.nil? && owner_pk.include?(key)
141
133
  owner[key] = target_key_values[index]
142
134
  end
143
135
  end
@@ -164,15 +156,7 @@ module ActiveRecord
164
156
  end
165
157
 
166
158
  def stale_state
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
159
+ owner._read_attribute(reflection.foreign_key) { |n| owner.send(:missing_attribute, n, caller) }
176
160
  end
177
161
  end
178
162
  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"
@@ -259,10 +259,10 @@ module ActiveRecord
259
259
  klass = reflection.klass
260
260
  return false unless record.is_a?(klass)
261
261
 
262
- if loaded?
263
- target.include?(record)
264
- elsif record.new_record?
262
+ if record.new_record?
265
263
  include_in_memory?(record)
264
+ elsif loaded?
265
+ target.include?(record)
266
266
  else
267
267
  record_id = klass.composite_primary_key? ? klass.primary_key.zip(record.id).to_h : record.id
268
268
  scope.exists?(record_id)
@@ -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
@@ -93,13 +93,7 @@ module ActiveRecord
93
93
  @through_scope = scope
94
94
  record = super
95
95
 
96
- inverse =
97
- if source_reflection.polymorphic?
98
- source_reflection.polymorphic_inverse_of(record.class)
99
- else
100
- source_reflection.inverse_of
101
- end
102
-
96
+ inverse = source_reflection.inverse_of
103
97
  if inverse
104
98
  if inverse.collection?
105
99
  record.send(inverse.name) << build_through_record(record)
@@ -146,7 +140,7 @@ module ActiveRecord
146
140
 
147
141
  case method
148
142
  when :destroy
149
- if scope.klass.primary_key
143
+ if scope.model.primary_key
150
144
  count = scope.destroy_all.count(&:destroyed?)
151
145
  else
152
146
  scope.each(&:_run_destroy_callbacks)
@@ -222,7 +216,8 @@ module ActiveRecord
222
216
  end
223
217
  end
224
218
 
225
- def find_target
219
+ def find_target(async: false)
220
+ raise NotImplementedError, "No async loading for HasManyThroughAssociation yet" if async
226
221
  return [] unless target_reflection_has_associated_record?
227
222
  return scope.to_a if disable_joins
228
223
  super
@@ -38,39 +38,41 @@ module ActiveRecord
38
38
  chain << [reflection, table]
39
39
  end
40
40
 
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
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
45
46
 
46
- scope = reflection.join_scope(table, foreign_table, foreign_klass)
47
+ scope = reflection.join_scope(table, foreign_table, foreign_klass)
47
48
 
48
- unless scope.references_values.empty?
49
- associations = scope.eager_load_values | scope.includes_values
49
+ unless scope.references_values.empty?
50
+ associations = scope.eager_load_values | scope.includes_values
50
51
 
51
- unless associations.empty?
52
- scope.joins! scope.construct_join_dependency(associations, Arel::Nodes::OuterJoin)
52
+ unless associations.empty?
53
+ scope.joins! scope.construct_join_dependency(associations, Arel::Nodes::OuterJoin)
54
+ end
53
55
  end
54
- end
55
56
 
56
- arel = scope.arel(alias_tracker.aliases)
57
- nodes = arel.constraints.first
57
+ arel = scope.arel(alias_tracker.aliases)
58
+ nodes = arel.constraints.first
58
59
 
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 }
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
62
64
  end
63
- end
64
65
 
65
- joins << join_type.new(table, Arel::Nodes::On.new(nodes))
66
+ joins << join_type.new(table, Arel::Nodes::On.new(nodes))
66
67
 
67
- if others && !others.empty?
68
- joins.concat arel.join_sources
69
- append_constraints(joins.last, others)
70
- end
68
+ if others && !others.empty?
69
+ joins.concat arel.join_sources
70
+ append_constraints(connection, joins.last, others)
71
+ end
71
72
 
72
- # The current table in this iteration becomes the foreign table in the next
73
- foreign_table, foreign_klass = table, klass
73
+ # The current table in this iteration becomes the foreign table in the next
74
+ foreign_table, foreign_klass = table, klass
75
+ end
74
76
  end
75
77
 
76
78
  joins
@@ -89,10 +91,10 @@ module ActiveRecord
89
91
  end
90
92
 
91
93
  private
92
- def append_constraints(join, constraints)
94
+ def append_constraints(connection, join, constraints)
93
95
  if join.is_a?(Arel::Nodes::StringJoin)
94
96
  join_string = Arel::Nodes::And.new(constraints.unshift join.left)
95
- join.left = join_string
97
+ join.left = Arel.sql(connection.visitor.compile(join_string))
96
98
  else
97
99
  right = join.right
98
100
  right.expr = Arel::Nodes::And.new(constraints.unshift right.expr)
@@ -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)
@@ -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
  #
@@ -537,7 +559,7 @@ module ActiveRecord
537
559
  # @group.avatars << Avatar.new # this would work if User belonged_to Avatar rather than the other way around
538
560
  # @group.avatars.delete(@group.avatars.last) # so would this
539
561
  #
540
- # === Setting Inverses
562
+ # == Setting Inverses
541
563
  #
542
564
  # If you are using a #belongs_to on the join model, it is a good idea to set the
543
565
  # <tt>:inverse_of</tt> option on the #belongs_to, which will mean that the following example
@@ -1199,11 +1221,8 @@ module ActiveRecord
1199
1221
  # If you are going to modify the association (rather than just read from it), then it is
1200
1222
  # a good idea to set the <tt>:inverse_of</tt> option on the source association on the
1201
1223
  # join model. This allows associated records to be built which will automatically create
1202
- # the appropriate join model records when they are saved. See
1203
- # {Association Join Models}[rdoc-ref:Associations::ClassMethods@Association+Join+Models]
1204
- # and {Setting Inverses}[rdoc-ref:Associations::ClassMethods@Setting+Inverses] for
1205
- # more detail.
1206
- #
1224
+ # the appropriate join model records when they are saved. (See the 'Association Join Models'
1225
+ # and 'Setting Inverses' sections above.)
1207
1226
  # [+:disable_joins+]
1208
1227
  # Specifies whether joins should be skipped for an association. If set to true, two or more queries
1209
1228
  # will be generated. Note that in some cases, if order or limit is applied, it will be done in-memory
@@ -1232,8 +1251,7 @@ module ActiveRecord
1232
1251
  # [+:inverse_of+]
1233
1252
  # Specifies the name of the #belongs_to association on the associated object
1234
1253
  # that is the inverse of this #has_many association.
1235
- # See {Bi-directional associations}[rdoc-ref:Associations::ClassMethods@Bi-directional+associations]
1236
- # for more detail.
1254
+ # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail.
1237
1255
  # [+:extend+]
1238
1256
  # Specifies a module or array of modules that will be extended into the association object returned.
1239
1257
  # Useful for defining methods on associations, especially when they should be shared between multiple
@@ -1255,6 +1273,14 @@ module ActiveRecord
1255
1273
  # persisted new records placed at the end.
1256
1274
  # When set to +:nested_attributes_order+, the index is based on the record order received by
1257
1275
  # nested attributes setter, when accepts_nested_attributes_for is used.
1276
+ # [:before_add]
1277
+ # Defines an {association callback}[rdoc-ref:Associations::ClassMethods@Association+callbacks] that gets triggered <b>before an object is added</b> to the association collection.
1278
+ # [:after_add]
1279
+ # Defines an {association callback}[rdoc-ref:Associations::ClassMethods@Association+callbacks] that gets triggered <b>after an object is added</b> to the association collection.
1280
+ # [:before_remove]
1281
+ # Defines an {association callback}[rdoc-ref:Associations::ClassMethods@Association+callbacks] that gets triggered <b>before an object is removed</b> from the association collection.
1282
+ # [:after_remove]
1283
+ # 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
1284
  #
1259
1285
  # Option examples:
1260
1286
  # has_many :comments, -> { order("posted_on") }
@@ -1274,12 +1300,10 @@ module ActiveRecord
1274
1300
  Reflection.add_reflection self, name, reflection
1275
1301
  end
1276
1302
 
1277
- # Specifies a one-to-one association with another class. This method
1278
- # should only be used if the other class contains the foreign key. If
1279
- # the current class contains the foreign key, then you should use
1280
- # #belongs_to instead. See {Is it a belongs_to or has_one
1281
- # association?}[rdoc-ref:Associations::ClassMethods@Is+it+a+-23belongs_to+or+-23has_one+association-3F]
1282
- # for more detail on when to use #has_one and when to use #belongs_to.
1303
+ # Specifies a one-to-one association with another class. This method should only be used
1304
+ # if the other class contains the foreign key. If the current class contains the foreign key,
1305
+ # then you should use #belongs_to instead. See also ActiveRecord::Associations::ClassMethods's overview
1306
+ # on when to use #has_one and when to use #belongs_to.
1283
1307
  #
1284
1308
  # The following methods for retrieval and query of a single associated object will be added:
1285
1309
  #
@@ -1394,10 +1418,8 @@ module ActiveRecord
1394
1418
  # If you are going to modify the association (rather than just read from it), then it is
1395
1419
  # a good idea to set the <tt>:inverse_of</tt> option on the source association on the
1396
1420
  # join model. This allows associated records to be built which will automatically create
1397
- # the appropriate join model records when they are saved. See
1398
- # {Association Join Models}[rdoc-ref:Associations::ClassMethods@Association+Join+Models]
1399
- # and {Setting Inverses}[rdoc-ref:Associations::ClassMethods@Setting+Inverses] for
1400
- # more detail.
1421
+ # the appropriate join model records when they are saved. (See the 'Association Join Models'
1422
+ # and 'Setting Inverses' sections above.)
1401
1423
  # [+:disable_joins+]
1402
1424
  # Specifies whether joins should be skipped for an association. If set to true, two or more queries
1403
1425
  # will be generated. Note that in some cases, if order or limit is applied, it will be done in-memory
@@ -1435,8 +1457,7 @@ module ActiveRecord
1435
1457
  # [+:inverse_of+]
1436
1458
  # Specifies the name of the #belongs_to association on the associated object
1437
1459
  # that is the inverse of this #has_one association.
1438
- # See {Bi-directional associations}[rdoc-ref:Associations::ClassMethods@Bi-directional+associations]
1439
- # for more detail.
1460
+ # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail.
1440
1461
  # [+:required+]
1441
1462
  # When set to +true+, the association will also have its presence validated.
1442
1463
  # This will validate the association itself, not the id. You can use
@@ -1470,12 +1491,10 @@ module ActiveRecord
1470
1491
  Reflection.add_reflection self, name, reflection
1471
1492
  end
1472
1493
 
1473
- # Specifies a one-to-one association with another class. This method
1474
- # should only be used if this class contains the foreign key. If the
1475
- # other class contains the foreign key, then you should use #has_one
1476
- # instead. See {Is it a belongs_to or has_one
1477
- # association?}[rdoc-ref:Associations::ClassMethods@Is+it+a+-23belongs_to+or+-23has_one+association-3F]
1478
- # for more detail on when to use #has_one and when to use #belongs_to.
1494
+ # Specifies a one-to-one association with another class. This method should only be used
1495
+ # if this class contains the foreign key. If the other class contains the foreign key,
1496
+ # then you should use #has_one instead. See also ActiveRecord::Associations::ClassMethods's overview
1497
+ # on when to use #has_one and when to use #belongs_to.
1479
1498
  #
1480
1499
  # Methods will be added for retrieval and query for a single associated object, for which
1481
1500
  # this object holds an id:
@@ -1617,8 +1636,7 @@ module ActiveRecord
1617
1636
  # [+:inverse_of+]
1618
1637
  # Specifies the name of the #has_one or #has_many association on the associated
1619
1638
  # object that is the inverse of this #belongs_to association.
1620
- # See {Bi-directional associations}[rdoc-ref:Associations::ClassMethods@Bi-directional+associations]
1621
- # for more detail.
1639
+ # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail.
1622
1640
  # [+:optional+]
1623
1641
  # When set to +true+, the association will not have its presence validated.
1624
1642
  # [+:required+]
@@ -1678,7 +1696,7 @@ module ActiveRecord
1678
1696
  # The join table should not have a primary key or a model associated with it. You must manually generate the
1679
1697
  # join table with a migration such as this:
1680
1698
  #
1681
- # class CreateDevelopersProjectsJoinTable < ActiveRecord::Migration[7.2]
1699
+ # class CreateDevelopersProjectsJoinTable < ActiveRecord::Migration[8.0]
1682
1700
  # def change
1683
1701
  # create_join_table :developers, :projects
1684
1702
  # 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
@@ -130,7 +130,7 @@ module ActiveRecord
130
130
  # silently cast unsupported types to +String+:
131
131
  #
132
132
  # >> JSON.parse(JSON.dump(Struct.new(:foo)))
133
- # # => "#<Class:0x000000013090b4c0>"
133
+ # => "#<Class:0x000000013090b4c0>"
134
134
  #
135
135
  # ==== Examples
136
136
  #