activerecord 6.1.3.2 → 7.0.0.alpha2

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 (229) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +734 -1058
  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 +35 -7
  8. data/lib/active_record/associations/association_scope.rb +1 -3
  9. data/lib/active_record/associations/belongs_to_association.rb +16 -6
  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 +24 -25
  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 -49
  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 +11 -1
  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 +14 -7
  55. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +5 -18
  56. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +30 -9
  57. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +60 -16
  58. data/lib/active_record/connection_adapters/abstract/transaction.rb +17 -6
  59. data/lib/active_record/connection_adapters/abstract_adapter.rb +115 -69
  60. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +96 -81
  61. data/lib/active_record/connection_adapters/legacy_pool_manager.rb +6 -2
  62. data/lib/active_record/connection_adapters/mysql/database_statements.rb +33 -21
  63. data/lib/active_record/connection_adapters/mysql/quoting.rb +16 -1
  64. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +3 -0
  65. data/lib/active_record/connection_adapters/mysql2_adapter.rb +12 -6
  66. data/lib/active_record/connection_adapters/pool_config.rb +1 -3
  67. data/lib/active_record/connection_adapters/pool_manager.rb +5 -1
  68. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +19 -12
  69. data/lib/active_record/connection_adapters/postgresql/oid/date.rb +8 -0
  70. data/lib/active_record/connection_adapters/postgresql/oid/date_time.rb +5 -0
  71. data/lib/active_record/connection_adapters/postgresql/oid/hstore.rb +53 -14
  72. data/lib/active_record/connection_adapters/postgresql/oid/range.rb +1 -1
  73. data/lib/active_record/connection_adapters/postgresql/oid/timestamp.rb +15 -0
  74. data/lib/active_record/connection_adapters/postgresql/oid/timestamp_with_time_zone.rb +28 -0
  75. data/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +18 -6
  76. data/lib/active_record/connection_adapters/postgresql/oid.rb +2 -0
  77. data/lib/active_record/connection_adapters/postgresql/quoting.rb +6 -6
  78. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +32 -0
  79. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +5 -1
  80. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +12 -12
  81. data/lib/active_record/connection_adapters/postgresql_adapter.rb +157 -100
  82. data/lib/active_record/connection_adapters/schema_cache.rb +35 -4
  83. data/lib/active_record/connection_adapters/sql_type_metadata.rb +0 -2
  84. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +23 -17
  85. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +4 -2
  86. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +61 -30
  87. data/lib/active_record/connection_adapters.rb +8 -5
  88. data/lib/active_record/connection_handling.rb +20 -38
  89. data/lib/active_record/core.rb +129 -117
  90. data/lib/active_record/database_configurations/database_config.rb +12 -0
  91. data/lib/active_record/database_configurations/hash_config.rb +27 -1
  92. data/lib/active_record/database_configurations/url_config.rb +2 -2
  93. data/lib/active_record/database_configurations.rb +18 -9
  94. data/lib/active_record/delegated_type.rb +33 -11
  95. data/lib/active_record/destroy_association_async_job.rb +1 -1
  96. data/lib/active_record/disable_joins_association_relation.rb +39 -0
  97. data/lib/active_record/dynamic_matchers.rb +1 -1
  98. data/lib/active_record/encryption/cipher/aes256_gcm.rb +98 -0
  99. data/lib/active_record/encryption/cipher.rb +53 -0
  100. data/lib/active_record/encryption/config.rb +44 -0
  101. data/lib/active_record/encryption/configurable.rb +61 -0
  102. data/lib/active_record/encryption/context.rb +35 -0
  103. data/lib/active_record/encryption/contexts.rb +72 -0
  104. data/lib/active_record/encryption/derived_secret_key_provider.rb +12 -0
  105. data/lib/active_record/encryption/deterministic_key_provider.rb +14 -0
  106. data/lib/active_record/encryption/encryptable_record.rb +208 -0
  107. data/lib/active_record/encryption/encrypted_attribute_type.rb +140 -0
  108. data/lib/active_record/encryption/encrypted_fixtures.rb +38 -0
  109. data/lib/active_record/encryption/encrypting_only_encryptor.rb +12 -0
  110. data/lib/active_record/encryption/encryptor.rb +155 -0
  111. data/lib/active_record/encryption/envelope_encryption_key_provider.rb +55 -0
  112. data/lib/active_record/encryption/errors.rb +15 -0
  113. data/lib/active_record/encryption/extended_deterministic_queries.rb +160 -0
  114. data/lib/active_record/encryption/extended_deterministic_uniqueness_validator.rb +29 -0
  115. data/lib/active_record/encryption/key.rb +28 -0
  116. data/lib/active_record/encryption/key_generator.rb +42 -0
  117. data/lib/active_record/encryption/key_provider.rb +46 -0
  118. data/lib/active_record/encryption/message.rb +33 -0
  119. data/lib/active_record/encryption/message_serializer.rb +80 -0
  120. data/lib/active_record/encryption/null_encryptor.rb +21 -0
  121. data/lib/active_record/encryption/properties.rb +76 -0
  122. data/lib/active_record/encryption/read_only_null_encryptor.rb +24 -0
  123. data/lib/active_record/encryption/scheme.rb +99 -0
  124. data/lib/active_record/encryption.rb +55 -0
  125. data/lib/active_record/enum.rb +44 -46
  126. data/lib/active_record/errors.rb +66 -3
  127. data/lib/active_record/fixture_set/file.rb +15 -1
  128. data/lib/active_record/fixture_set/table_row.rb +40 -5
  129. data/lib/active_record/fixture_set/table_rows.rb +4 -4
  130. data/lib/active_record/fixtures.rb +16 -11
  131. data/lib/active_record/future_result.rb +139 -0
  132. data/lib/active_record/gem_version.rb +4 -4
  133. data/lib/active_record/inheritance.rb +55 -17
  134. data/lib/active_record/insert_all.rb +39 -6
  135. data/lib/active_record/integration.rb +1 -1
  136. data/lib/active_record/internal_metadata.rb +3 -5
  137. data/lib/active_record/legacy_yaml_adapter.rb +1 -1
  138. data/lib/active_record/locking/optimistic.rb +10 -9
  139. data/lib/active_record/log_subscriber.rb +6 -2
  140. data/lib/active_record/middleware/database_selector/resolver.rb +6 -10
  141. data/lib/active_record/middleware/database_selector.rb +8 -3
  142. data/lib/active_record/migration/command_recorder.rb +4 -4
  143. data/lib/active_record/migration/compatibility.rb +83 -1
  144. data/lib/active_record/migration/join_table.rb +1 -1
  145. data/lib/active_record/migration.rb +109 -79
  146. data/lib/active_record/model_schema.rb +46 -32
  147. data/lib/active_record/nested_attributes.rb +3 -3
  148. data/lib/active_record/no_touching.rb +2 -2
  149. data/lib/active_record/null_relation.rb +2 -6
  150. data/lib/active_record/persistence.rb +134 -45
  151. data/lib/active_record/query_cache.rb +2 -2
  152. data/lib/active_record/query_logs.rb +203 -0
  153. data/lib/active_record/querying.rb +15 -5
  154. data/lib/active_record/railtie.rb +117 -17
  155. data/lib/active_record/railties/controller_runtime.rb +1 -1
  156. data/lib/active_record/railties/databases.rake +83 -58
  157. data/lib/active_record/readonly_attributes.rb +11 -0
  158. data/lib/active_record/reflection.rb +45 -44
  159. data/lib/active_record/relation/batches/batch_enumerator.rb +19 -5
  160. data/lib/active_record/relation/batches.rb +3 -3
  161. data/lib/active_record/relation/calculations.rb +42 -25
  162. data/lib/active_record/relation/delegation.rb +6 -6
  163. data/lib/active_record/relation/finder_methods.rb +32 -23
  164. data/lib/active_record/relation/merger.rb +20 -13
  165. data/lib/active_record/relation/predicate_builder.rb +1 -6
  166. data/lib/active_record/relation/query_attribute.rb +5 -11
  167. data/lib/active_record/relation/query_methods.rb +233 -50
  168. data/lib/active_record/relation/record_fetch_warning.rb +2 -2
  169. data/lib/active_record/relation/spawn_methods.rb +2 -2
  170. data/lib/active_record/relation/where_clause.rb +22 -15
  171. data/lib/active_record/relation.rb +170 -87
  172. data/lib/active_record/result.rb +17 -2
  173. data/lib/active_record/runtime_registry.rb +2 -4
  174. data/lib/active_record/sanitization.rb +11 -7
  175. data/lib/active_record/schema_dumper.rb +3 -3
  176. data/lib/active_record/schema_migration.rb +0 -4
  177. data/lib/active_record/scoping/default.rb +62 -15
  178. data/lib/active_record/scoping/named.rb +3 -11
  179. data/lib/active_record/scoping.rb +40 -22
  180. data/lib/active_record/serialization.rb +1 -1
  181. data/lib/active_record/signed_id.rb +1 -1
  182. data/lib/active_record/statement_cache.rb +2 -2
  183. data/lib/active_record/tasks/database_tasks.rb +107 -23
  184. data/lib/active_record/tasks/mysql_database_tasks.rb +1 -1
  185. data/lib/active_record/tasks/postgresql_database_tasks.rb +14 -11
  186. data/lib/active_record/test_databases.rb +1 -1
  187. data/lib/active_record/test_fixtures.rb +45 -4
  188. data/lib/active_record/timestamp.rb +3 -4
  189. data/lib/active_record/transactions.rb +9 -14
  190. data/lib/active_record/translation.rb +2 -2
  191. data/lib/active_record/type/adapter_specific_registry.rb +32 -7
  192. data/lib/active_record/type/hash_lookup_type_map.rb +34 -1
  193. data/lib/active_record/type/internal/timezone.rb +2 -2
  194. data/lib/active_record/type/serialized.rb +1 -1
  195. data/lib/active_record/type/type_map.rb +17 -20
  196. data/lib/active_record/type.rb +1 -2
  197. data/lib/active_record/validations/associated.rb +1 -1
  198. data/lib/active_record/validations/numericality.rb +1 -1
  199. data/lib/active_record.rb +170 -2
  200. data/lib/arel/attributes/attribute.rb +0 -8
  201. data/lib/arel/collectors/bind.rb +2 -2
  202. data/lib/arel/collectors/composite.rb +3 -3
  203. data/lib/arel/collectors/sql_string.rb +1 -1
  204. data/lib/arel/collectors/substitute_binds.rb +1 -1
  205. data/lib/arel/crud.rb +18 -22
  206. data/lib/arel/delete_manager.rb +2 -4
  207. data/lib/arel/insert_manager.rb +2 -3
  208. data/lib/arel/nodes/casted.rb +1 -1
  209. data/lib/arel/nodes/delete_statement.rb +8 -13
  210. data/lib/arel/nodes/homogeneous_in.rb +4 -0
  211. data/lib/arel/nodes/insert_statement.rb +2 -2
  212. data/lib/arel/nodes/select_core.rb +2 -2
  213. data/lib/arel/nodes/select_statement.rb +2 -2
  214. data/lib/arel/nodes/update_statement.rb +3 -2
  215. data/lib/arel/predications.rb +3 -3
  216. data/lib/arel/select_manager.rb +10 -4
  217. data/lib/arel/table.rb +0 -1
  218. data/lib/arel/tree_manager.rb +0 -12
  219. data/lib/arel/update_manager.rb +2 -4
  220. data/lib/arel/visitors/dot.rb +80 -90
  221. data/lib/arel/visitors/mysql.rb +6 -1
  222. data/lib/arel/visitors/postgresql.rb +0 -10
  223. data/lib/arel/visitors/to_sql.rb +44 -3
  224. data/lib/arel.rb +1 -1
  225. data/lib/rails/generators/active_record/application_record/templates/application_record.rb.tt +1 -1
  226. data/lib/rails/generators/active_record/model/templates/abstract_base_class.rb.tt +1 -1
  227. data/lib/rails/generators/active_record/model/templates/model.rb.tt +1 -1
  228. data/lib/rails/generators/active_record/model/templates/module.rb.tt +2 -2
  229. metadata +55 -16
@@ -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
@@ -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