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
@@ -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,17 +3,89 @@
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
+ ids = loaders.flat_map(&:owner_keys).uniq
27
+
28
+ scope.where(association_key_name => ids).load do |record|
29
+ loaders.each { |l| l.set_inverse(record) }
30
+ end
31
+ end
32
+
33
+ def load_records_in_batch(loaders)
34
+ raw_records = records_for(loaders)
35
+
36
+ loaders.each do |loader|
37
+ loader.load_records(raw_records)
38
+ loader.run
39
+ end
40
+ end
41
+ end
42
+
43
+ attr_reader :klass
44
+
45
+ def initialize(klass, owners, reflection, preload_scope, reflection_scope, associate_by_default)
8
46
  @klass = klass
9
47
  @owners = owners.uniq(&:__id__)
10
48
  @reflection = reflection
11
49
  @preload_scope = preload_scope
50
+ @reflection_scope = reflection_scope
12
51
  @associate = associate_by_default || !preload_scope || preload_scope.empty_scope?
13
52
  @model = owners.first && owners.first.class
53
+ @run = false
54
+ end
55
+
56
+ def table_name
57
+ @klass.table_name
58
+ end
59
+
60
+ def data_available?
61
+ already_loaded?
62
+ end
63
+
64
+ def future_classes
65
+ if run? || already_loaded?
66
+ []
67
+ else
68
+ [@klass]
69
+ end
70
+ end
71
+
72
+ def runnable_loaders
73
+ [self]
74
+ end
75
+
76
+ def run?
77
+ @run
14
78
  end
15
79
 
16
80
  def run
81
+ return self if run?
82
+ @run = true
83
+
84
+ if already_loaded?
85
+ fetch_from_preloaded_records
86
+ return self
87
+ end
88
+
17
89
  records = records_by_owner
18
90
 
19
91
  owners.each do |owner|
@@ -24,45 +96,105 @@ module ActiveRecord
24
96
  end
25
97
 
26
98
  def records_by_owner
27
- load_records unless defined?(@records_by_owner)
99
+ ensure_loaded unless defined?(@records_by_owner)
28
100
 
29
101
  @records_by_owner
30
102
  end
31
103
 
32
104
  def preloaded_records
33
- load_records unless defined?(@preloaded_records)
105
+ ensure_loaded unless defined?(@preloaded_records)
34
106
 
35
107
  @preloaded_records
36
108
  end
37
109
 
38
- private
39
- attr_reader :owners, :reflection, :preload_scope, :model, :klass
110
+ def ensure_loaded
111
+ if already_loaded?
112
+ fetch_from_preloaded_records
113
+ else
114
+ load_records
115
+ end
116
+ end
117
+
118
+ # The name of the key on the associated records
119
+ def association_key_name
120
+ reflection.join_primary_key(klass)
121
+ end
40
122
 
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)
123
+ def loader_query
124
+ LoaderQuery.new(scope, association_key_name)
125
+ end
126
+
127
+ def owner_keys
128
+ @owner_keys ||= owners_by_key.keys
129
+ end
130
+
131
+ def scope
132
+ @scope ||= build_scope
133
+ end
46
134
 
47
- @preloaded_records = raw_records.select do |record|
48
- assignments = false
135
+ def set_inverse(record)
136
+ if owners = owners_by_key[convert_key(record[association_key_name])]
137
+ # Processing only the first owner
138
+ # because the record is modified but not an owner
139
+ association = owners.first.association(reflection.name)
140
+ association.set_inverse_instance(record)
141
+ end
142
+ end
143
+
144
+ def load_records(raw_records = nil)
145
+ # owners can be duplicated when a relation has a collection association join
146
+ # #compare_by_identity makes such owners different hash keys
147
+ @records_by_owner = {}.compare_by_identity
148
+ raw_records ||= loader_query.records_for([self])
49
149
 
50
- owners_by_key[convert_key(record[association_key_name])].each do |owner|
51
- entries = (@records_by_owner[owner] ||= [])
150
+ @preloaded_records = raw_records.select do |record|
151
+ assignments = false
52
152
 
53
- if reflection.collection? || entries.empty?
54
- entries << record
55
- assignments = true
56
- end
153
+ owners_by_key[convert_key(record[association_key_name])]&.each do |owner|
154
+ entries = (@records_by_owner[owner] ||= [])
155
+
156
+ if reflection.collection? || entries.empty?
157
+ entries << record
158
+ assignments = true
57
159
  end
160
+ end
58
161
 
59
- assignments
162
+ assignments
163
+ end
164
+ end
165
+
166
+ def associate_records_from_unscoped(unscoped_records)
167
+ return if unscoped_records.nil? || unscoped_records.empty?
168
+ return if !reflection_scope.empty_scope?
169
+ return if preload_scope && !preload_scope.empty_scope?
170
+ return if reflection.collection?
171
+
172
+ unscoped_records.each do |record|
173
+ owners = owners_by_key[convert_key(record[association_key_name])]
174
+ owners&.each_with_index do |owner, i|
175
+ association = owner.association(reflection.name)
176
+ association.target = record
177
+
178
+ if i == 0 # Set inverse on first owner
179
+ association.set_inverse_instance(record)
180
+ end
60
181
  end
61
182
  end
183
+ end
62
184
 
63
- # The name of the key on the associated records
64
- def association_key_name
65
- reflection.join_primary_key(klass)
185
+ private
186
+ attr_reader :owners, :reflection, :preload_scope, :model
187
+
188
+ def already_loaded?
189
+ @already_loaded ||= owners.all? { |o| o.association(reflection.name).loaded? }
190
+ end
191
+
192
+ def fetch_from_preloaded_records
193
+ @records_by_owner = owners.index_with do |owner|
194
+ Array(owner.association(reflection.name).target)
195
+ end
196
+
197
+ @preloaded_records = records_by_owner.flat_map(&:last)
66
198
  end
67
199
 
68
200
  # The name of the key on the model which declares the association
@@ -79,10 +211,6 @@ module ActiveRecord
79
211
  end
80
212
  end
81
213
 
82
- def owner_keys
83
- @owner_keys ||= owners_by_key.keys
84
- end
85
-
86
214
  def owners_by_key
87
215
  @owners_by_key ||= owners.each_with_object({}) do |owner, result|
88
216
  key = convert_key(owner[owner_key_name])
@@ -114,22 +242,8 @@ module ActiveRecord
114
242
  @model.type_for_attribute(owner_key_name).type
115
243
  end
116
244
 
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
245
  def reflection_scope
132
- @reflection_scope ||= reflection.join_scopes(klass.arel_table, klass.predicate_builder, klass).inject(klass.unscoped, &:merge!)
246
+ @reflection_scope ||= reflection.join_scopes(klass.arel_table, klass.predicate_builder, klass).inject(&:merge!) || klass.unscoped
133
247
  end
134
248
 
135
249
  def build_scope
@@ -145,11 +259,11 @@ module ActiveRecord
145
259
  scope.merge!(preload_scope)
146
260
  end
147
261
 
148
- if preload_scope && preload_scope.strict_loading_value
149
- scope.strict_loading
150
- else
151
- scope
152
- end
262
+ cascade_strict_loading(scope)
263
+ end
264
+
265
+ def cascade_strict_loading(scope)
266
+ preload_scope&.strict_loading_value ? scope.strict_loading : scope
153
267
  end
154
268
  end
155
269
  end
@@ -0,0 +1,51 @@
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(&: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]) }
18
+
19
+ already_loaded = loaders.select(&:data_available?)
20
+ if already_loaded.any?
21
+ already_loaded.each(&:run)
22
+ elsif loaders.any?
23
+ future_tables = branches.flat_map do |branch|
24
+ branch.future_classes - branch.runnable_loaders.map(&:klass)
25
+ end.map(&:table_name).uniq
26
+
27
+ target_loaders = loaders.reject { |l| future_tables.include?(l.table_name) }
28
+ target_loaders = loaders if target_loaders.empty?
29
+
30
+ group_and_load_similar(target_loaders)
31
+ target_loaders.each(&:run)
32
+ end
33
+
34
+ finished, in_progress = branches.partition(&:done?)
35
+
36
+ branches = in_progress + finished.flat_map(&:children)
37
+ end
38
+ end
39
+
40
+ private
41
+ attr_reader :loaders
42
+
43
+ def group_and_load_similar(loaders)
44
+ loaders.grep_v(ThroughAssociation).group_by(&:loader_query).each_pair do |query, similar_loaders|
45
+ query.load_records_in_batch(similar_loaders)
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ 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
@@ -4,13 +4,6 @@ module ActiveRecord
4
4
  module Associations
5
5
  class Preloader
6
6
  class ThroughAssociation < Association # :nodoc:
7
- PRELOADER = ActiveRecord::Associations::Preloader.new(associate_by_default: false)
8
-
9
- def initialize(*)
10
- super
11
- @already_loaded = owners.first.association(through_reflection.name).loaded?
12
- end
13
-
14
7
  def preloaded_records
15
8
  @preloaded_records ||= source_preloaders.flat_map(&:preloaded_records)
16
9
  end
@@ -23,7 +16,7 @@ module ActiveRecord
23
16
  @records_by_owner = owners.each_with_object({}) do |owner, result|
24
17
  through_records = through_records_by_owner[owner] || []
25
18
 
26
- if @already_loaded
19
+ if owners.first.association(through_reflection.name).loaded?
27
20
  if source_type = reflection.options[:source_type]
28
21
  through_records = through_records.select do |record|
29
22
  record[reflection.foreign_type] == source_type
@@ -42,9 +35,40 @@ module ActiveRecord
42
35
  end
43
36
  end
44
37
 
38
+ def data_available?
39
+ return true if super()
40
+ through_preloaders.all?(&:run?) &&
41
+ source_preloaders.all?(&:run?)
42
+ end
43
+
44
+ def runnable_loaders
45
+ if data_available?
46
+ [self]
47
+ elsif through_preloaders.all?(&:run?)
48
+ source_preloaders.flat_map(&:runnable_loaders)
49
+ else
50
+ through_preloaders.flat_map(&:runnable_loaders)
51
+ end
52
+ end
53
+
54
+ def future_classes
55
+ if run? || data_available?
56
+ []
57
+ elsif through_preloaders.all?(&:run?)
58
+ source_preloaders.flat_map(&:future_classes).uniq
59
+ else
60
+ through_classes = through_preloaders.flat_map(&:future_classes)
61
+ source_classes = source_reflection.
62
+ chain.
63
+ reject { |reflection| reflection.respond_to?(:polymorphic?) && reflection.polymorphic? }.
64
+ map(&:klass)
65
+ (through_classes + source_classes).uniq
66
+ end
67
+ end
68
+
45
69
  private
46
70
  def source_preloaders
47
- @source_preloaders ||= PRELOADER.preload(middle_records, source_reflection.name, scope)
71
+ @source_preloaders ||= ActiveRecord::Associations::Preloader.new(records: middle_records, associations: source_reflection.name, scope: scope, associate_by_default: false).loaders
48
72
  end
49
73
 
50
74
  def middle_records
@@ -52,7 +76,7 @@ module ActiveRecord
52
76
  end
53
77
 
54
78
  def through_preloaders
55
- @through_preloaders ||= PRELOADER.preload(owners, through_reflection.name, through_scope)
79
+ @through_preloaders ||= ActiveRecord::Associations::Preloader.new(records: owners, associations: through_reflection.name, scope: through_scope, associate_by_default: false).loaders
56
80
  end
57
81
 
58
82
  def through_reflection
@@ -73,6 +97,8 @@ module ActiveRecord
73
97
  scope = through_reflection.klass.unscoped
74
98
  options = reflection.options
75
99
 
100
+ return scope if options[:disable_joins]
101
+
76
102
  values = reflection_scope.values
77
103
  if annotations = values[:annotate]
78
104
  scope.annotate!(*annotations)
@@ -108,7 +134,7 @@ module ActiveRecord
108
134
  end
109
135
  end
110
136
 
111
- scope
137
+ cascade_strict_loading(scope)
112
138
  end
113
139
  end
114
140
  end