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
@@ -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
  }
@@ -235,6 +235,8 @@ module ActiveRecord
235
235
  raise EagerLoadPolymorphicError.new(reflection)
236
236
  end
237
237
 
238
+ Deprecation.guard(reflection) { "referenced in query to join its table" }
239
+
238
240
  JoinAssociation.new(reflection, build(right, reflection.klass))
239
241
  end
240
242
  end
@@ -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
@@ -118,6 +118,7 @@ module ActiveRecord
118
118
  def loaders
119
119
  @loaders ||=
120
120
  grouped_records.flat_map do |reflection, reflection_records|
121
+ Deprecation.guard(reflection) { "referenced in query to preload records" }
121
122
  preloaders_for_reflection(reflection, reflection_records)
122
123
  end
123
124
  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
 
@@ -39,6 +39,8 @@ module ActiveRecord
39
39
  autoload :AssociationScope
40
40
  autoload :DisableJoinsAssociationScope
41
41
  autoload :AliasTracker
42
+
43
+ autoload :Deprecation
42
44
  end
43
45
 
44
46
  def self.eager_load!
@@ -79,7 +81,7 @@ module ActiveRecord
79
81
 
80
82
  # Returns the specified association instance if it exists, +nil+ otherwise.
81
83
  def association_instance_get(name)
82
- @association_cache[name]
84
+ (@association_cache ||= {})[name]
83
85
  end
84
86
 
85
87
  # Set the specified association instance.
@@ -87,6 +89,14 @@ module ActiveRecord
87
89
  @association_cache[name] = association
88
90
  end
89
91
 
92
+ def deprecated_associations_api_guard(association, method_name)
93
+ Deprecation.guard(association.reflection) { "the method #{method_name} was invoked" }
94
+ end
95
+
96
+ def report_deprecated_association(reflection, context:)
97
+ Deprecation.report(reflection, context: context)
98
+ end
99
+
90
100
  # = Active Record \Associations
91
101
  #
92
102
  # \Associations are a set of macro-like class methods for tying objects together through
@@ -379,21 +389,43 @@ module ActiveRecord
379
389
  # after_add: :congratulate_client,
380
390
  # after_remove: :log_after_remove
381
391
  #
382
- # def congratulate_client(record)
392
+ # def congratulate_client(client)
383
393
  # # ...
384
394
  # end
385
395
  #
386
- # def log_after_remove(record)
396
+ # def log_after_remove(client)
387
397
  # # ...
388
398
  # end
389
399
  # end
390
400
  #
401
+ # Callbacks can be defined in three ways:
402
+ #
403
+ # 1. A symbol that references a method defined on the class with the
404
+ # associated collection. For example, <tt>after_add: :congratulate_client</tt>
405
+ # invokes <tt>Firm#congratulate_client(client)</tt>.
406
+ # 2. A callable with a signature that accepts both the record with the
407
+ # associated collection and the record being added or removed. For
408
+ # example, <tt>after_add: ->(firm, client) { ... }</tt>.
409
+ # 3. An object that responds to the callback name. For example, passing
410
+ # <tt>after_add: CallbackObject.new</tt> invokes <tt>CallbackObject#after_add(firm,
411
+ # client)</tt>.
412
+ #
391
413
  # It's possible to stack callbacks by passing them as an array. Example:
392
414
  #
415
+ # class CallbackObject
416
+ # def after_add(firm, client)
417
+ # firm.log << "after_adding #{client.id}"
418
+ # end
419
+ # end
420
+ #
393
421
  # class Firm < ActiveRecord::Base
394
422
  # has_many :clients,
395
423
  # dependent: :destroy,
396
- # after_add: [:congratulate_client, -> (firm, record) { firm.log << "after_adding#{record.id}" }],
424
+ # after_add: [
425
+ # :congratulate_client,
426
+ # -> (firm, client) { firm.log << "after_adding #{client.id}" },
427
+ # CallbackObject.new
428
+ # ],
397
429
  # after_remove: :log_after_remove
398
430
  # end
399
431
  #
@@ -998,6 +1030,116 @@ module ActiveRecord
998
1030
  # associated records themselves, you can always do something along the lines of
999
1031
  # <tt>person.tasks.each(&:destroy)</tt>.
1000
1032
  #
1033
+ # == Deprecated \Associations
1034
+ #
1035
+ # \Associations can be marked as deprecated by passing <tt>deprecated: true</tt>:
1036
+ #
1037
+ # has_many :posts, deprecated: true
1038
+ #
1039
+ # When a deprecated association is used, a warning is issued using the
1040
+ # Active Record logger, though more options are available via
1041
+ # configuration.
1042
+ #
1043
+ # The message includes some context that helps understand the deprecated
1044
+ # usage:
1045
+ #
1046
+ # The association Author#posts is deprecated, the method post_ids was invoked (...)
1047
+ # The association Author#posts is deprecated, referenced in query to preload records (...)
1048
+ #
1049
+ # The dots in the examples above would have the application-level spot
1050
+ # where usage occurred, to help locate what triggered the warning. That
1051
+ # location is computed using the Active Record backtrace cleaner.
1052
+ #
1053
+ # === What is considered to be usage?
1054
+ #
1055
+ # * Invocation of any association methods like +posts+, <tt>posts=</tt>,
1056
+ # etc.
1057
+ #
1058
+ # * If the association accepts nested attributes, assignment to those
1059
+ # attributes.
1060
+ #
1061
+ # * If the association is a through association and some of its nested
1062
+ # associations are deprecated, you'll get warnings for them whenever the
1063
+ # top-level through is used. This is so regardless of whether the
1064
+ # through itself is deprecated.
1065
+ #
1066
+ # * Execution of queries that refer to the association. Think execution of
1067
+ # <tt>eager_load(:posts)</tt>, <tt>joins(author: :posts)</tt>, etc.
1068
+ #
1069
+ # * If the association has a +:dependent+ option, destroying the
1070
+ # associated record issues warnings (because that has a side-effect that
1071
+ # would not happen if the association was removed).
1072
+ #
1073
+ # * If the association has a +:touch+ option, saving or destroying the
1074
+ # record issues a warning (because that has a side-effect that would not
1075
+ # happen if the association was removed).
1076
+ #
1077
+ # === Things that do NOT issue warnings
1078
+ #
1079
+ # The rationale behind most of the following edge cases is that Active
1080
+ # Record accesses associations lazily, when used. Before that, the
1081
+ # reference to the association is basically just a Ruby symbol.
1082
+ #
1083
+ # * If +posts+ is deprecated, <tt>has_many :comments, through: :posts</tt>
1084
+ # does not warn. Usage of the +comments+ association reports usage of
1085
+ # +posts+, as we explained above, but the definition of the +has_many+
1086
+ # itself does not.
1087
+ #
1088
+ # * Similarly, <tt>accepts_nested_attributes_for :posts</tt> does not
1089
+ # warn. Assignment to the posts attributes warns, as explained above,
1090
+ # but the +accepts_nested_attributes_for+ call itself does not.
1091
+ #
1092
+ # * Same if an association declares to be inverse of a deprecated one, the
1093
+ # macro itself does not warn.
1094
+ #
1095
+ # * In the same line, the declaration <tt>validates_associated :posts</tt>
1096
+ # does not warn by itself, though access is reported when the validation
1097
+ # runs.
1098
+ #
1099
+ # * Relation query methods like <tt>Author.includes(:posts)</tt> do not
1100
+ # warn by themselves. At that point, that is a relation that internally
1101
+ # stores a symbol for later use. As explained in the previous section,
1102
+ # you get a warning when/if the query is executed.
1103
+ #
1104
+ # * Access to the reflection object of the association as in
1105
+ # <tt>Author.reflect_on_association(:posts)</tt> or
1106
+ # <tt>Author.reflect_on_all_associations</tt> does not warn.
1107
+ #
1108
+ # === Configuration
1109
+ #
1110
+ # Reporting deprecated usage can be configured:
1111
+ #
1112
+ # config.active_record.deprecated_associations_options = { ... }
1113
+ #
1114
+ # If present, this has to be a hash with keys +:mode+ and/or +:backtrace+.
1115
+ #
1116
+ # ==== Mode
1117
+ #
1118
+ # * In +:warn+ mode, usage issues a warning that includes the
1119
+ # application-level place where the access happened, if any. This is the
1120
+ # default mode.
1121
+ #
1122
+ # * In +:raise+ mode, usage raises an
1123
+ # ActiveRecord::DeprecatedAssociationError with a similar message and a
1124
+ # clean backtrace in the exception object.
1125
+ #
1126
+ # * In +:notify+ mode, a <tt>deprecated_association.active_record</tt>
1127
+ # Active Support notification is published. The event payload has the
1128
+ # association reflection (+:reflection+), the application-level location
1129
+ # (+:location+) where the access happened (a Thread::Backtrace::Location
1130
+ # object, or +nil+), and a deprecation message (+:message+).
1131
+ #
1132
+ # ==== Backtrace
1133
+ #
1134
+ # If +:backtrace+ is true, warnings include a clean backtrace in the message
1135
+ # and notifications have a +:backtrace+ key in the payload with an array
1136
+ # of clean Thread::Backtrace::Location objects. Exceptions always get a
1137
+ # clean stack trace set.
1138
+ #
1139
+ # Clean backtraces are computed using the Active Record backtrace cleaner.
1140
+ # In Rails applications, that is by the default the same as
1141
+ # <tt>Rails.backtrace_cleaner</tt>.
1142
+ #
1001
1143
  # == Type safety with ActiveRecord::AssociationTypeMismatch
1002
1144
  #
1003
1145
  # If you attempt to assign an object to an association that doesn't match the inferred
@@ -1186,8 +1328,10 @@ module ActiveRecord
1186
1328
  # [+:as+]
1187
1329
  # Specifies a polymorphic interface (See #belongs_to).
1188
1330
  # [+:through+]
1189
- # Specifies an association through which to perform the query. This can be any other type
1190
- # of association, including other <tt>:through</tt> associations. Options for <tt>:class_name</tt>,
1331
+ # Specifies an association through which to perform the query.
1332
+ #
1333
+ # This can be any other type of association, including other <tt>:through</tt> associations,
1334
+ # but it cannot be a polymorphic association. Options for <tt>:class_name</tt>,
1191
1335
  # <tt>:primary_key</tt> and <tt>:foreign_key</tt> are ignored, as the association uses the
1192
1336
  # source reflection.
1193
1337
  #
@@ -1255,6 +1399,17 @@ module ActiveRecord
1255
1399
  # persisted new records placed at the end.
1256
1400
  # When set to +:nested_attributes_order+, the index is based on the record order received by
1257
1401
  # nested attributes setter, when accepts_nested_attributes_for is used.
1402
+ # [:before_add]
1403
+ # Defines an {association callback}[rdoc-ref:Associations::ClassMethods@Association+callbacks] that gets triggered <b>before an object is added</b> to the association collection.
1404
+ # [:after_add]
1405
+ # Defines an {association callback}[rdoc-ref:Associations::ClassMethods@Association+callbacks] that gets triggered <b>after an object is added</b> to the association collection.
1406
+ # [:before_remove]
1407
+ # Defines an {association callback}[rdoc-ref:Associations::ClassMethods@Association+callbacks] that gets triggered <b>before an object is removed</b> from the association collection.
1408
+ # [:after_remove]
1409
+ # Defines an {association callback}[rdoc-ref:Associations::ClassMethods@Association+callbacks] that gets triggered <b>after an object is removed</b> from the association collection.
1410
+ # [+:deprecated+]
1411
+ # If true, marks the association as deprecated. Usage of deprecated associations is reported.
1412
+ # Please, check the class documentation above for details.
1258
1413
  #
1259
1414
  # Option examples:
1260
1415
  # has_many :comments, -> { order("posted_on") }
@@ -1271,7 +1426,7 @@ module ActiveRecord
1271
1426
  # has_many :comments, index_errors: :nested_attributes_order
1272
1427
  def has_many(name, scope = nil, **options, &extension)
1273
1428
  reflection = Builder::HasMany.build(self, name, scope, options, &extension)
1274
- Reflection.add_reflection self, name, reflection
1429
+ Reflection.add_reflection(self, name, reflection)
1275
1430
  end
1276
1431
 
1277
1432
  # Specifies a one-to-one association with another class. This method
@@ -1381,10 +1536,12 @@ module ActiveRecord
1381
1536
  # [+:as+]
1382
1537
  # Specifies a polymorphic interface (See #belongs_to).
1383
1538
  # [+:through+]
1384
- # Specifies a Join Model through which to perform the query. Options for <tt>:class_name</tt>,
1385
- # <tt>:primary_key</tt>, and <tt>:foreign_key</tt> are ignored, as the association uses the
1386
- # source reflection. You can only use a <tt>:through</tt> query through a #has_one
1387
- # or #belongs_to association on the join model.
1539
+ # Specifies an association through which to perform the query.
1540
+ #
1541
+ # The through association must be a +has_one+, <tt>has_one :through</tt>, or non-polymorphic +belongs_to+.
1542
+ # That is, a non-polymorphic singular association. Options for <tt>:class_name</tt>, <tt>:primary_key</tt>,
1543
+ # and <tt>:foreign_key</tt> are ignored, as the association uses the source reflection. You can only
1544
+ # use a <tt>:through</tt> query through a #has_one or #belongs_to association on the join model.
1388
1545
  #
1389
1546
  # If the association on the join model is a #belongs_to, the collection can be modified
1390
1547
  # and the records on the <tt>:through</tt> model will be automatically created and removed
@@ -1450,12 +1607,15 @@ module ActiveRecord
1450
1607
  # Serves as a composite foreign key. Defines the list of columns to be used to query the associated object.
1451
1608
  # This is an optional option. By default Rails will attempt to derive the value automatically.
1452
1609
  # When the value is set the Array size must match associated model's primary key or +query_constraints+ size.
1610
+ # [+:deprecated+]
1611
+ # If true, marks the association as deprecated. Usage of deprecated associations is reported.
1612
+ # Please, check the class documentation above for details.
1453
1613
  #
1454
1614
  # Option examples:
1455
1615
  # has_one :credit_card, dependent: :destroy # destroys the associated credit card
1456
1616
  # has_one :credit_card, dependent: :nullify # updates the associated records foreign
1457
1617
  # # key value to NULL rather than destroying it
1458
- # has_one :last_comment, -> { order('posted_on') }, class_name: "Comment"
1618
+ # has_one :last_comment, -> { order('posted_on desc') }, class_name: "Comment"
1459
1619
  # has_one :project_manager, -> { where(role: 'project_manager') }, class_name: "Person"
1460
1620
  # has_one :attachment, as: :attachable
1461
1621
  # has_one :boss, -> { readonly }
@@ -1467,7 +1627,7 @@ module ActiveRecord
1467
1627
  # has_one :employment_record_book, query_constraints: [:organization_id, :employee_id]
1468
1628
  def has_one(name, scope = nil, **options)
1469
1629
  reflection = Builder::HasOne.build(self, name, scope, options)
1470
- Reflection.add_reflection self, name, reflection
1630
+ Reflection.add_reflection(self, name, reflection)
1471
1631
  end
1472
1632
 
1473
1633
  # Specifies a one-to-one association with another class. This method
@@ -1546,7 +1706,9 @@ module ActiveRecord
1546
1706
  # [+:class_name+]
1547
1707
  # Specify the class name of the association. Use it only if that name can't be inferred
1548
1708
  # from the association name. So <tt>belongs_to :author</tt> will by default be linked to the Author class, but
1549
- # if the real class name is Person, you'll have to specify it with this option.
1709
+ # if the real class name is Person, you'll have to specify it with this option. +:class_name+
1710
+ # is not supported in polymorphic associations, since in that case the class name of the
1711
+ # associated record is stored in the type column.
1550
1712
  # [+:foreign_key+]
1551
1713
  # Specify the foreign key used for the association. By default this is guessed to be the name
1552
1714
  # of the association with an "_id" suffix. So a class that defines a <tt>belongs_to :person</tt>
@@ -1640,6 +1802,9 @@ module ActiveRecord
1640
1802
  # Serves as a composite foreign key. Defines the list of columns to be used to query the associated object.
1641
1803
  # This is an optional option. By default Rails will attempt to derive the value automatically.
1642
1804
  # When the value is set the Array size must match associated model's primary key or +query_constraints+ size.
1805
+ # [+:deprecated+]
1806
+ # If true, marks the association as deprecated. Usage of deprecated associations is reported.
1807
+ # Please, check the class documentation above for details.
1643
1808
  #
1644
1809
  # Option examples:
1645
1810
  # belongs_to :firm, foreign_key: "client_of"
@@ -1658,7 +1823,7 @@ module ActiveRecord
1658
1823
  # belongs_to :note, query_constraints: [:organization_id, :note_id]
1659
1824
  def belongs_to(name, scope = nil, **options)
1660
1825
  reflection = Builder::BelongsTo.build(self, name, scope, options)
1661
- Reflection.add_reflection self, name, reflection
1826
+ Reflection.add_reflection(self, name, reflection)
1662
1827
  end
1663
1828
 
1664
1829
  # Specifies a many-to-many relationship with another class. This associates two classes via an
@@ -1678,7 +1843,7 @@ module ActiveRecord
1678
1843
  # The join table should not have a primary key or a model associated with it. You must manually generate the
1679
1844
  # join table with a migration such as this:
1680
1845
  #
1681
- # class CreateDevelopersProjectsJoinTable < ActiveRecord::Migration[7.2]
1846
+ # class CreateDevelopersProjectsJoinTable < ActiveRecord::Migration[8.1]
1682
1847
  # def change
1683
1848
  # create_join_table :developers, :projects
1684
1849
  # end
@@ -1829,6 +1994,9 @@ module ActiveRecord
1829
1994
  # <tt>:autosave</tt> to <tt>true</tt>.
1830
1995
  # [+:strict_loading+]
1831
1996
  # Enforces strict loading every time an associated record is loaded through this association.
1997
+ # [+:deprecated+]
1998
+ # If true, marks the association as deprecated. Usage of deprecated associations is reported.
1999
+ # Please, check the class documentation above for details.
1832
2000
  #
1833
2001
  # Option examples:
1834
2002
  # has_and_belongs_to_many :projects
@@ -1840,17 +2008,17 @@ module ActiveRecord
1840
2008
  def has_and_belongs_to_many(name, scope = nil, **options, &extension)
1841
2009
  habtm_reflection = ActiveRecord::Reflection::HasAndBelongsToManyReflection.new(name, scope, options, self)
1842
2010
 
1843
- builder = Builder::HasAndBelongsToMany.new name, self, options
2011
+ builder = Builder::HasAndBelongsToMany.new(name, self, options)
1844
2012
 
1845
2013
  join_model = builder.through_model
1846
2014
 
1847
- const_set join_model.name, join_model
1848
- private_constant join_model.name
2015
+ const_set(join_model.name, join_model)
2016
+ private_constant(join_model.name)
1849
2017
 
1850
- middle_reflection = builder.middle_reflection join_model
2018
+ middle_reflection = builder.middle_reflection(join_model)
1851
2019
 
1852
- Builder::HasMany.define_callbacks self, middle_reflection
1853
- Reflection.add_reflection self, middle_reflection.name, middle_reflection
2020
+ Builder::HasMany.define_callbacks(self, middle_reflection)
2021
+ Reflection.add_reflection(self, middle_reflection.name, middle_reflection)
1854
2022
  middle_reflection.parent_reflection = habtm_reflection
1855
2023
 
1856
2024
  include Module.new {
@@ -1867,8 +2035,8 @@ module ActiveRecord
1867
2035
  hm_options[:through] = middle_reflection.name
1868
2036
  hm_options[:source] = join_model.right_reflection.name
1869
2037
 
1870
- [:before_add, :after_add, :before_remove, :after_remove, :autosave, :validate, :join_table, :class_name, :extend, :strict_loading].each do |k|
1871
- hm_options[k] = options[k] if options.key? k
2038
+ [:before_add, :after_add, :before_remove, :after_remove, :autosave, :validate, :join_table, :class_name, :extend, :strict_loading, :deprecated].each do |k|
2039
+ hm_options[k] = options[k] if options.key?(k)
1872
2040
  end
1873
2041
 
1874
2042
  has_many name, scope, **hm_options, &extension
@@ -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