activerecord 6.1.7.7 → 7.0.0.alpha1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


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

Files changed (220) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +726 -1389
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +1 -1
  5. data/lib/active_record/aggregations.rb +1 -1
  6. data/lib/active_record/association_relation.rb +0 -10
  7. data/lib/active_record/associations/association.rb +31 -9
  8. data/lib/active_record/associations/association_scope.rb +1 -3
  9. data/lib/active_record/associations/belongs_to_association.rb +15 -4
  10. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +10 -2
  11. data/lib/active_record/associations/builder/association.rb +8 -2
  12. data/lib/active_record/associations/builder/belongs_to.rb +19 -6
  13. data/lib/active_record/associations/builder/collection_association.rb +1 -1
  14. data/lib/active_record/associations/builder/has_many.rb +3 -2
  15. data/lib/active_record/associations/builder/has_one.rb +2 -1
  16. data/lib/active_record/associations/builder/singular_association.rb +2 -2
  17. data/lib/active_record/associations/collection_association.rb +14 -23
  18. data/lib/active_record/associations/collection_proxy.rb +8 -3
  19. data/lib/active_record/associations/disable_joins_association_scope.rb +59 -0
  20. data/lib/active_record/associations/has_many_association.rb +1 -1
  21. data/lib/active_record/associations/has_many_through_association.rb +2 -1
  22. data/lib/active_record/associations/has_one_association.rb +10 -7
  23. data/lib/active_record/associations/has_one_through_association.rb +1 -1
  24. data/lib/active_record/associations/preloader/association.rb +161 -47
  25. data/lib/active_record/associations/preloader/batch.rb +51 -0
  26. data/lib/active_record/associations/preloader/branch.rb +147 -0
  27. data/lib/active_record/associations/preloader/through_association.rb +37 -11
  28. data/lib/active_record/associations/preloader.rb +46 -110
  29. data/lib/active_record/associations/singular_association.rb +8 -2
  30. data/lib/active_record/associations/through_association.rb +1 -1
  31. data/lib/active_record/associations.rb +76 -81
  32. data/lib/active_record/asynchronous_queries_tracker.rb +57 -0
  33. data/lib/active_record/attribute_assignment.rb +1 -1
  34. data/lib/active_record/attribute_methods/before_type_cast.rb +7 -2
  35. data/lib/active_record/attribute_methods/dirty.rb +41 -16
  36. data/lib/active_record/attribute_methods/primary_key.rb +2 -2
  37. data/lib/active_record/attribute_methods/query.rb +2 -2
  38. data/lib/active_record/attribute_methods/read.rb +7 -5
  39. data/lib/active_record/attribute_methods/serialization.rb +66 -12
  40. data/lib/active_record/attribute_methods/time_zone_conversion.rb +4 -3
  41. data/lib/active_record/attribute_methods/write.rb +7 -10
  42. data/lib/active_record/attribute_methods.rb +6 -9
  43. data/lib/active_record/attributes.rb +24 -35
  44. data/lib/active_record/autosave_association.rb +3 -18
  45. data/lib/active_record/base.rb +19 -1
  46. data/lib/active_record/callbacks.rb +2 -2
  47. data/lib/active_record/coders/yaml_column.rb +2 -14
  48. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +312 -0
  49. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +209 -0
  50. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +76 -0
  51. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +31 -558
  52. data/lib/active_record/connection_adapters/abstract/database_statements.rb +45 -21
  53. data/lib/active_record/connection_adapters/abstract/query_cache.rb +24 -12
  54. data/lib/active_record/connection_adapters/abstract/quoting.rb +12 -14
  55. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +4 -17
  56. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +30 -13
  57. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +60 -16
  58. data/lib/active_record/connection_adapters/abstract/transaction.rb +3 -3
  59. data/lib/active_record/connection_adapters/abstract_adapter.rb +112 -66
  60. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +96 -81
  61. data/lib/active_record/connection_adapters/mysql/database_statements.rb +33 -23
  62. data/lib/active_record/connection_adapters/mysql/quoting.rb +16 -1
  63. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +1 -1
  64. data/lib/active_record/connection_adapters/mysql2_adapter.rb +12 -6
  65. data/lib/active_record/connection_adapters/pool_config.rb +1 -3
  66. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +19 -14
  67. data/lib/active_record/connection_adapters/postgresql/oid/date.rb +8 -0
  68. data/lib/active_record/connection_adapters/postgresql/oid/date_time.rb +5 -0
  69. data/lib/active_record/connection_adapters/postgresql/oid/hstore.rb +53 -14
  70. data/lib/active_record/connection_adapters/postgresql/oid/range.rb +1 -1
  71. data/lib/active_record/connection_adapters/postgresql/oid/timestamp.rb +15 -0
  72. data/lib/active_record/connection_adapters/postgresql/oid/timestamp_with_time_zone.rb +28 -0
  73. data/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +18 -6
  74. data/lib/active_record/connection_adapters/postgresql/oid.rb +2 -0
  75. data/lib/active_record/connection_adapters/postgresql/quoting.rb +6 -32
  76. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +32 -0
  77. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +5 -1
  78. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +12 -12
  79. data/lib/active_record/connection_adapters/postgresql_adapter.rb +159 -102
  80. data/lib/active_record/connection_adapters/schema_cache.rb +36 -37
  81. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +23 -19
  82. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +4 -2
  83. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +61 -30
  84. data/lib/active_record/connection_adapters.rb +6 -5
  85. data/lib/active_record/connection_handling.rb +20 -38
  86. data/lib/active_record/core.rb +111 -125
  87. data/lib/active_record/database_configurations/connection_url_resolver.rb +0 -1
  88. data/lib/active_record/database_configurations/database_config.rb +12 -0
  89. data/lib/active_record/database_configurations/hash_config.rb +27 -1
  90. data/lib/active_record/database_configurations/url_config.rb +2 -2
  91. data/lib/active_record/database_configurations.rb +17 -9
  92. data/lib/active_record/delegated_type.rb +33 -11
  93. data/lib/active_record/destroy_association_async_job.rb +1 -1
  94. data/lib/active_record/disable_joins_association_relation.rb +39 -0
  95. data/lib/active_record/dynamic_matchers.rb +1 -1
  96. data/lib/active_record/encryption/cipher/aes256_gcm.rb +98 -0
  97. data/lib/active_record/encryption/cipher.rb +53 -0
  98. data/lib/active_record/encryption/config.rb +44 -0
  99. data/lib/active_record/encryption/configurable.rb +61 -0
  100. data/lib/active_record/encryption/context.rb +35 -0
  101. data/lib/active_record/encryption/contexts.rb +72 -0
  102. data/lib/active_record/encryption/derived_secret_key_provider.rb +12 -0
  103. data/lib/active_record/encryption/deterministic_key_provider.rb +14 -0
  104. data/lib/active_record/encryption/encryptable_record.rb +208 -0
  105. data/lib/active_record/encryption/encrypted_attribute_type.rb +140 -0
  106. data/lib/active_record/encryption/encrypted_fixtures.rb +38 -0
  107. data/lib/active_record/encryption/encrypting_only_encryptor.rb +12 -0
  108. data/lib/active_record/encryption/encryptor.rb +155 -0
  109. data/lib/active_record/encryption/envelope_encryption_key_provider.rb +55 -0
  110. data/lib/active_record/encryption/errors.rb +15 -0
  111. data/lib/active_record/encryption/extended_deterministic_queries.rb +160 -0
  112. data/lib/active_record/encryption/extended_deterministic_uniqueness_validator.rb +29 -0
  113. data/lib/active_record/encryption/key.rb +28 -0
  114. data/lib/active_record/encryption/key_generator.rb +42 -0
  115. data/lib/active_record/encryption/key_provider.rb +46 -0
  116. data/lib/active_record/encryption/message.rb +33 -0
  117. data/lib/active_record/encryption/message_serializer.rb +80 -0
  118. data/lib/active_record/encryption/null_encryptor.rb +21 -0
  119. data/lib/active_record/encryption/properties.rb +76 -0
  120. data/lib/active_record/encryption/read_only_null_encryptor.rb +24 -0
  121. data/lib/active_record/encryption/scheme.rb +99 -0
  122. data/lib/active_record/encryption.rb +55 -0
  123. data/lib/active_record/enum.rb +41 -41
  124. data/lib/active_record/errors.rb +66 -3
  125. data/lib/active_record/fixture_set/file.rb +15 -1
  126. data/lib/active_record/fixture_set/table_row.rb +40 -5
  127. data/lib/active_record/fixture_set/table_rows.rb +4 -4
  128. data/lib/active_record/fixtures.rb +16 -11
  129. data/lib/active_record/future_result.rb +139 -0
  130. data/lib/active_record/gem_version.rb +4 -4
  131. data/lib/active_record/inheritance.rb +55 -17
  132. data/lib/active_record/insert_all.rb +34 -5
  133. data/lib/active_record/integration.rb +1 -1
  134. data/lib/active_record/internal_metadata.rb +1 -5
  135. data/lib/active_record/locking/optimistic.rb +10 -9
  136. data/lib/active_record/log_subscriber.rb +6 -2
  137. data/lib/active_record/middleware/database_selector/resolver.rb +6 -10
  138. data/lib/active_record/middleware/database_selector.rb +8 -3
  139. data/lib/active_record/migration/command_recorder.rb +4 -4
  140. data/lib/active_record/migration/compatibility.rb +89 -10
  141. data/lib/active_record/migration/join_table.rb +1 -1
  142. data/lib/active_record/migration.rb +109 -79
  143. data/lib/active_record/model_schema.rb +45 -31
  144. data/lib/active_record/nested_attributes.rb +3 -3
  145. data/lib/active_record/no_touching.rb +2 -2
  146. data/lib/active_record/null_relation.rb +2 -6
  147. data/lib/active_record/persistence.rb +134 -45
  148. data/lib/active_record/query_cache.rb +2 -2
  149. data/lib/active_record/query_logs.rb +203 -0
  150. data/lib/active_record/querying.rb +15 -5
  151. data/lib/active_record/railtie.rb +117 -17
  152. data/lib/active_record/railties/controller_runtime.rb +1 -1
  153. data/lib/active_record/railties/databases.rake +72 -48
  154. data/lib/active_record/readonly_attributes.rb +11 -0
  155. data/lib/active_record/reflection.rb +45 -44
  156. data/lib/active_record/relation/batches/batch_enumerator.rb +19 -5
  157. data/lib/active_record/relation/batches.rb +3 -3
  158. data/lib/active_record/relation/calculations.rb +39 -26
  159. data/lib/active_record/relation/delegation.rb +6 -6
  160. data/lib/active_record/relation/finder_methods.rb +31 -22
  161. data/lib/active_record/relation/merger.rb +20 -13
  162. data/lib/active_record/relation/predicate_builder.rb +1 -6
  163. data/lib/active_record/relation/query_attribute.rb +5 -11
  164. data/lib/active_record/relation/query_methods.rb +230 -49
  165. data/lib/active_record/relation/record_fetch_warning.rb +2 -2
  166. data/lib/active_record/relation/spawn_methods.rb +2 -2
  167. data/lib/active_record/relation/where_clause.rb +8 -4
  168. data/lib/active_record/relation.rb +166 -77
  169. data/lib/active_record/result.rb +17 -2
  170. data/lib/active_record/runtime_registry.rb +2 -4
  171. data/lib/active_record/sanitization.rb +11 -7
  172. data/lib/active_record/schema_dumper.rb +3 -3
  173. data/lib/active_record/schema_migration.rb +0 -4
  174. data/lib/active_record/scoping/default.rb +61 -12
  175. data/lib/active_record/scoping/named.rb +3 -11
  176. data/lib/active_record/scoping.rb +40 -22
  177. data/lib/active_record/serialization.rb +1 -1
  178. data/lib/active_record/signed_id.rb +1 -1
  179. data/lib/active_record/store.rb +1 -6
  180. data/lib/active_record/tasks/database_tasks.rb +106 -22
  181. data/lib/active_record/tasks/mysql_database_tasks.rb +1 -1
  182. data/lib/active_record/tasks/postgresql_database_tasks.rb +14 -11
  183. data/lib/active_record/test_databases.rb +1 -1
  184. data/lib/active_record/test_fixtures.rb +9 -13
  185. data/lib/active_record/timestamp.rb +3 -4
  186. data/lib/active_record/transactions.rb +9 -14
  187. data/lib/active_record/translation.rb +2 -2
  188. data/lib/active_record/type/adapter_specific_registry.rb +32 -7
  189. data/lib/active_record/type/hash_lookup_type_map.rb +34 -1
  190. data/lib/active_record/type/internal/timezone.rb +2 -2
  191. data/lib/active_record/type/serialized.rb +1 -1
  192. data/lib/active_record/type/type_map.rb +17 -20
  193. data/lib/active_record/type.rb +1 -2
  194. data/lib/active_record/validations/associated.rb +1 -1
  195. data/lib/active_record.rb +170 -2
  196. data/lib/arel/attributes/attribute.rb +0 -8
  197. data/lib/arel/crud.rb +18 -22
  198. data/lib/arel/delete_manager.rb +2 -4
  199. data/lib/arel/insert_manager.rb +2 -3
  200. data/lib/arel/nodes/casted.rb +1 -1
  201. data/lib/arel/nodes/delete_statement.rb +8 -13
  202. data/lib/arel/nodes/insert_statement.rb +2 -2
  203. data/lib/arel/nodes/select_core.rb +2 -2
  204. data/lib/arel/nodes/select_statement.rb +2 -2
  205. data/lib/arel/nodes/update_statement.rb +3 -2
  206. data/lib/arel/predications.rb +1 -1
  207. data/lib/arel/select_manager.rb +10 -4
  208. data/lib/arel/table.rb +0 -1
  209. data/lib/arel/tree_manager.rb +0 -12
  210. data/lib/arel/update_manager.rb +2 -4
  211. data/lib/arel/visitors/dot.rb +80 -90
  212. data/lib/arel/visitors/mysql.rb +6 -1
  213. data/lib/arel/visitors/postgresql.rb +0 -10
  214. data/lib/arel/visitors/to_sql.rb +43 -2
  215. data/lib/arel.rb +1 -1
  216. data/lib/rails/generators/active_record/application_record/templates/application_record.rb.tt +1 -1
  217. data/lib/rails/generators/active_record/model/templates/abstract_base_class.rb.tt +1 -1
  218. data/lib/rails/generators/active_record/model/templates/model.rb.tt +1 -1
  219. data/lib/rails/generators/active_record/model/templates/module.rb.tt +2 -2
  220. metadata +55 -17
@@ -41,15 +41,18 @@ module ActiveRecord
41
41
  #
42
42
  # This could result in many rows that contain redundant data and it performs poorly at scale
43
43
  # and is therefore only used when necessary.
44
- #
45
- class Preloader #:nodoc:
44
+ class Preloader # :nodoc:
46
45
  extend ActiveSupport::Autoload
47
46
 
48
47
  eager_autoload do
49
48
  autoload :Association, "active_record/associations/preloader/association"
49
+ autoload :Batch, "active_record/associations/preloader/batch"
50
+ autoload :Branch, "active_record/associations/preloader/branch"
50
51
  autoload :ThroughAssociation, "active_record/associations/preloader/through_association"
51
52
  end
52
53
 
54
+ attr_reader :records, :associations, :scope, :associate_by_default
55
+
53
56
  # Eager loads the named associations for the given Active Record record(s).
54
57
  #
55
58
  # In this description, 'association name' shall refer to the name passed
@@ -77,130 +80,63 @@ module ActiveRecord
77
80
  # example, specifying <tt>{ author: :avatar }</tt> will preload a
78
81
  # book's author, as well as that author's avatar.
79
82
  #
80
- # +:associations+ has the same format as the +:include+ option for
81
- # <tt>ActiveRecord::Base.find</tt>. So +associations+ could look like this:
83
+ # +:associations+ has the same format as the +:include+ method in
84
+ # <tt>ActiveRecord::QueryMethods</tt>. So +associations+ could look like this:
82
85
  #
83
86
  # :books
84
87
  # [ :books, :author ]
85
88
  # { author: :avatar }
86
89
  # [ :books, { author: :avatar } ]
87
- def preload(records, associations, preload_scope = nil)
88
- records = Array.wrap(records).compact
89
-
90
- if records.empty?
91
- []
90
+ #
91
+ # +available_records+ is an array of ActiveRecord::Base. The Preloader
92
+ # will try to use the objects in this array to preload the requested
93
+ # associations before querying the database. This can save database
94
+ # queries by reusing in-memory objects. The optimization is only applied
95
+ # to single associations (i.e. :belongs_to, :has_one) with no scopes.
96
+ def initialize(associate_by_default: true, **kwargs)
97
+ if kwargs.empty?
98
+ ActiveSupport::Deprecation.warn("Calling `Preloader#initialize` without arguments is deprecated and will be removed in Rails 7.0.")
92
99
  else
93
- Array.wrap(associations).flat_map { |association|
94
- preloaders_on association, records, preload_scope
95
- }
100
+ @records = kwargs[:records]
101
+ @associations = kwargs[:associations]
102
+ @scope = kwargs[:scope]
103
+ @available_records = kwargs[:available_records] || []
104
+ @associate_by_default = associate_by_default
105
+
106
+ @tree = Branch.new(
107
+ parent: nil,
108
+ association: nil,
109
+ children: associations,
110
+ associate_by_default: @associate_by_default,
111
+ scope: @scope
112
+ )
113
+ @tree.preloaded_records = records
96
114
  end
97
115
  end
98
116
 
99
- def initialize(associate_by_default: true)
100
- @associate_by_default = associate_by_default
117
+ def empty?
118
+ associations.nil? || records.length == 0
101
119
  end
102
120
 
103
- private
104
- # Loads all the given data into +records+ for the +association+.
105
- def preloaders_on(association, records, scope, polymorphic_parent = false)
106
- case association
107
- when Hash
108
- preloaders_for_hash(association, records, scope, polymorphic_parent)
109
- when Symbol, String
110
- preloaders_for_one(association, records, scope, polymorphic_parent)
111
- else
112
- raise ArgumentError, "#{association.inspect} was not recognized for preload"
113
- end
114
- end
115
-
116
- def preloaders_for_hash(association, records, scope, polymorphic_parent)
117
- association.flat_map { |parent, child|
118
- grouped_records(parent, records, polymorphic_parent).flat_map do |reflection, reflection_records|
119
- loaders = preloaders_for_reflection(reflection, reflection_records, scope)
120
- recs = loaders.flat_map(&:preloaded_records).uniq
121
- child_polymorphic_parent = reflection && reflection.options[:polymorphic]
122
- loaders.concat Array.wrap(child).flat_map { |assoc|
123
- preloaders_on assoc, recs, scope, child_polymorphic_parent
124
- }
125
- loaders
126
- end
127
- }
128
- end
129
-
130
- # Loads all the given data into +records+ for a singular +association+.
131
- #
132
- # Functions by instantiating a preloader class such as Preloader::Association and
133
- # call the +run+ method for each passed in class in the +records+ argument.
134
- #
135
- # Not all records have the same class, so group then preload group on the reflection
136
- # itself so that if various subclass share the same association then we do not split
137
- # them unnecessarily
138
- #
139
- # Additionally, polymorphic belongs_to associations can have multiple associated
140
- # classes, depending on the polymorphic_type field. So we group by the classes as
141
- # well.
142
- def preloaders_for_one(association, records, scope, polymorphic_parent)
143
- grouped_records(association, records, polymorphic_parent)
144
- .flat_map do |reflection, reflection_records|
145
- preloaders_for_reflection reflection, reflection_records, scope
146
- end
147
- end
148
-
149
- def preloaders_for_reflection(reflection, records, scope)
150
- records.group_by { |record| record.association(reflection.name).klass }.map do |rhs_klass, rs|
151
- preloader_for(reflection, rs).new(rhs_klass, rs, reflection, scope, @associate_by_default).run
152
- end
153
- end
121
+ def call
122
+ Batch.new([self], available_records: @available_records).call
154
123
 
155
- def grouped_records(association, records, polymorphic_parent)
156
- h = {}
157
- records.each do |record|
158
- reflection = record.class._reflect_on_association(association)
159
- next if polymorphic_parent && !reflection || !record.association(association).klass
160
- (h[reflection] ||= []) << record
161
- end
162
- h
163
- end
164
-
165
- class AlreadyLoaded # :nodoc:
166
- def initialize(klass, owners, reflection, preload_scope, associate_by_default = true)
167
- @owners = owners
168
- @reflection = reflection
169
- end
170
-
171
- def run
172
- self
173
- end
174
-
175
- def preloaded_records
176
- @preloaded_records ||= records_by_owner.flat_map(&:last)
177
- end
124
+ loaders
125
+ end
178
126
 
179
- def records_by_owner
180
- @records_by_owner ||= owners.index_with do |owner|
181
- Array(owner.association(reflection.name).target)
182
- end
183
- end
127
+ def preload(records, associations, preload_scope = nil)
128
+ ActiveSupport::Deprecation.warn("`preload` is deprecated and will be removed in Rails 7.0. Call `Preloader.new(kwargs).call` instead.")
184
129
 
185
- private
186
- attr_reader :owners, :reflection
187
- end
130
+ Preloader.new(records: records, associations: associations, scope: preload_scope).call
131
+ end
188
132
 
189
- # Returns a class containing the logic needed to load preload the data
190
- # and attach it to a relation. The class returned implements a `run` method
191
- # that accepts a preloader.
192
- def preloader_for(reflection, owners)
193
- if owners.all? { |o| o.association(reflection.name).loaded? }
194
- return AlreadyLoaded
195
- end
196
- reflection.check_preloadable!
133
+ def branches
134
+ @tree.children
135
+ end
197
136
 
198
- if reflection.options[:through]
199
- ThroughAssociation
200
- else
201
- Association
202
- end
203
- end
137
+ def loaders
138
+ branches.flat_map(&:loaders)
139
+ end
204
140
  end
205
141
  end
206
142
  end
@@ -2,9 +2,11 @@
2
2
 
3
3
  module ActiveRecord
4
4
  module Associations
5
- class SingularAssociation < Association #:nodoc:
5
+ class SingularAssociation < Association # :nodoc:
6
6
  # Implements the reader method, e.g. foo.bar for Foo.has_one :bar
7
7
  def reader
8
+ ensure_klass_exists!
9
+
8
10
  if !loaded? || stale_target?
9
11
  reload
10
12
  end
@@ -36,7 +38,11 @@ module ActiveRecord
36
38
  end
37
39
 
38
40
  def find_target
39
- super.first
41
+ if disable_joins
42
+ scope.first
43
+ else
44
+ super.first
45
+ end
40
46
  end
41
47
 
42
48
  def replace(record)
@@ -3,7 +3,7 @@
3
3
  module ActiveRecord
4
4
  module Associations
5
5
  # = Active Record Through Association
6
- module ThroughAssociation #:nodoc:
6
+ module ThroughAssociation # :nodoc:
7
7
  delegate :source_reflection, to: :reflection
8
8
 
9
9
  private
@@ -1,11 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/core_ext/enumerable"
4
- require "active_support/core_ext/string/conversions"
5
-
6
3
  module ActiveRecord
7
- class AssociationNotFoundError < ConfigurationError #:nodoc:
4
+ class AssociationNotFoundError < ConfigurationError # :nodoc:
8
5
  attr_reader :record, :association_name
6
+
9
7
  def initialize(record = nil, association_name = nil)
10
8
  @record = record
11
9
  @association_name = association_name
@@ -16,32 +14,25 @@ module ActiveRecord
16
14
  end
17
15
  end
18
16
 
19
- class Correction
20
- def initialize(error)
21
- @error = error
22
- end
17
+ if defined?(DidYouMean::Correctable) && defined?(DidYouMean::SpellChecker)
18
+ include DidYouMean::Correctable
23
19
 
24
20
  def corrections
25
- if @error.association_name
26
- maybe_these = @error.record.class.reflections.keys
27
-
28
- maybe_these.sort_by { |n|
29
- DidYouMean::Jaro.distance(@error.association_name.to_s, n)
30
- }.reverse.first(4)
21
+ if record && association_name
22
+ @corrections ||= begin
23
+ maybe_these = record.class.reflections.keys
24
+ DidYouMean::SpellChecker.new(dictionary: maybe_these).correct(association_name)
25
+ end
31
26
  else
32
27
  []
33
28
  end
34
29
  end
35
30
  end
36
-
37
- # We may not have DYM, and DYM might not let us register error handlers
38
- if defined?(DidYouMean) && DidYouMean.respond_to?(:correct_error)
39
- DidYouMean.correct_error(self, Correction)
40
- end
41
31
  end
42
32
 
43
- class InverseOfAssociationNotFoundError < ActiveRecordError #:nodoc:
33
+ class InverseOfAssociationNotFoundError < ActiveRecordError # :nodoc:
44
34
  attr_reader :reflection, :associated_class
35
+
45
36
  def initialize(reflection = nil, associated_class = nil)
46
37
  if reflection
47
38
  @reflection = reflection
@@ -52,31 +43,23 @@ module ActiveRecord
52
43
  end
53
44
  end
54
45
 
55
- class Correction
56
- def initialize(error)
57
- @error = error
58
- end
46
+ if defined?(DidYouMean::Correctable) && defined?(DidYouMean::SpellChecker)
47
+ include DidYouMean::Correctable
59
48
 
60
49
  def corrections
61
- if @error.reflection && @error.associated_class
62
- maybe_these = @error.associated_class.reflections.keys
63
-
64
- maybe_these.sort_by { |n|
65
- DidYouMean::Jaro.distance(@error.reflection.options[:inverse_of].to_s, n)
66
- }.reverse.first(4)
50
+ if reflection && associated_class
51
+ @corrections ||= begin
52
+ maybe_these = associated_class.reflections.keys
53
+ DidYouMean::SpellChecker.new(dictionary: maybe_these).correct(reflection.options[:inverse_of].to_s)
54
+ end
67
55
  else
68
56
  []
69
57
  end
70
58
  end
71
59
  end
72
-
73
- # We may not have DYM, and DYM might not let us register error handlers
74
- if defined?(DidYouMean) && DidYouMean.respond_to?(:correct_error)
75
- DidYouMean.correct_error(self, Correction)
76
- end
77
60
  end
78
61
 
79
- class HasManyThroughAssociationNotFoundError < ActiveRecordError #:nodoc:
62
+ class HasManyThroughAssociationNotFoundError < ActiveRecordError # :nodoc:
80
63
  attr_reader :owner_class, :reflection
81
64
 
82
65
  def initialize(owner_class = nil, reflection = nil)
@@ -89,32 +72,24 @@ module ActiveRecord
89
72
  end
90
73
  end
91
74
 
92
- class Correction
93
- def initialize(error)
94
- @error = error
95
- end
75
+ if defined?(DidYouMean::Correctable) && defined?(DidYouMean::SpellChecker)
76
+ include DidYouMean::Correctable
96
77
 
97
78
  def corrections
98
- if @error.reflection && @error.owner_class
99
- maybe_these = @error.owner_class.reflections.keys
100
- maybe_these -= [@error.reflection.name.to_s] # remove failing reflection
101
-
102
- maybe_these.sort_by { |n|
103
- DidYouMean::Jaro.distance(@error.reflection.options[:through].to_s, n)
104
- }.reverse.first(4)
79
+ if owner_class && reflection
80
+ @corrections ||= begin
81
+ maybe_these = owner_class.reflections.keys
82
+ maybe_these -= [reflection.name.to_s] # remove failing reflection
83
+ DidYouMean::SpellChecker.new(dictionary: maybe_these).correct(reflection.options[:through].to_s)
84
+ end
105
85
  else
106
86
  []
107
87
  end
108
88
  end
109
89
  end
110
-
111
- # We may not have DYM, and DYM might not let us register error handlers
112
- if defined?(DidYouMean) && DidYouMean.respond_to?(:correct_error)
113
- DidYouMean.correct_error(self, Correction)
114
- end
115
90
  end
116
91
 
117
- class HasManyThroughAssociationPolymorphicSourceError < ActiveRecordError #:nodoc:
92
+ class HasManyThroughAssociationPolymorphicSourceError < ActiveRecordError # :nodoc:
118
93
  def initialize(owner_class_name = nil, reflection = nil, source_reflection = nil)
119
94
  if owner_class_name && reflection && source_reflection
120
95
  super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' on the polymorphic object '#{source_reflection.class_name}##{source_reflection.name}' without 'source_type'. Try adding 'source_type: \"#{reflection.name.to_s.classify}\"' to 'has_many :through' definition.")
@@ -124,7 +99,7 @@ module ActiveRecord
124
99
  end
125
100
  end
126
101
 
127
- class HasManyThroughAssociationPolymorphicThroughError < ActiveRecordError #:nodoc:
102
+ class HasManyThroughAssociationPolymorphicThroughError < ActiveRecordError # :nodoc:
128
103
  def initialize(owner_class_name = nil, reflection = nil)
129
104
  if owner_class_name && reflection
130
105
  super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' which goes through the polymorphic association '#{owner_class_name}##{reflection.through_reflection.name}'.")
@@ -134,7 +109,7 @@ module ActiveRecord
134
109
  end
135
110
  end
136
111
 
137
- class HasManyThroughAssociationPointlessSourceTypeError < ActiveRecordError #:nodoc:
112
+ class HasManyThroughAssociationPointlessSourceTypeError < ActiveRecordError # :nodoc:
138
113
  def initialize(owner_class_name = nil, reflection = nil, source_reflection = nil)
139
114
  if owner_class_name && reflection && source_reflection
140
115
  super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' with a :source_type option if the '#{reflection.through_reflection.class_name}##{source_reflection.name}' is not polymorphic. Try removing :source_type on your association.")
@@ -144,7 +119,7 @@ module ActiveRecord
144
119
  end
145
120
  end
146
121
 
147
- class HasOneThroughCantAssociateThroughCollection < ActiveRecordError #:nodoc:
122
+ class HasOneThroughCantAssociateThroughCollection < ActiveRecordError # :nodoc:
148
123
  def initialize(owner_class_name = nil, reflection = nil, through_reflection = nil)
149
124
  if owner_class_name && reflection && through_reflection
150
125
  super("Cannot have a has_one :through association '#{owner_class_name}##{reflection.name}' where the :through association '#{owner_class_name}##{through_reflection.name}' is a collection. Specify a has_one or belongs_to association in the :through option instead.")
@@ -154,7 +129,7 @@ module ActiveRecord
154
129
  end
155
130
  end
156
131
 
157
- class HasOneAssociationPolymorphicThroughError < ActiveRecordError #:nodoc:
132
+ class HasOneAssociationPolymorphicThroughError < ActiveRecordError # :nodoc:
158
133
  def initialize(owner_class_name = nil, reflection = nil)
159
134
  if owner_class_name && reflection
160
135
  super("Cannot have a has_one :through association '#{owner_class_name}##{reflection.name}' which goes through the polymorphic association '#{owner_class_name}##{reflection.through_reflection.name}'.")
@@ -164,7 +139,7 @@ module ActiveRecord
164
139
  end
165
140
  end
166
141
 
167
- class HasManyThroughSourceAssociationNotFoundError < ActiveRecordError #:nodoc:
142
+ class HasManyThroughSourceAssociationNotFoundError < ActiveRecordError # :nodoc:
168
143
  def initialize(reflection = nil)
169
144
  if reflection
170
145
  through_reflection = reflection.through_reflection
@@ -177,7 +152,7 @@ module ActiveRecord
177
152
  end
178
153
  end
179
154
 
180
- class HasManyThroughOrderError < ActiveRecordError #:nodoc:
155
+ class HasManyThroughOrderError < ActiveRecordError # :nodoc:
181
156
  def initialize(owner_class_name = nil, reflection = nil, through_reflection = nil)
182
157
  if owner_class_name && reflection && through_reflection
183
158
  super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' which goes through '#{owner_class_name}##{through_reflection.name}' before the through association is defined.")
@@ -187,7 +162,7 @@ module ActiveRecord
187
162
  end
188
163
  end
189
164
 
190
- class ThroughCantAssociateThroughHasOneOrManyReflection < ActiveRecordError #:nodoc:
165
+ class ThroughCantAssociateThroughHasOneOrManyReflection < ActiveRecordError # :nodoc:
191
166
  def initialize(owner = nil, reflection = nil)
192
167
  if owner && reflection
193
168
  super("Cannot modify association '#{owner.class.name}##{reflection.name}' because the source reflection class '#{reflection.source_reflection.class_name}' is associated to '#{reflection.through_reflection.class_name}' via :#{reflection.source_reflection.macro}.")
@@ -212,13 +187,13 @@ module ActiveRecord
212
187
  end
213
188
  end
214
189
 
215
- class HasManyThroughCantAssociateThroughHasOneOrManyReflection < ThroughCantAssociateThroughHasOneOrManyReflection #:nodoc:
190
+ class HasManyThroughCantAssociateThroughHasOneOrManyReflection < ThroughCantAssociateThroughHasOneOrManyReflection # :nodoc:
216
191
  end
217
192
 
218
- class HasOneThroughCantAssociateThroughHasOneOrManyReflection < ThroughCantAssociateThroughHasOneOrManyReflection #:nodoc:
193
+ class HasOneThroughCantAssociateThroughHasOneOrManyReflection < ThroughCantAssociateThroughHasOneOrManyReflection # :nodoc:
219
194
  end
220
195
 
221
- class ThroughNestedAssociationsAreReadonly < ActiveRecordError #:nodoc:
196
+ class ThroughNestedAssociationsAreReadonly < ActiveRecordError # :nodoc:
222
197
  def initialize(owner = nil, reflection = nil)
223
198
  if owner && reflection
224
199
  super("Cannot modify association '#{owner.class.name}##{reflection.name}' because it goes through more than one other association.")
@@ -228,10 +203,10 @@ module ActiveRecord
228
203
  end
229
204
  end
230
205
 
231
- class HasManyThroughNestedAssociationsAreReadonly < ThroughNestedAssociationsAreReadonly #:nodoc:
206
+ class HasManyThroughNestedAssociationsAreReadonly < ThroughNestedAssociationsAreReadonly # :nodoc:
232
207
  end
233
208
 
234
- class HasOneThroughNestedAssociationsAreReadonly < ThroughNestedAssociationsAreReadonly #:nodoc:
209
+ class HasOneThroughNestedAssociationsAreReadonly < ThroughNestedAssociationsAreReadonly # :nodoc:
235
210
  end
236
211
 
237
212
  # This error is raised when trying to eager load a polymorphic association using a JOIN.
@@ -250,7 +225,7 @@ module ActiveRecord
250
225
  # This error is raised when trying to destroy a parent instance in N:1 or 1:1 associations
251
226
  # (has_many, has_one) when there is at least 1 child associated instance.
252
227
  # ex: if @project.tasks.size > 0, DeleteRestrictionError will be raised when trying to destroy @project
253
- class DeleteRestrictionError < ActiveRecordError #:nodoc:
228
+ class DeleteRestrictionError < ActiveRecordError # :nodoc:
254
229
  def initialize(name = nil)
255
230
  if name
256
231
  super("Cannot delete record because of dependent #{name}")
@@ -274,7 +249,7 @@ module ActiveRecord
274
249
  autoload :CollectionProxy
275
250
  autoload :ThroughAssociation
276
251
 
277
- module Builder #:nodoc:
252
+ module Builder # :nodoc:
278
253
  autoload :Association, "active_record/associations/builder/association"
279
254
  autoload :SingularAssociation, "active_record/associations/builder/singular_association"
280
255
  autoload :CollectionAssociation, "active_record/associations/builder/collection_association"
@@ -296,6 +271,7 @@ module ActiveRecord
296
271
  autoload :Preloader
297
272
  autoload :JoinDependency
298
273
  autoload :AssociationScope
274
+ autoload :DisableJoinsAssociationScope
299
275
  autoload :AliasTracker
300
276
  end
301
277
 
@@ -305,7 +281,7 @@ module ActiveRecord
305
281
  end
306
282
 
307
283
  # Returns the association instance for the given name, instantiating it if it doesn't already exist
308
- def association(name) #:nodoc:
284
+ def association(name) # :nodoc:
309
285
  association = association_instance_get(name)
310
286
 
311
287
  if association.nil?
@@ -328,17 +304,7 @@ module ActiveRecord
328
304
  super
329
305
  end
330
306
 
331
- def reload(*) # :nodoc:
332
- clear_association_cache
333
- super
334
- end
335
-
336
307
  private
337
- # Clears out the association cache.
338
- def clear_association_cache
339
- @association_cache.clear if persisted?
340
- end
341
-
342
308
  def init_internals
343
309
  @association_cache = {}
344
310
  super
@@ -398,6 +364,8 @@ module ActiveRecord
398
364
  # create_other(attributes={}) | X | | X
399
365
  # create_other!(attributes={}) | X | | X
400
366
  # reload_other | X | X | X
367
+ # other_changed? | X | X |
368
+ # other_previously_changed? | X | X |
401
369
  #
402
370
  # === Collection associations (one-to-many / many-to-many)
403
371
  # | | | has_many
@@ -1410,6 +1378,11 @@ module ActiveRecord
1410
1378
  # join model. This allows associated records to be built which will automatically create
1411
1379
  # the appropriate join model records when they are saved. (See the 'Association Join Models'
1412
1380
  # section above.)
1381
+ # [:disable_joins]
1382
+ # Specifies whether joins should be skipped for an association. If set to true, two or more queries
1383
+ # will be generated. Note that in some cases, if order or limit is applied, it will be done in-memory
1384
+ # due to database limitations. This option is only applicable on `has_many :through` associations as
1385
+ # `has_many` alone do not perform a join.
1413
1386
  # [:source]
1414
1387
  # Specifies the source association name used by #has_many <tt>:through</tt> queries.
1415
1388
  # Only use it if the name cannot be inferred from the association.
@@ -1439,7 +1412,8 @@ module ActiveRecord
1439
1412
  # Useful for defining methods on associations, especially when they should be shared between multiple
1440
1413
  # association objects.
1441
1414
  # [:strict_loading]
1442
- # Enforces strict loading every time the associated record is loaded through this association.
1415
+ # When set to +true+, enforces strict loading every time the associated record is loaded through this
1416
+ # association.
1443
1417
  # [:ensuring_owner_was]
1444
1418
  # Specifies an instance method to be called on the owner. The method must return true in order for the
1445
1419
  # associated records to be deleted in a background job.
@@ -1453,6 +1427,7 @@ module ActiveRecord
1453
1427
  # has_many :tags, as: :taggable
1454
1428
  # has_many :reports, -> { readonly }
1455
1429
  # has_many :subscribers, through: :subscriptions, source: :user
1430
+ # has_many :subscribers, through: :subscriptions, disable_joins: true
1456
1431
  # has_many :comments, strict_loading: true
1457
1432
  def has_many(name, scope = nil, **options, &extension)
1458
1433
  reflection = Builder::HasMany.build(self, name, scope, options, &extension)
@@ -1558,8 +1533,21 @@ module ActiveRecord
1558
1533
  # source reflection. You can only use a <tt>:through</tt> query through a #has_one
1559
1534
  # or #belongs_to association on the join model.
1560
1535
  #
1536
+ # If the association on the join model is a #belongs_to, the collection can be modified
1537
+ # and the records on the <tt>:through</tt> model will be automatically created and removed
1538
+ # as appropriate. Otherwise, the collection is read-only, so you should manipulate the
1539
+ # <tt>:through</tt> association directly.
1540
+ #
1561
1541
  # If you are going to modify the association (rather than just read from it), then it is
1562
- # a good idea to set the <tt>:inverse_of</tt> option.
1542
+ # a good idea to set the <tt>:inverse_of</tt> option on the source association on the
1543
+ # join model. This allows associated records to be built which will automatically create
1544
+ # the appropriate join model records when they are saved. (See the 'Association Join Models'
1545
+ # section above.)
1546
+ # [:disable_joins]
1547
+ # Specifies whether joins should be skipped for an association. If set to true, two or more queries
1548
+ # will be generated. Note that in some cases, if order or limit is applied, it will be done in-memory
1549
+ # due to database limitations. This option is only applicable on `has_one :through` associations as
1550
+ # `has_one` alone does not perform a join.
1563
1551
  # [:source]
1564
1552
  # Specifies the source association name used by #has_one <tt>:through</tt> queries.
1565
1553
  # Only use it if the name cannot be inferred from the association.
@@ -1601,6 +1589,7 @@ module ActiveRecord
1601
1589
  # has_one :attachment, as: :attachable
1602
1590
  # has_one :boss, -> { readonly }
1603
1591
  # has_one :club, through: :membership
1592
+ # has_one :club, through: :membership, disable_joins: true
1604
1593
  # has_one :primary_address, -> { where(primary: true) }, through: :addressables, source: :addressable
1605
1594
  # has_one :credit_card, required: true
1606
1595
  # has_one :credit_card, strict_loading: true
@@ -1637,6 +1626,10 @@ module ActiveRecord
1637
1626
  # if the record is invalid.
1638
1627
  # [reload_association]
1639
1628
  # Returns the associated object, forcing a database read.
1629
+ # [association_changed?]
1630
+ # Returns true if a new associate object has been assigned and the next save will update the foreign key.
1631
+ # [association_previously_changed?]
1632
+ # Returns true if the previous save updated the association to reference a new associate object.
1640
1633
  #
1641
1634
  # === Example
1642
1635
  #
@@ -1647,6 +1640,8 @@ module ActiveRecord
1647
1640
  # * <tt>Post#create_author</tt> (similar to <tt>post.author = Author.new; post.author.save; post.author</tt>)
1648
1641
  # * <tt>Post#create_author!</tt> (similar to <tt>post.author = Author.new; post.author.save!; post.author</tt>)
1649
1642
  # * <tt>Post#reload_author</tt>
1643
+ # * <tt>Post#author_changed?</tt>
1644
+ # * <tt>Post#author_previously_changed?</tt>
1650
1645
  # The declaration can also include an +options+ hash to specialize the behavior of the association.
1651
1646
  #
1652
1647
  # === Scopes
@@ -1780,7 +1775,7 @@ module ActiveRecord
1780
1775
  # The join table should not have a primary key or a model associated with it. You must manually generate the
1781
1776
  # join table with a migration such as this:
1782
1777
  #
1783
- # class CreateDevelopersProjectsJoinTable < ActiveRecord::Migration[6.0]
1778
+ # class CreateDevelopersProjectsJoinTable < ActiveRecord::Migration[7.0]
1784
1779
  # def change
1785
1780
  # create_join_table :developers, :projects
1786
1781
  # end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ class AsynchronousQueriesTracker # :nodoc:
5
+ module NullSession # :nodoc:
6
+ class << self
7
+ def active?
8
+ true
9
+ end
10
+ end
11
+ end
12
+
13
+ class Session # :nodoc:
14
+ def initialize
15
+ @active = true
16
+ end
17
+
18
+ def active?
19
+ @active
20
+ end
21
+
22
+ def finalize
23
+ @active = false
24
+ end
25
+ end
26
+
27
+ class << self
28
+ def install_executor_hooks(executor = ActiveSupport::Executor)
29
+ executor.register_hook(self)
30
+ end
31
+
32
+ def run
33
+ ActiveRecord::Base.asynchronous_queries_tracker.start_session
34
+ end
35
+
36
+ def complete(asynchronous_queries_tracker)
37
+ asynchronous_queries_tracker.finalize_session
38
+ end
39
+ end
40
+
41
+ attr_reader :current_session
42
+
43
+ def initialize
44
+ @current_session = NullSession
45
+ end
46
+
47
+ def start_session
48
+ @current_session = Session.new
49
+ self
50
+ end
51
+
52
+ def finalize_session
53
+ @current_session.finalize
54
+ @current_session = NullSession
55
+ end
56
+ end
57
+ end
@@ -46,7 +46,7 @@ module ActiveRecord
46
46
  def execute_callstack_for_multiparameter_attributes(callstack)
47
47
  errors = []
48
48
  callstack.each do |name, values_with_empty_parameters|
49
- if values_with_empty_parameters.each_value.all?(&:nil?)
49
+ if values_with_empty_parameters.each_value.all?(NilClass)
50
50
  values = nil
51
51
  else
52
52
  values = values_with_empty_parameters