activerecord 6.1.4.1 → 7.0.2.3

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 (240) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1203 -922
  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 +33 -17
  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 +10 -3
  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 +34 -27
  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/join_dependency.rb +6 -2
  25. data/lib/active_record/associations/preloader/association.rb +187 -55
  26. data/lib/active_record/associations/preloader/batch.rb +48 -0
  27. data/lib/active_record/associations/preloader/branch.rb +147 -0
  28. data/lib/active_record/associations/preloader/through_association.rb +49 -13
  29. data/lib/active_record/associations/preloader.rb +39 -113
  30. data/lib/active_record/associations/singular_association.rb +8 -2
  31. data/lib/active_record/associations/through_association.rb +3 -3
  32. data/lib/active_record/associations.rb +119 -90
  33. data/lib/active_record/asynchronous_queries_tracker.rb +60 -0
  34. data/lib/active_record/attribute_assignment.rb +1 -1
  35. data/lib/active_record/attribute_methods/before_type_cast.rb +7 -2
  36. data/lib/active_record/attribute_methods/dirty.rb +49 -16
  37. data/lib/active_record/attribute_methods/primary_key.rb +2 -2
  38. data/lib/active_record/attribute_methods/query.rb +2 -2
  39. data/lib/active_record/attribute_methods/read.rb +7 -5
  40. data/lib/active_record/attribute_methods/serialization.rb +66 -12
  41. data/lib/active_record/attribute_methods/time_zone_conversion.rb +4 -3
  42. data/lib/active_record/attribute_methods/write.rb +7 -10
  43. data/lib/active_record/attribute_methods.rb +13 -14
  44. data/lib/active_record/attributes.rb +24 -35
  45. data/lib/active_record/autosave_association.rb +8 -23
  46. data/lib/active_record/base.rb +19 -1
  47. data/lib/active_record/callbacks.rb +2 -2
  48. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +292 -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 +47 -561
  52. data/lib/active_record/connection_adapters/abstract/database_limits.rb +0 -17
  53. data/lib/active_record/connection_adapters/abstract/database_statements.rb +46 -22
  54. data/lib/active_record/connection_adapters/abstract/query_cache.rb +24 -12
  55. data/lib/active_record/connection_adapters/abstract/quoting.rb +42 -72
  56. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +4 -17
  57. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +38 -13
  58. data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +14 -1
  59. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +78 -22
  60. data/lib/active_record/connection_adapters/abstract/transaction.rb +15 -22
  61. data/lib/active_record/connection_adapters/abstract_adapter.rb +149 -74
  62. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +97 -81
  63. data/lib/active_record/connection_adapters/column.rb +4 -0
  64. data/lib/active_record/connection_adapters/mysql/database_statements.rb +38 -24
  65. data/lib/active_record/connection_adapters/mysql/quoting.rb +35 -21
  66. data/lib/active_record/connection_adapters/mysql/schema_dumper.rb +7 -1
  67. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +5 -1
  68. data/lib/active_record/connection_adapters/mysql2_adapter.rb +12 -6
  69. data/lib/active_record/connection_adapters/pool_config.rb +7 -7
  70. data/lib/active_record/connection_adapters/postgresql/column.rb +17 -1
  71. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +21 -12
  72. data/lib/active_record/connection_adapters/postgresql/oid/date.rb +8 -0
  73. data/lib/active_record/connection_adapters/postgresql/oid/date_time.rb +5 -0
  74. data/lib/active_record/connection_adapters/postgresql/oid/hstore.rb +53 -14
  75. data/lib/active_record/connection_adapters/postgresql/oid/range.rb +1 -1
  76. data/lib/active_record/connection_adapters/postgresql/oid/timestamp.rb +15 -0
  77. data/lib/active_record/connection_adapters/postgresql/oid/timestamp_with_time_zone.rb +30 -0
  78. data/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +18 -6
  79. data/lib/active_record/connection_adapters/postgresql/oid.rb +2 -0
  80. data/lib/active_record/connection_adapters/postgresql/quoting.rb +50 -50
  81. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +32 -0
  82. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +21 -1
  83. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +22 -1
  84. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +25 -0
  85. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +35 -19
  86. data/lib/active_record/connection_adapters/postgresql_adapter.rb +207 -107
  87. data/lib/active_record/connection_adapters/schema_cache.rb +29 -4
  88. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +27 -19
  89. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +28 -16
  90. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +16 -14
  91. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +89 -30
  92. data/lib/active_record/connection_adapters.rb +6 -5
  93. data/lib/active_record/connection_handling.rb +47 -53
  94. data/lib/active_record/core.rb +122 -132
  95. data/lib/active_record/database_configurations/connection_url_resolver.rb +3 -1
  96. data/lib/active_record/database_configurations/database_config.rb +12 -9
  97. data/lib/active_record/database_configurations/hash_config.rb +63 -5
  98. data/lib/active_record/database_configurations/url_config.rb +2 -2
  99. data/lib/active_record/database_configurations.rb +16 -32
  100. data/lib/active_record/delegated_type.rb +52 -11
  101. data/lib/active_record/destroy_association_async_job.rb +1 -1
  102. data/lib/active_record/disable_joins_association_relation.rb +39 -0
  103. data/lib/active_record/dynamic_matchers.rb +1 -1
  104. data/lib/active_record/encryption/cipher/aes256_gcm.rb +98 -0
  105. data/lib/active_record/encryption/cipher.rb +53 -0
  106. data/lib/active_record/encryption/config.rb +44 -0
  107. data/lib/active_record/encryption/configurable.rb +61 -0
  108. data/lib/active_record/encryption/context.rb +35 -0
  109. data/lib/active_record/encryption/contexts.rb +72 -0
  110. data/lib/active_record/encryption/derived_secret_key_provider.rb +12 -0
  111. data/lib/active_record/encryption/deterministic_key_provider.rb +14 -0
  112. data/lib/active_record/encryption/encryptable_record.rb +208 -0
  113. data/lib/active_record/encryption/encrypted_attribute_type.rb +140 -0
  114. data/lib/active_record/encryption/encrypted_fixtures.rb +38 -0
  115. data/lib/active_record/encryption/encrypting_only_encryptor.rb +12 -0
  116. data/lib/active_record/encryption/encryptor.rb +155 -0
  117. data/lib/active_record/encryption/envelope_encryption_key_provider.rb +55 -0
  118. data/lib/active_record/encryption/errors.rb +15 -0
  119. data/lib/active_record/encryption/extended_deterministic_queries.rb +160 -0
  120. data/lib/active_record/encryption/extended_deterministic_uniqueness_validator.rb +28 -0
  121. data/lib/active_record/encryption/key.rb +28 -0
  122. data/lib/active_record/encryption/key_generator.rb +42 -0
  123. data/lib/active_record/encryption/key_provider.rb +46 -0
  124. data/lib/active_record/encryption/message.rb +33 -0
  125. data/lib/active_record/encryption/message_serializer.rb +90 -0
  126. data/lib/active_record/encryption/null_encryptor.rb +21 -0
  127. data/lib/active_record/encryption/properties.rb +76 -0
  128. data/lib/active_record/encryption/read_only_null_encryptor.rb +24 -0
  129. data/lib/active_record/encryption/scheme.rb +99 -0
  130. data/lib/active_record/encryption.rb +55 -0
  131. data/lib/active_record/enum.rb +49 -42
  132. data/lib/active_record/errors.rb +67 -4
  133. data/lib/active_record/explain_registry.rb +11 -6
  134. data/lib/active_record/fixture_set/file.rb +15 -1
  135. data/lib/active_record/fixture_set/table_row.rb +41 -6
  136. data/lib/active_record/fixture_set/table_rows.rb +4 -4
  137. data/lib/active_record/fixtures.rb +17 -20
  138. data/lib/active_record/future_result.rb +139 -0
  139. data/lib/active_record/gem_version.rb +4 -4
  140. data/lib/active_record/inheritance.rb +55 -17
  141. data/lib/active_record/insert_all.rb +80 -14
  142. data/lib/active_record/integration.rb +4 -3
  143. data/lib/active_record/internal_metadata.rb +3 -5
  144. data/lib/active_record/legacy_yaml_adapter.rb +2 -39
  145. data/lib/active_record/locking/optimistic.rb +10 -9
  146. data/lib/active_record/locking/pessimistic.rb +9 -3
  147. data/lib/active_record/log_subscriber.rb +14 -3
  148. data/lib/active_record/middleware/database_selector/resolver.rb +6 -10
  149. data/lib/active_record/middleware/database_selector.rb +8 -3
  150. data/lib/active_record/middleware/shard_selector.rb +60 -0
  151. data/lib/active_record/migration/command_recorder.rb +4 -4
  152. data/lib/active_record/migration/compatibility.rb +107 -3
  153. data/lib/active_record/migration/join_table.rb +1 -1
  154. data/lib/active_record/migration.rb +109 -79
  155. data/lib/active_record/model_schema.rb +45 -58
  156. data/lib/active_record/nested_attributes.rb +13 -12
  157. data/lib/active_record/no_touching.rb +3 -3
  158. data/lib/active_record/null_relation.rb +2 -6
  159. data/lib/active_record/persistence.rb +219 -52
  160. data/lib/active_record/query_cache.rb +2 -2
  161. data/lib/active_record/query_logs.rb +138 -0
  162. data/lib/active_record/querying.rb +15 -5
  163. data/lib/active_record/railtie.rb +127 -17
  164. data/lib/active_record/railties/controller_runtime.rb +1 -1
  165. data/lib/active_record/railties/databases.rake +66 -129
  166. data/lib/active_record/readonly_attributes.rb +11 -0
  167. data/lib/active_record/reflection.rb +67 -50
  168. data/lib/active_record/relation/batches/batch_enumerator.rb +19 -5
  169. data/lib/active_record/relation/batches.rb +3 -3
  170. data/lib/active_record/relation/calculations.rb +43 -38
  171. data/lib/active_record/relation/delegation.rb +7 -7
  172. data/lib/active_record/relation/finder_methods.rb +31 -35
  173. data/lib/active_record/relation/merger.rb +20 -13
  174. data/lib/active_record/relation/predicate_builder.rb +1 -6
  175. data/lib/active_record/relation/query_attribute.rb +5 -11
  176. data/lib/active_record/relation/query_methods.rb +249 -61
  177. data/lib/active_record/relation/record_fetch_warning.rb +7 -9
  178. data/lib/active_record/relation/spawn_methods.rb +2 -2
  179. data/lib/active_record/relation/where_clause.rb +10 -19
  180. data/lib/active_record/relation.rb +184 -84
  181. data/lib/active_record/result.rb +17 -7
  182. data/lib/active_record/runtime_registry.rb +9 -13
  183. data/lib/active_record/sanitization.rb +11 -7
  184. data/lib/active_record/schema.rb +38 -23
  185. data/lib/active_record/schema_dumper.rb +25 -19
  186. data/lib/active_record/schema_migration.rb +4 -4
  187. data/lib/active_record/scoping/default.rb +61 -12
  188. data/lib/active_record/scoping/named.rb +3 -11
  189. data/lib/active_record/scoping.rb +64 -34
  190. data/lib/active_record/serialization.rb +1 -1
  191. data/lib/active_record/signed_id.rb +1 -1
  192. data/lib/active_record/suppressor.rb +11 -15
  193. data/lib/active_record/tasks/database_tasks.rb +120 -58
  194. data/lib/active_record/tasks/mysql_database_tasks.rb +1 -1
  195. data/lib/active_record/tasks/postgresql_database_tasks.rb +19 -12
  196. data/lib/active_record/test_databases.rb +1 -1
  197. data/lib/active_record/test_fixtures.rb +4 -4
  198. data/lib/active_record/timestamp.rb +3 -4
  199. data/lib/active_record/transactions.rb +9 -14
  200. data/lib/active_record/translation.rb +2 -2
  201. data/lib/active_record/type/adapter_specific_registry.rb +32 -7
  202. data/lib/active_record/type/hash_lookup_type_map.rb +34 -1
  203. data/lib/active_record/type/internal/timezone.rb +2 -2
  204. data/lib/active_record/type/serialized.rb +1 -1
  205. data/lib/active_record/type/type_map.rb +17 -20
  206. data/lib/active_record/type.rb +1 -2
  207. data/lib/active_record/validations/associated.rb +1 -1
  208. data/lib/active_record/validations/uniqueness.rb +1 -1
  209. data/lib/active_record.rb +204 -28
  210. data/lib/arel/attributes/attribute.rb +0 -8
  211. data/lib/arel/crud.rb +28 -22
  212. data/lib/arel/delete_manager.rb +18 -4
  213. data/lib/arel/filter_predications.rb +9 -0
  214. data/lib/arel/insert_manager.rb +2 -3
  215. data/lib/arel/nodes/casted.rb +1 -1
  216. data/lib/arel/nodes/delete_statement.rb +12 -13
  217. data/lib/arel/nodes/filter.rb +10 -0
  218. data/lib/arel/nodes/function.rb +1 -0
  219. data/lib/arel/nodes/insert_statement.rb +2 -2
  220. data/lib/arel/nodes/select_core.rb +2 -2
  221. data/lib/arel/nodes/select_statement.rb +2 -2
  222. data/lib/arel/nodes/update_statement.rb +8 -3
  223. data/lib/arel/nodes.rb +1 -0
  224. data/lib/arel/predications.rb +11 -3
  225. data/lib/arel/select_manager.rb +10 -4
  226. data/lib/arel/table.rb +0 -1
  227. data/lib/arel/tree_manager.rb +0 -12
  228. data/lib/arel/update_manager.rb +18 -4
  229. data/lib/arel/visitors/dot.rb +80 -90
  230. data/lib/arel/visitors/mysql.rb +8 -2
  231. data/lib/arel/visitors/postgresql.rb +0 -10
  232. data/lib/arel/visitors/to_sql.rb +58 -2
  233. data/lib/arel.rb +2 -1
  234. data/lib/rails/generators/active_record/application_record/templates/application_record.rb.tt +1 -1
  235. data/lib/rails/generators/active_record/model/templates/abstract_base_class.rb.tt +1 -1
  236. data/lib/rails/generators/active_record/model/templates/model.rb.tt +1 -1
  237. data/lib/rails/generators/active_record/model/templates/module.rb.tt +2 -2
  238. data/lib/rails/generators/active_record/multi_db/multi_db_generator.rb +16 -0
  239. data/lib/rails/generators/active_record/multi_db/templates/multi_db.rb.tt +44 -0
  240. metadata +56 -11
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Associations
5
+ class DisableJoinsAssociationScope < AssociationScope # :nodoc:
6
+ def scope(association)
7
+ source_reflection = association.reflection
8
+ owner = association.owner
9
+ unscoped = association.klass.unscoped
10
+ reverse_chain = get_chain(source_reflection, association, unscoped.alias_tracker).reverse
11
+
12
+ last_reflection, last_ordered, last_join_ids = last_scope_chain(reverse_chain, owner)
13
+
14
+ add_constraints(last_reflection, last_reflection.join_primary_key, last_join_ids, owner, last_ordered)
15
+ end
16
+
17
+ private
18
+ def last_scope_chain(reverse_chain, owner)
19
+ first_item = reverse_chain.shift
20
+ first_scope = [first_item, false, [owner._read_attribute(first_item.join_foreign_key)]]
21
+
22
+ reverse_chain.inject(first_scope) do |(reflection, ordered, join_ids), next_reflection|
23
+ key = reflection.join_primary_key
24
+ records = add_constraints(reflection, key, join_ids, owner, ordered)
25
+ foreign_key = next_reflection.join_foreign_key
26
+ record_ids = records.pluck(foreign_key)
27
+ records_ordered = records && records.order_values.any?
28
+
29
+ [next_reflection, records_ordered, record_ids]
30
+ end
31
+ end
32
+
33
+ def add_constraints(reflection, key, join_ids, owner, ordered)
34
+ scope = reflection.build_scope(reflection.aliased_table).where(key => join_ids)
35
+
36
+ relation = reflection.klass.scope_for_association
37
+ scope.merge!(
38
+ relation.except(:select, :create_with, :includes, :preload, :eager_load, :joins, :left_outer_joins)
39
+ )
40
+
41
+ scope = reflection.constraints.inject(scope) do |memo, scope_chain_item|
42
+ item = eval_scope(reflection, scope_chain_item, owner)
43
+ scope.unscope!(*item.unscope_values)
44
+ scope.where_clause += item.where_clause
45
+ scope.order_values = item.order_values | scope.order_values
46
+ scope
47
+ end
48
+
49
+ if scope.order_values.empty? && ordered
50
+ split_scope = DisableJoinsAssociationRelation.create(scope.klass, key, join_ids)
51
+ split_scope.where_clause += scope.where_clause
52
+ split_scope
53
+ else
54
+ scope
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -7,7 +7,7 @@ module ActiveRecord
7
7
  #
8
8
  # If the association has a <tt>:through</tt> option further specialization
9
9
  # is provided by its child HasManyThroughAssociation.
10
- class HasManyAssociation < CollectionAssociation #:nodoc:
10
+ class HasManyAssociation < CollectionAssociation # :nodoc:
11
11
  include ForeignAssociation
12
12
 
13
13
  def handle_dependency
@@ -3,7 +3,7 @@
3
3
  module ActiveRecord
4
4
  module Associations
5
5
  # = Active Record Has Many Through Association
6
- class HasManyThroughAssociation < HasManyAssociation #:nodoc:
6
+ class HasManyThroughAssociation < HasManyAssociation # :nodoc:
7
7
  include ThroughAssociation
8
8
 
9
9
  def initialize(owner, reflection)
@@ -214,6 +214,7 @@ module ActiveRecord
214
214
 
215
215
  def find_target
216
216
  return [] unless target_reflection_has_associated_record?
217
+ return scope.to_a if disable_joins
217
218
  super
218
219
  end
219
220
 
@@ -3,7 +3,7 @@
3
3
  module ActiveRecord
4
4
  module Associations
5
5
  # = Active Record Has One Association
6
- class HasOneAssociation < SingularAssociation #:nodoc:
6
+ class HasOneAssociation < SingularAssociation # :nodoc:
7
7
  include ForeignAssociation
8
8
 
9
9
  def handle_dependency
@@ -70,7 +70,7 @@ module ActiveRecord
70
70
  if save && !record.save
71
71
  nullify_owner_attributes(record)
72
72
  set_owner_attributes(target) if target
73
- raise RecordNotSaved, "Failed to save the new associated #{reflection.name}."
73
+ raise RecordNotSaved.new("Failed to save the new associated #{reflection.name}.", record)
74
74
  end
75
75
  end
76
76
  end
@@ -102,8 +102,11 @@ module ActiveRecord
102
102
 
103
103
  if target.persisted? && owner.persisted? && !target.save
104
104
  set_owner_attributes(target)
105
- raise RecordNotSaved, "Failed to remove the existing associated #{reflection.name}. " \
106
- "The record failed to save after its foreign key was set to nil."
105
+ raise RecordNotSaved.new(
106
+ "Failed to remove the existing associated #{reflection.name}. " \
107
+ "The record failed to save after its foreign key was set to nil.",
108
+ target
109
+ )
107
110
  end
108
111
  end
109
112
  end
@@ -112,9 +115,9 @@ module ActiveRecord
112
115
  record[reflection.foreign_key] = nil
113
116
  end
114
117
 
115
- def transaction_if(value)
118
+ def transaction_if(value, &block)
116
119
  if value
117
- reflection.klass.transaction { yield }
120
+ reflection.klass.transaction(&block)
118
121
  else
119
122
  yield
120
123
  end
@@ -122,7 +125,7 @@ module ActiveRecord
122
125
 
123
126
  def _create_record(attributes, raise_error = false, &block)
124
127
  unless owner.persisted?
125
- raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved"
128
+ raise ActiveRecord::RecordNotSaved.new("You cannot call create unless the parent is saved", owner)
126
129
  end
127
130
 
128
131
  super
@@ -3,7 +3,7 @@
3
3
  module ActiveRecord
4
4
  module Associations
5
5
  # = Active Record Has One Through Association
6
- class HasOneThroughAssociation < HasOneAssociation #:nodoc:
6
+ class HasOneThroughAssociation < HasOneAssociation # :nodoc:
7
7
  include ThroughAssociation
8
8
 
9
9
  private
@@ -3,8 +3,12 @@
3
3
  module ActiveRecord
4
4
  module Associations
5
5
  class JoinDependency # :nodoc:
6
- autoload :JoinBase, "active_record/associations/join_dependency/join_base"
7
- autoload :JoinAssociation, "active_record/associations/join_dependency/join_association"
6
+ extend ActiveSupport::Autoload
7
+
8
+ eager_autoload do
9
+ autoload :JoinBase
10
+ autoload :JoinAssociation
11
+ end
8
12
 
9
13
  class Aliases # :nodoc:
10
14
  def initialize(tables)
@@ -3,17 +3,122 @@
3
3
  module ActiveRecord
4
4
  module Associations
5
5
  class Preloader
6
- class Association #:nodoc:
7
- def initialize(klass, owners, reflection, preload_scope, associate_by_default = true)
6
+ class Association # :nodoc:
7
+ class LoaderQuery
8
+ attr_reader :scope, :association_key_name
9
+
10
+ def initialize(scope, association_key_name)
11
+ @scope = scope
12
+ @association_key_name = association_key_name
13
+ end
14
+
15
+ def eql?(other)
16
+ association_key_name == other.association_key_name &&
17
+ scope.table_name == other.scope.table_name &&
18
+ scope.values_for_queries == other.scope.values_for_queries
19
+ end
20
+
21
+ def hash
22
+ [association_key_name, scope.table_name, scope.values_for_queries].hash
23
+ end
24
+
25
+ def records_for(loaders)
26
+ LoaderRecords.new(loaders, self).records
27
+ end
28
+
29
+ def load_records_in_batch(loaders)
30
+ raw_records = records_for(loaders)
31
+
32
+ loaders.each do |loader|
33
+ loader.load_records(raw_records)
34
+ loader.run
35
+ end
36
+ end
37
+
38
+ def load_records_for_keys(keys, &block)
39
+ scope.where(association_key_name => keys).load(&block)
40
+ end
41
+ end
42
+
43
+ class LoaderRecords
44
+ def initialize(loaders, loader_query)
45
+ @loader_query = loader_query
46
+ @loaders = loaders
47
+ @keys_to_load = Set.new
48
+ @already_loaded_records_by_key = {}
49
+
50
+ populate_keys_to_load_and_already_loaded_records
51
+ end
52
+
53
+ def records
54
+ load_records + already_loaded_records
55
+ end
56
+
57
+ private
58
+ attr_reader :loader_query, :loaders, :keys_to_load, :already_loaded_records_by_key
59
+
60
+ def populate_keys_to_load_and_already_loaded_records
61
+ loaders.each do |loader|
62
+ loader.owners_by_key.each do |key, owners|
63
+ if loaded_owner = owners.find { |owner| loader.loaded?(owner) }
64
+ already_loaded_records_by_key[key] = loader.target_for(loaded_owner)
65
+ else
66
+ keys_to_load << key
67
+ end
68
+ end
69
+ end
70
+
71
+ @keys_to_load.subtract(already_loaded_records_by_key.keys)
72
+ end
73
+
74
+ def load_records
75
+ loader_query.load_records_for_keys(keys_to_load) do |record|
76
+ loaders.each { |l| l.set_inverse(record) }
77
+ end
78
+ end
79
+
80
+ def already_loaded_records
81
+ already_loaded_records_by_key.values.flatten
82
+ end
83
+ end
84
+
85
+ attr_reader :klass
86
+
87
+ def initialize(klass, owners, reflection, preload_scope, reflection_scope, associate_by_default)
8
88
  @klass = klass
9
89
  @owners = owners.uniq(&:__id__)
10
90
  @reflection = reflection
11
91
  @preload_scope = preload_scope
92
+ @reflection_scope = reflection_scope
12
93
  @associate = associate_by_default || !preload_scope || preload_scope.empty_scope?
13
94
  @model = owners.first && owners.first.class
95
+ @run = false
96
+ end
97
+
98
+ def table_name
99
+ @klass.table_name
100
+ end
101
+
102
+ def future_classes
103
+ if run?
104
+ []
105
+ else
106
+ [@klass]
107
+ end
108
+ end
109
+
110
+ def runnable_loaders
111
+ [self]
112
+ end
113
+
114
+ def run?
115
+ @run
14
116
  end
15
117
 
16
118
  def run
119
+ return self if run?
120
+ @run = true
121
+
17
122
  records = records_by_owner
18
123
 
19
124
  owners.each do |owner|
@@ -35,35 +140,86 @@ module ActiveRecord
35
140
  @preloaded_records
36
141
  end
37
142
 
38
- private
39
- attr_reader :owners, :reflection, :preload_scope, :model, :klass
143
+ # The name of the key on the associated records
144
+ def association_key_name
145
+ reflection.join_primary_key(klass)
146
+ end
40
147
 
41
- def load_records
42
- # owners can be duplicated when a relation has a collection association join
43
- # #compare_by_identity makes such owners different hash keys
44
- @records_by_owner = {}.compare_by_identity
45
- raw_records = owner_keys.empty? ? [] : records_for(owner_keys)
148
+ def loader_query
149
+ LoaderQuery.new(scope, association_key_name)
150
+ end
46
151
 
47
- @preloaded_records = raw_records.select do |record|
48
- assignments = false
152
+ def owners_by_key
153
+ @owners_by_key ||= owners.each_with_object({}) do |owner, result|
154
+ key = convert_key(owner[owner_key_name])
155
+ (result[key] ||= []) << owner if key
156
+ end
157
+ end
49
158
 
50
- owners_by_key[convert_key(record[association_key_name])].each do |owner|
51
- entries = (@records_by_owner[owner] ||= [])
159
+ def loaded?(owner)
160
+ owner.association(reflection.name).loaded?
161
+ end
52
162
 
53
- if reflection.collection? || entries.empty?
54
- entries << record
55
- assignments = true
56
- end
57
- end
163
+ def target_for(owner)
164
+ Array.wrap(owner.association(reflection.name).target)
165
+ end
166
+
167
+ def scope
168
+ @scope ||= build_scope
169
+ end
170
+
171
+ def set_inverse(record)
172
+ if owners = owners_by_key[convert_key(record[association_key_name])]
173
+ # Processing only the first owner
174
+ # because the record is modified but not an owner
175
+ association = owners.first.association(reflection.name)
176
+ association.set_inverse_instance(record)
177
+ end
178
+ end
179
+
180
+ def load_records(raw_records = nil)
181
+ # owners can be duplicated when a relation has a collection association join
182
+ # #compare_by_identity makes such owners different hash keys
183
+ @records_by_owner = {}.compare_by_identity
184
+ raw_records ||= loader_query.records_for([self])
185
+
186
+ @preloaded_records = raw_records.select do |record|
187
+ assignments = false
58
188
 
59
- assignments
189
+ owners_by_key[convert_key(record[association_key_name])]&.each do |owner|
190
+ entries = (@records_by_owner[owner] ||= [])
191
+
192
+ if reflection.collection? || entries.empty?
193
+ entries << record
194
+ assignments = true
195
+ end
60
196
  end
197
+
198
+ assignments
61
199
  end
200
+ end
201
+
202
+ def associate_records_from_unscoped(unscoped_records)
203
+ return if unscoped_records.nil? || unscoped_records.empty?
204
+ return if !reflection_scope.empty_scope?
205
+ return if preload_scope && !preload_scope.empty_scope?
206
+ return if reflection.collection?
62
207
 
63
- # The name of the key on the associated records
64
- def association_key_name
65
- reflection.join_primary_key(klass)
208
+ unscoped_records.select { |r| r[association_key_name].present? }.each do |record|
209
+ owners = owners_by_key[convert_key(record[association_key_name])]
210
+ owners&.each_with_index do |owner, i|
211
+ association = owner.association(reflection.name)
212
+ association.target = record
213
+
214
+ if i == 0 # Set inverse on first owner
215
+ association.set_inverse_instance(record)
216
+ end
217
+ end
66
218
  end
219
+ end
220
+
221
+ private
222
+ attr_reader :owners, :reflection, :preload_scope, :model
67
223
 
68
224
  # The name of the key on the model which declares the association
69
225
  def owner_key_name
@@ -71,7 +227,10 @@ module ActiveRecord
71
227
  end
72
228
 
73
229
  def associate_records_to_owner(owner, records)
230
+ return if loaded?(owner)
231
+
74
232
  association = owner.association(reflection.name)
233
+
75
234
  if reflection.collection?
76
235
  association.target = records
77
236
  else
@@ -79,17 +238,6 @@ module ActiveRecord
79
238
  end
80
239
  end
81
240
 
82
- def owner_keys
83
- @owner_keys ||= owners_by_key.keys
84
- end
85
-
86
- def owners_by_key
87
- @owners_by_key ||= owners.each_with_object({}) do |owner, result|
88
- key = convert_key(owner[owner_key_name])
89
- (result[key] ||= []) << owner if key
90
- end
91
- end
92
-
93
241
  def key_conversion_required?
94
242
  unless defined?(@key_conversion_required)
95
243
  @key_conversion_required = (association_key_type != owner_key_type)
@@ -114,24 +262,8 @@ module ActiveRecord
114
262
  @model.type_for_attribute(owner_key_name).type
115
263
  end
116
264
 
117
- def records_for(ids)
118
- scope.where(association_key_name => ids).load do |record|
119
- # Processing only the first owner
120
- # because the record is modified but not an owner
121
- owner = owners_by_key[convert_key(record[association_key_name])].first
122
- association = owner.association(reflection.name)
123
- association.set_inverse_instance(record)
124
- end
125
- end
126
-
127
- def scope
128
- @scope ||= build_scope
129
- end
130
-
131
265
  def reflection_scope
132
- @reflection_scope ||= begin
133
- reflection.join_scopes(klass.arel_table, klass.predicate_builder, klass).inject(&:merge!) || klass.unscoped
134
- end
266
+ @reflection_scope ||= reflection.join_scopes(klass.arel_table, klass.predicate_builder, klass).inject(klass.unscoped, &:merge!)
135
267
  end
136
268
 
137
269
  def build_scope
@@ -147,11 +279,11 @@ module ActiveRecord
147
279
  scope.merge!(preload_scope)
148
280
  end
149
281
 
150
- if preload_scope && preload_scope.strict_loading_value
151
- scope.strict_loading
152
- else
153
- scope
154
- end
282
+ cascade_strict_loading(scope)
283
+ end
284
+
285
+ def cascade_strict_loading(scope)
286
+ preload_scope&.strict_loading_value ? scope.strict_loading : scope
155
287
  end
156
288
  end
157
289
  end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Associations
5
+ class Preloader
6
+ class Batch # :nodoc:
7
+ def initialize(preloaders, available_records:)
8
+ @preloaders = preloaders.reject(&:empty?)
9
+ @available_records = available_records.flatten.group_by { |r| r.class.base_class }
10
+ end
11
+
12
+ def call
13
+ branches = @preloaders.flat_map(&:branches)
14
+ until branches.empty?
15
+ loaders = branches.flat_map(&:runnable_loaders)
16
+
17
+ loaders.each { |loader| loader.associate_records_from_unscoped(@available_records[loader.klass.base_class]) }
18
+
19
+ if loaders.any?
20
+ future_tables = branches.flat_map do |branch|
21
+ branch.future_classes - branch.runnable_loaders.map(&:klass)
22
+ end.map(&:table_name).uniq
23
+
24
+ target_loaders = loaders.reject { |l| future_tables.include?(l.table_name) }
25
+ target_loaders = loaders if target_loaders.empty?
26
+
27
+ group_and_load_similar(target_loaders)
28
+ target_loaders.each(&:run)
29
+ end
30
+
31
+ finished, in_progress = branches.partition(&:done?)
32
+
33
+ branches = in_progress + finished.flat_map(&:children)
34
+ end
35
+ end
36
+
37
+ private
38
+ attr_reader :loaders
39
+
40
+ def group_and_load_similar(loaders)
41
+ loaders.grep_v(ThroughAssociation).group_by(&:loader_query).each_pair do |query, similar_loaders|
42
+ query.load_records_in_batch(similar_loaders)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Associations
5
+ class Preloader
6
+ class Branch # :nodoc:
7
+ attr_reader :association, :children, :parent
8
+ attr_reader :scope, :associate_by_default
9
+ attr_writer :preloaded_records
10
+
11
+ def initialize(association:, children:, parent:, associate_by_default:, scope:)
12
+ @association = association
13
+ @parent = parent
14
+ @scope = scope
15
+ @associate_by_default = associate_by_default
16
+
17
+ @children = build_children(children)
18
+ @loaders = nil
19
+ end
20
+
21
+ def future_classes
22
+ (immediate_future_classes + children.flat_map(&:future_classes)).uniq
23
+ end
24
+
25
+ def immediate_future_classes
26
+ if parent.done?
27
+ loaders.flat_map(&:future_classes).uniq
28
+ else
29
+ likely_reflections.reject(&:polymorphic?).flat_map do |reflection|
30
+ reflection.
31
+ chain.
32
+ map(&:klass)
33
+ end.uniq
34
+ end
35
+ end
36
+
37
+ def target_classes
38
+ if done?
39
+ preloaded_records.map(&:klass).uniq
40
+ elsif parent.done?
41
+ loaders.map(&:klass).uniq
42
+ else
43
+ likely_reflections.reject(&:polymorphic?).map(&:klass).uniq
44
+ end
45
+ end
46
+
47
+ def likely_reflections
48
+ parent_classes = parent.target_classes
49
+ parent_classes.filter_map do |parent_klass|
50
+ parent_klass._reflect_on_association(@association)
51
+ end
52
+ end
53
+
54
+ def root?
55
+ parent.nil?
56
+ end
57
+
58
+ def source_records
59
+ @parent.preloaded_records
60
+ end
61
+
62
+ def preloaded_records
63
+ @preloaded_records ||= loaders.flat_map(&:preloaded_records)
64
+ end
65
+
66
+ def done?
67
+ root? || (@loaders && @loaders.all?(&:run?))
68
+ end
69
+
70
+ def runnable_loaders
71
+ loaders.flat_map(&:runnable_loaders).reject(&:run?)
72
+ end
73
+
74
+ def grouped_records
75
+ h = {}
76
+ polymorphic_parent = !root? && parent.polymorphic?
77
+ source_records.each do |record|
78
+ reflection = record.class._reflect_on_association(association)
79
+ next if polymorphic_parent && !reflection || !record.association(association).klass
80
+ (h[reflection] ||= []) << record
81
+ end
82
+ h
83
+ end
84
+
85
+ def preloaders_for_reflection(reflection, reflection_records)
86
+ reflection_records.group_by do |record|
87
+ klass = record.association(association).klass
88
+
89
+ if reflection.scope && reflection.scope.arity != 0
90
+ # For instance dependent scopes, the scope is potentially
91
+ # different for each record. To allow this we'll group each
92
+ # object separately into its own preloader
93
+ reflection_scope = reflection.join_scopes(klass.arel_table, klass.predicate_builder, klass, record).inject(&:merge!)
94
+ end
95
+
96
+ [klass, reflection_scope]
97
+ end.map do |(rhs_klass, reflection_scope), rs|
98
+ preloader_for(reflection).new(rhs_klass, rs, reflection, scope, reflection_scope, associate_by_default)
99
+ end
100
+ end
101
+
102
+ def polymorphic?
103
+ return false if root?
104
+ return @polymorphic if defined?(@polymorphic)
105
+
106
+ @polymorphic = source_records.any? do |record|
107
+ reflection = record.class._reflect_on_association(association)
108
+ reflection && reflection.options[:polymorphic]
109
+ end
110
+ end
111
+
112
+ def loaders
113
+ @loaders ||=
114
+ grouped_records.flat_map do |reflection, reflection_records|
115
+ preloaders_for_reflection(reflection, reflection_records)
116
+ end
117
+ end
118
+
119
+ private
120
+ def build_children(children)
121
+ Array.wrap(children).flat_map { |association|
122
+ Array(association).flat_map { |parent, child|
123
+ Branch.new(
124
+ parent: self,
125
+ association: parent,
126
+ children: child,
127
+ associate_by_default: associate_by_default,
128
+ scope: scope
129
+ )
130
+ }
131
+ }
132
+ end
133
+
134
+ # Returns a class containing the logic needed to load preload the data
135
+ # and attach it to a relation. The class returned implements a `run` method
136
+ # that accepts a preloader.
137
+ def preloader_for(reflection)
138
+ if reflection.options[:through]
139
+ ThroughAssociation
140
+ else
141
+ Association
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end