activerecord 6.1.7 → 7.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (311) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +2030 -1020
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +18 -18
  5. data/lib/active_record/aggregations.rb +17 -14
  6. data/lib/active_record/association_relation.rb +1 -11
  7. data/lib/active_record/associations/association.rb +51 -19
  8. data/lib/active_record/associations/association_scope.rb +17 -12
  9. data/lib/active_record/associations/belongs_to_association.rb +28 -9
  10. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +10 -2
  11. data/lib/active_record/associations/builder/association.rb +11 -5
  12. data/lib/active_record/associations/builder/belongs_to.rb +40 -14
  13. data/lib/active_record/associations/builder/collection_association.rb +10 -3
  14. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +1 -5
  15. data/lib/active_record/associations/builder/has_many.rb +3 -2
  16. data/lib/active_record/associations/builder/has_one.rb +2 -1
  17. data/lib/active_record/associations/builder/singular_association.rb +6 -2
  18. data/lib/active_record/associations/collection_association.rb +39 -35
  19. data/lib/active_record/associations/collection_proxy.rb +30 -15
  20. data/lib/active_record/associations/disable_joins_association_scope.rb +59 -0
  21. data/lib/active_record/associations/foreign_association.rb +10 -3
  22. data/lib/active_record/associations/has_many_association.rb +28 -18
  23. data/lib/active_record/associations/has_many_through_association.rb +12 -7
  24. data/lib/active_record/associations/has_one_association.rb +20 -10
  25. data/lib/active_record/associations/has_one_through_association.rb +1 -1
  26. data/lib/active_record/associations/join_dependency/join_association.rb +3 -2
  27. data/lib/active_record/associations/join_dependency.rb +28 -20
  28. data/lib/active_record/associations/preloader/association.rb +210 -52
  29. data/lib/active_record/associations/preloader/batch.rb +48 -0
  30. data/lib/active_record/associations/preloader/branch.rb +147 -0
  31. data/lib/active_record/associations/preloader/through_association.rb +50 -14
  32. data/lib/active_record/associations/preloader.rb +50 -121
  33. data/lib/active_record/associations/singular_association.rb +9 -3
  34. data/lib/active_record/associations/through_association.rb +25 -14
  35. data/lib/active_record/associations.rb +446 -306
  36. data/lib/active_record/asynchronous_queries_tracker.rb +60 -0
  37. data/lib/active_record/attribute_assignment.rb +1 -3
  38. data/lib/active_record/attribute_methods/before_type_cast.rb +24 -2
  39. data/lib/active_record/attribute_methods/dirty.rb +73 -22
  40. data/lib/active_record/attribute_methods/primary_key.rb +78 -26
  41. data/lib/active_record/attribute_methods/query.rb +31 -19
  42. data/lib/active_record/attribute_methods/read.rb +27 -12
  43. data/lib/active_record/attribute_methods/serialization.rb +194 -37
  44. data/lib/active_record/attribute_methods/time_zone_conversion.rb +8 -3
  45. data/lib/active_record/attribute_methods/write.rb +12 -15
  46. data/lib/active_record/attribute_methods.rb +161 -40
  47. data/lib/active_record/attributes.rb +27 -38
  48. data/lib/active_record/autosave_association.rb +65 -31
  49. data/lib/active_record/base.rb +25 -2
  50. data/lib/active_record/callbacks.rb +18 -34
  51. data/lib/active_record/coders/column_serializer.rb +61 -0
  52. data/lib/active_record/coders/json.rb +1 -1
  53. data/lib/active_record/coders/yaml_column.rb +70 -46
  54. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +367 -0
  55. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +211 -0
  56. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +78 -0
  57. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +113 -597
  58. data/lib/active_record/connection_adapters/abstract/database_limits.rb +5 -17
  59. data/lib/active_record/connection_adapters/abstract/database_statements.rb +172 -50
  60. data/lib/active_record/connection_adapters/abstract/query_cache.rb +78 -27
  61. data/lib/active_record/connection_adapters/abstract/quoting.rb +87 -73
  62. data/lib/active_record/connection_adapters/abstract/savepoints.rb +4 -3
  63. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +21 -20
  64. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +186 -31
  65. data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +14 -1
  66. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +367 -141
  67. data/lib/active_record/connection_adapters/abstract/transaction.rb +281 -59
  68. data/lib/active_record/connection_adapters/abstract_adapter.rb +631 -150
  69. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +317 -164
  70. data/lib/active_record/connection_adapters/column.rb +13 -0
  71. data/lib/active_record/connection_adapters/mysql/column.rb +1 -0
  72. data/lib/active_record/connection_adapters/mysql/database_statements.rb +25 -134
  73. data/lib/active_record/connection_adapters/mysql/quoting.rb +56 -25
  74. data/lib/active_record/connection_adapters/mysql/schema_creation.rb +9 -0
  75. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +10 -1
  76. data/lib/active_record/connection_adapters/mysql/schema_dumper.rb +8 -2
  77. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +39 -14
  78. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +151 -0
  79. data/lib/active_record/connection_adapters/mysql2_adapter.rb +112 -55
  80. data/lib/active_record/connection_adapters/pool_config.rb +20 -11
  81. data/lib/active_record/connection_adapters/pool_manager.rb +19 -9
  82. data/lib/active_record/connection_adapters/postgresql/column.rb +30 -1
  83. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +89 -52
  84. data/lib/active_record/connection_adapters/postgresql/oid/array.rb +1 -1
  85. data/lib/active_record/connection_adapters/postgresql/oid/cidr.rb +6 -0
  86. data/lib/active_record/connection_adapters/postgresql/oid/date.rb +8 -0
  87. data/lib/active_record/connection_adapters/postgresql/oid/date_time.rb +5 -0
  88. data/lib/active_record/connection_adapters/postgresql/oid/hstore.rb +53 -14
  89. data/lib/active_record/connection_adapters/postgresql/oid/money.rb +3 -2
  90. data/lib/active_record/connection_adapters/postgresql/oid/range.rb +12 -3
  91. data/lib/active_record/connection_adapters/postgresql/oid/timestamp.rb +15 -0
  92. data/lib/active_record/connection_adapters/postgresql/oid/timestamp_with_time_zone.rb +30 -0
  93. data/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +18 -6
  94. data/lib/active_record/connection_adapters/postgresql/oid.rb +2 -0
  95. data/lib/active_record/connection_adapters/postgresql/quoting.rb +89 -56
  96. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +28 -0
  97. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +92 -2
  98. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +153 -3
  99. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +78 -0
  100. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +397 -75
  101. data/lib/active_record/connection_adapters/postgresql/utils.rb +9 -10
  102. data/lib/active_record/connection_adapters/postgresql_adapter.rb +508 -246
  103. data/lib/active_record/connection_adapters/schema_cache.rb +319 -90
  104. data/lib/active_record/connection_adapters/sqlite3/column.rb +49 -0
  105. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +72 -53
  106. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +37 -21
  107. data/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb +7 -0
  108. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +43 -22
  109. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +296 -104
  110. data/lib/active_record/connection_adapters/statement_pool.rb +7 -0
  111. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +99 -0
  112. data/lib/active_record/connection_adapters/trilogy_adapter.rb +258 -0
  113. data/lib/active_record/connection_adapters.rb +9 -6
  114. data/lib/active_record/connection_handling.rb +108 -137
  115. data/lib/active_record/core.rb +242 -233
  116. data/lib/active_record/counter_cache.rb +52 -27
  117. data/lib/active_record/database_configurations/connection_url_resolver.rb +3 -2
  118. data/lib/active_record/database_configurations/database_config.rb +21 -12
  119. data/lib/active_record/database_configurations/hash_config.rb +88 -16
  120. data/lib/active_record/database_configurations/url_config.rb +18 -12
  121. data/lib/active_record/database_configurations.rb +95 -59
  122. data/lib/active_record/delegated_type.rb +66 -20
  123. data/lib/active_record/deprecator.rb +7 -0
  124. data/lib/active_record/destroy_association_async_job.rb +4 -2
  125. data/lib/active_record/disable_joins_association_relation.rb +39 -0
  126. data/lib/active_record/dynamic_matchers.rb +1 -1
  127. data/lib/active_record/encryption/auto_filtered_parameters.rb +66 -0
  128. data/lib/active_record/encryption/cipher/aes256_gcm.rb +101 -0
  129. data/lib/active_record/encryption/cipher.rb +53 -0
  130. data/lib/active_record/encryption/config.rb +68 -0
  131. data/lib/active_record/encryption/configurable.rb +60 -0
  132. data/lib/active_record/encryption/context.rb +42 -0
  133. data/lib/active_record/encryption/contexts.rb +76 -0
  134. data/lib/active_record/encryption/derived_secret_key_provider.rb +18 -0
  135. data/lib/active_record/encryption/deterministic_key_provider.rb +14 -0
  136. data/lib/active_record/encryption/encryptable_record.rb +230 -0
  137. data/lib/active_record/encryption/encrypted_attribute_type.rb +155 -0
  138. data/lib/active_record/encryption/encrypted_fixtures.rb +38 -0
  139. data/lib/active_record/encryption/encrypting_only_encryptor.rb +12 -0
  140. data/lib/active_record/encryption/encryptor.rb +155 -0
  141. data/lib/active_record/encryption/envelope_encryption_key_provider.rb +55 -0
  142. data/lib/active_record/encryption/errors.rb +15 -0
  143. data/lib/active_record/encryption/extended_deterministic_queries.rb +157 -0
  144. data/lib/active_record/encryption/extended_deterministic_uniqueness_validator.rb +28 -0
  145. data/lib/active_record/encryption/key.rb +28 -0
  146. data/lib/active_record/encryption/key_generator.rb +53 -0
  147. data/lib/active_record/encryption/key_provider.rb +46 -0
  148. data/lib/active_record/encryption/message.rb +33 -0
  149. data/lib/active_record/encryption/message_serializer.rb +92 -0
  150. data/lib/active_record/encryption/null_encryptor.rb +21 -0
  151. data/lib/active_record/encryption/properties.rb +76 -0
  152. data/lib/active_record/encryption/read_only_null_encryptor.rb +24 -0
  153. data/lib/active_record/encryption/scheme.rb +100 -0
  154. data/lib/active_record/encryption.rb +58 -0
  155. data/lib/active_record/enum.rb +154 -63
  156. data/lib/active_record/errors.rb +172 -15
  157. data/lib/active_record/explain.rb +23 -3
  158. data/lib/active_record/explain_registry.rb +11 -6
  159. data/lib/active_record/explain_subscriber.rb +1 -1
  160. data/lib/active_record/fixture_set/file.rb +15 -1
  161. data/lib/active_record/fixture_set/model_metadata.rb +14 -4
  162. data/lib/active_record/fixture_set/render_context.rb +2 -0
  163. data/lib/active_record/fixture_set/table_row.rb +70 -14
  164. data/lib/active_record/fixture_set/table_rows.rb +4 -4
  165. data/lib/active_record/fixtures.rb +147 -86
  166. data/lib/active_record/future_result.rb +174 -0
  167. data/lib/active_record/gem_version.rb +3 -3
  168. data/lib/active_record/inheritance.rb +81 -29
  169. data/lib/active_record/insert_all.rb +135 -22
  170. data/lib/active_record/integration.rb +11 -10
  171. data/lib/active_record/internal_metadata.rb +119 -33
  172. data/lib/active_record/legacy_yaml_adapter.rb +2 -39
  173. data/lib/active_record/locking/optimistic.rb +37 -22
  174. data/lib/active_record/locking/pessimistic.rb +15 -6
  175. data/lib/active_record/log_subscriber.rb +52 -19
  176. data/lib/active_record/marshalling.rb +59 -0
  177. data/lib/active_record/message_pack.rb +124 -0
  178. data/lib/active_record/middleware/database_selector/resolver.rb +10 -10
  179. data/lib/active_record/middleware/database_selector.rb +23 -13
  180. data/lib/active_record/middleware/shard_selector.rb +62 -0
  181. data/lib/active_record/migration/command_recorder.rb +112 -14
  182. data/lib/active_record/migration/compatibility.rb +233 -46
  183. data/lib/active_record/migration/default_strategy.rb +23 -0
  184. data/lib/active_record/migration/execution_strategy.rb +19 -0
  185. data/lib/active_record/migration/join_table.rb +1 -1
  186. data/lib/active_record/migration/pending_migration_connection.rb +21 -0
  187. data/lib/active_record/migration.rb +361 -173
  188. data/lib/active_record/model_schema.rb +125 -101
  189. data/lib/active_record/nested_attributes.rb +50 -20
  190. data/lib/active_record/no_touching.rb +3 -3
  191. data/lib/active_record/normalization.rb +167 -0
  192. data/lib/active_record/persistence.rb +409 -88
  193. data/lib/active_record/promise.rb +84 -0
  194. data/lib/active_record/query_cache.rb +4 -22
  195. data/lib/active_record/query_logs.rb +174 -0
  196. data/lib/active_record/query_logs_formatter.rb +41 -0
  197. data/lib/active_record/querying.rb +29 -6
  198. data/lib/active_record/railtie.rb +220 -44
  199. data/lib/active_record/railties/controller_runtime.rb +15 -10
  200. data/lib/active_record/railties/databases.rake +188 -252
  201. data/lib/active_record/railties/job_runtime.rb +23 -0
  202. data/lib/active_record/readonly_attributes.rb +41 -3
  203. data/lib/active_record/reflection.rb +248 -81
  204. data/lib/active_record/relation/batches/batch_enumerator.rb +23 -7
  205. data/lib/active_record/relation/batches.rb +192 -63
  206. data/lib/active_record/relation/calculations.rb +246 -90
  207. data/lib/active_record/relation/delegation.rb +28 -14
  208. data/lib/active_record/relation/finder_methods.rb +108 -51
  209. data/lib/active_record/relation/merger.rb +22 -13
  210. data/lib/active_record/relation/predicate_builder/association_query_value.rb +31 -3
  211. data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +10 -7
  212. data/lib/active_record/relation/predicate_builder/relation_handler.rb +5 -1
  213. data/lib/active_record/relation/predicate_builder.rb +27 -20
  214. data/lib/active_record/relation/query_attribute.rb +30 -12
  215. data/lib/active_record/relation/query_methods.rb +670 -129
  216. data/lib/active_record/relation/record_fetch_warning.rb +7 -9
  217. data/lib/active_record/relation/spawn_methods.rb +20 -3
  218. data/lib/active_record/relation/where_clause.rb +10 -19
  219. data/lib/active_record/relation.rb +287 -120
  220. data/lib/active_record/result.rb +37 -11
  221. data/lib/active_record/runtime_registry.rb +32 -13
  222. data/lib/active_record/sanitization.rb +65 -20
  223. data/lib/active_record/schema.rb +36 -22
  224. data/lib/active_record/schema_dumper.rb +73 -24
  225. data/lib/active_record/schema_migration.rb +68 -33
  226. data/lib/active_record/scoping/default.rb +72 -15
  227. data/lib/active_record/scoping/named.rb +5 -13
  228. data/lib/active_record/scoping.rb +65 -34
  229. data/lib/active_record/secure_password.rb +60 -0
  230. data/lib/active_record/secure_token.rb +21 -3
  231. data/lib/active_record/serialization.rb +6 -1
  232. data/lib/active_record/signed_id.rb +10 -8
  233. data/lib/active_record/store.rb +10 -10
  234. data/lib/active_record/suppressor.rb +13 -15
  235. data/lib/active_record/table_metadata.rb +16 -3
  236. data/lib/active_record/tasks/database_tasks.rb +251 -140
  237. data/lib/active_record/tasks/mysql_database_tasks.rb +16 -7
  238. data/lib/active_record/tasks/postgresql_database_tasks.rb +35 -26
  239. data/lib/active_record/tasks/sqlite_database_tasks.rb +15 -7
  240. data/lib/active_record/test_databases.rb +1 -1
  241. data/lib/active_record/test_fixtures.rb +117 -96
  242. data/lib/active_record/timestamp.rb +32 -19
  243. data/lib/active_record/token_for.rb +113 -0
  244. data/lib/active_record/touch_later.rb +11 -6
  245. data/lib/active_record/transactions.rb +48 -27
  246. data/lib/active_record/translation.rb +3 -3
  247. data/lib/active_record/type/adapter_specific_registry.rb +32 -14
  248. data/lib/active_record/type/hash_lookup_type_map.rb +34 -1
  249. data/lib/active_record/type/internal/timezone.rb +7 -2
  250. data/lib/active_record/type/serialized.rb +9 -5
  251. data/lib/active_record/type/time.rb +4 -0
  252. data/lib/active_record/type/type_map.rb +17 -20
  253. data/lib/active_record/type.rb +1 -2
  254. data/lib/active_record/validations/absence.rb +1 -1
  255. data/lib/active_record/validations/associated.rb +4 -4
  256. data/lib/active_record/validations/numericality.rb +5 -4
  257. data/lib/active_record/validations/presence.rb +5 -28
  258. data/lib/active_record/validations/uniqueness.rb +51 -6
  259. data/lib/active_record/validations.rb +8 -4
  260. data/lib/active_record/version.rb +1 -1
  261. data/lib/active_record.rb +335 -32
  262. data/lib/arel/attributes/attribute.rb +0 -8
  263. data/lib/arel/crud.rb +28 -22
  264. data/lib/arel/delete_manager.rb +18 -4
  265. data/lib/arel/errors.rb +10 -0
  266. data/lib/arel/factory_methods.rb +4 -0
  267. data/lib/arel/filter_predications.rb +9 -0
  268. data/lib/arel/insert_manager.rb +2 -3
  269. data/lib/arel/nodes/and.rb +4 -0
  270. data/lib/arel/nodes/binary.rb +6 -1
  271. data/lib/arel/nodes/bound_sql_literal.rb +61 -0
  272. data/lib/arel/nodes/casted.rb +1 -1
  273. data/lib/arel/nodes/cte.rb +36 -0
  274. data/lib/arel/nodes/delete_statement.rb +12 -13
  275. data/lib/arel/nodes/filter.rb +10 -0
  276. data/lib/arel/nodes/fragments.rb +35 -0
  277. data/lib/arel/nodes/function.rb +1 -0
  278. data/lib/arel/nodes/homogeneous_in.rb +1 -9
  279. data/lib/arel/nodes/insert_statement.rb +2 -2
  280. data/lib/arel/nodes/leading_join.rb +8 -0
  281. data/lib/arel/nodes/node.rb +111 -2
  282. data/lib/arel/nodes/select_core.rb +2 -2
  283. data/lib/arel/nodes/select_statement.rb +2 -2
  284. data/lib/arel/nodes/sql_literal.rb +6 -0
  285. data/lib/arel/nodes/table_alias.rb +4 -0
  286. data/lib/arel/nodes/update_statement.rb +8 -3
  287. data/lib/arel/nodes.rb +5 -0
  288. data/lib/arel/predications.rb +13 -3
  289. data/lib/arel/select_manager.rb +10 -4
  290. data/lib/arel/table.rb +9 -6
  291. data/lib/arel/tree_manager.rb +5 -13
  292. data/lib/arel/update_manager.rb +18 -4
  293. data/lib/arel/visitors/dot.rb +80 -90
  294. data/lib/arel/visitors/mysql.rb +16 -3
  295. data/lib/arel/visitors/postgresql.rb +0 -10
  296. data/lib/arel/visitors/to_sql.rb +141 -20
  297. data/lib/arel/visitors/visitor.rb +2 -2
  298. data/lib/arel.rb +18 -3
  299. data/lib/rails/generators/active_record/application_record/USAGE +8 -0
  300. data/lib/rails/generators/active_record/application_record/templates/application_record.rb.tt +1 -1
  301. data/lib/rails/generators/active_record/migration.rb +3 -1
  302. data/lib/rails/generators/active_record/model/USAGE +113 -0
  303. data/lib/rails/generators/active_record/model/model_generator.rb +15 -6
  304. data/lib/rails/generators/active_record/model/templates/abstract_base_class.rb.tt +1 -1
  305. data/lib/rails/generators/active_record/model/templates/model.rb.tt +1 -1
  306. data/lib/rails/generators/active_record/model/templates/module.rb.tt +2 -2
  307. data/lib/rails/generators/active_record/multi_db/multi_db_generator.rb +16 -0
  308. data/lib/rails/generators/active_record/multi_db/templates/multi_db.rb.tt +44 -0
  309. metadata +96 -16
  310. data/lib/active_record/connection_adapters/legacy_pool_manager.rb +0 -35
  311. data/lib/active_record/null_relation.rb +0 -67
@@ -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
@@ -4,26 +4,22 @@ 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
17
10
 
18
11
  def records_by_owner
19
12
  return @records_by_owner if defined?(@records_by_owner)
20
- source_records_by_owner = source_preloaders.map(&:records_by_owner).reduce(:merge)
21
- through_records_by_owner = through_preloaders.map(&:records_by_owner).reduce(:merge)
22
13
 
23
14
  @records_by_owner = owners.each_with_object({}) do |owner, result|
15
+ if loaded?(owner)
16
+ result[owner] = target_for(owner)
17
+ next
18
+ end
19
+
24
20
  through_records = through_records_by_owner[owner] || []
25
21
 
26
- if @already_loaded
22
+ if owners.first.association(through_reflection.name).loaded?
27
23
  if source_type = reflection.options[:source_type]
28
24
  through_records = through_records.select do |record|
29
25
  record[reflection.foreign_type] == source_type
@@ -42,17 +38,47 @@ module ActiveRecord
42
38
  end
43
39
  end
44
40
 
41
+ def runnable_loaders
42
+ if data_available?
43
+ [self]
44
+ elsif through_preloaders.all?(&:run?)
45
+ source_preloaders.flat_map(&:runnable_loaders)
46
+ else
47
+ through_preloaders.flat_map(&:runnable_loaders)
48
+ end
49
+ end
50
+
51
+ def future_classes
52
+ if run?
53
+ []
54
+ elsif through_preloaders.all?(&:run?)
55
+ source_preloaders.flat_map(&:future_classes).uniq
56
+ else
57
+ through_classes = through_preloaders.flat_map(&:future_classes)
58
+ source_classes = source_reflection.
59
+ chain.
60
+ reject { |reflection| reflection.respond_to?(:polymorphic?) && reflection.polymorphic? }.
61
+ map(&:klass)
62
+ (through_classes + source_classes).uniq
63
+ end
64
+ end
65
+
45
66
  private
67
+ def data_available?
68
+ owners.all? { |owner| loaded?(owner) } ||
69
+ through_preloaders.all?(&:run?) && source_preloaders.all?(&:run?)
70
+ end
71
+
46
72
  def source_preloaders
47
- @source_preloaders ||= PRELOADER.preload(middle_records, source_reflection.name, scope)
73
+ @source_preloaders ||= ActiveRecord::Associations::Preloader.new(records: middle_records, associations: source_reflection.name, scope: scope, associate_by_default: false).loaders
48
74
  end
49
75
 
50
76
  def middle_records
51
- through_preloaders.flat_map(&:preloaded_records)
77
+ through_records_by_owner.values.flatten
52
78
  end
53
79
 
54
80
  def through_preloaders
55
- @through_preloaders ||= PRELOADER.preload(owners, through_reflection.name, through_scope)
81
+ @through_preloaders ||= ActiveRecord::Associations::Preloader.new(records: owners, associations: through_reflection.name, scope: through_scope, associate_by_default: false).loaders
56
82
  end
57
83
 
58
84
  def through_reflection
@@ -63,6 +89,14 @@ module ActiveRecord
63
89
  reflection.source_reflection
64
90
  end
65
91
 
92
+ def source_records_by_owner
93
+ @source_records_by_owner ||= source_preloaders.map(&:records_by_owner).reduce(:merge)
94
+ end
95
+
96
+ def through_records_by_owner
97
+ @through_records_by_owner ||= through_preloaders.map(&:records_by_owner).reduce(:merge)
98
+ end
99
+
66
100
  def preload_index
67
101
  @preload_index ||= preloaded_records.each_with_object({}).with_index do |(record, result), index|
68
102
  result[record] = index
@@ -73,6 +107,8 @@ module ActiveRecord
73
107
  scope = through_reflection.klass.unscoped
74
108
  options = reflection.options
75
109
 
110
+ return scope if options[:disable_joins]
111
+
76
112
  values = reflection_scope.values
77
113
  if annotations = values[:annotate]
78
114
  scope.annotate!(*annotations)
@@ -108,7 +144,7 @@ module ActiveRecord
108
144
  end
109
145
  end
110
146
 
111
- scope
147
+ cascade_strict_loading(scope)
112
148
  end
113
149
  end
114
150
  end
@@ -4,6 +4,8 @@ require "active_support/core_ext/enumerable"
4
4
 
5
5
  module ActiveRecord
6
6
  module Associations
7
+ # = Active Record \Preloader
8
+ #
7
9
  # Implements the details of eager loading of Active Record associations.
8
10
  #
9
11
  # Suppose that you have the following two Active Record models:
@@ -22,8 +24,8 @@ module ActiveRecord
22
24
  #
23
25
  # Author.includes(:books).where(name: ['bell hooks', 'Homer']).to_a
24
26
  #
25
- # => SELECT `authors`.* FROM `authors` WHERE `name` IN ('bell hooks', 'Homer')
26
- # => SELECT `books`.* FROM `books` WHERE `author_id` IN (2, 5)
27
+ # # SELECT `authors`.* FROM `authors` WHERE `name` IN ('bell hooks', 'Homer')
28
+ # # SELECT `books`.* FROM `books` WHERE `author_id` IN (2, 5)
27
29
  #
28
30
  # Active Record saves the ids of the records from the first query to use in
29
31
  # the second. Depending on the number of associations involved there can be
@@ -33,23 +35,26 @@ module ActiveRecord
33
35
  # Record will fall back to a slightly more resource-intensive single query:
34
36
  #
35
37
  # Author.includes(:books).where(books: {title: 'Illiad'}).to_a
36
- # => SELECT `authors`.`id` AS t0_r0, `authors`.`name` AS t0_r1, `authors`.`age` AS t0_r2,
37
- # `books`.`id` AS t1_r0, `books`.`title` AS t1_r1, `books`.`sales` AS t1_r2
38
- # FROM `authors`
39
- # LEFT OUTER JOIN `books` ON `authors`.`id` = `books`.`author_id`
40
- # WHERE `books`.`title` = 'Illiad'
38
+ # # SELECT `authors`.`id` AS t0_r0, `authors`.`name` AS t0_r1, `authors`.`age` AS t0_r2,
39
+ # # `books`.`id` AS t1_r0, `books`.`title` AS t1_r1, `books`.`sales` AS t1_r2
40
+ # # FROM `authors`
41
+ # # LEFT OUTER JOIN `books` ON `authors`.`id` = `books`.`author_id`
42
+ # # WHERE `books`.`title` = 'Illiad'
41
43
  #
42
44
  # This could result in many rows that contain redundant data and it performs poorly at scale
43
45
  # and is therefore only used when necessary.
44
- #
45
- class Preloader #:nodoc:
46
+ class Preloader # :nodoc:
46
47
  extend ActiveSupport::Autoload
47
48
 
48
49
  eager_autoload do
49
50
  autoload :Association, "active_record/associations/preloader/association"
51
+ autoload :Batch, "active_record/associations/preloader/batch"
52
+ autoload :Branch, "active_record/associations/preloader/branch"
50
53
  autoload :ThroughAssociation, "active_record/associations/preloader/through_association"
51
54
  end
52
55
 
56
+ attr_reader :records, :associations, :scope, :associate_by_default
57
+
53
58
  # Eager loads the named associations for the given Active Record record(s).
54
59
  #
55
60
  # In this description, 'association name' shall refer to the name passed
@@ -70,137 +75,61 @@ module ActiveRecord
70
75
  # for an Author.
71
76
  # - an Array which specifies multiple association names. This array
72
77
  # is processed recursively. For example, specifying <tt>[:avatar, :books]</tt>
73
- # allows this method to preload an author's avatar as well as all of his
78
+ # allows this method to preload an author's avatar as well as all of their
74
79
  # books.
75
80
  # - a Hash which specifies multiple association names, as well as
76
81
  # association names for the to-be-preloaded association objects. For
77
82
  # example, specifying <tt>{ author: :avatar }</tt> will preload a
78
83
  # book's author, as well as that author's avatar.
79
84
  #
80
- # +:associations+ has the same format as the +:include+ option for
81
- # <tt>ActiveRecord::Base.find</tt>. So +associations+ could look like this:
85
+ # +:associations+ has the same format as the arguments to
86
+ # ActiveRecord::QueryMethods#includes. So +associations+ could look like
87
+ # this:
82
88
  #
83
89
  # :books
84
90
  # [ :books, :author ]
85
91
  # { author: :avatar }
86
92
  # [ :books, { author: :avatar } ]
87
- def preload(records, associations, preload_scope = nil)
88
- records = Array.wrap(records).compact
93
+ #
94
+ # +available_records+ is an array of ActiveRecord::Base. The Preloader
95
+ # will try to use the objects in this array to preload the requested
96
+ # associations before querying the database. This can save database
97
+ # queries by reusing in-memory objects. The optimization is only applied
98
+ # to single associations (i.e. :belongs_to, :has_one) with no scopes.
99
+ def initialize(records:, associations:, scope: nil, available_records: [], associate_by_default: true)
100
+ @records = records
101
+ @associations = associations
102
+ @scope = scope
103
+ @available_records = available_records || []
104
+ @associate_by_default = associate_by_default
89
105
 
90
- if records.empty?
91
- []
92
- else
93
- Array.wrap(associations).flat_map { |association|
94
- preloaders_on association, records, preload_scope
95
- }
96
- end
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
97
114
  end
98
115
 
99
- def initialize(associate_by_default: true)
100
- @associate_by_default = associate_by_default
116
+ def empty?
117
+ associations.nil? || records.length == 0
101
118
  end
102
119
 
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
120
+ def call
121
+ Batch.new([self], available_records: @available_records).call
154
122
 
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
178
-
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
184
-
185
- private
186
- attr_reader :owners, :reflection
187
- end
123
+ loaders
124
+ end
188
125
 
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!
126
+ def branches
127
+ @tree.children
128
+ end
197
129
 
198
- if reflection.options[:through]
199
- ThroughAssociation
200
- else
201
- Association
202
- end
203
- end
130
+ def loaders
131
+ branches.flat_map(&:loaders)
132
+ end
204
133
  end
205
134
  end
206
135
  end
@@ -2,9 +2,11 @@
2
2
 
3
3
  module ActiveRecord
4
4
  module Associations
5
- class SingularAssociation < Association #:nodoc:
5
+ class SingularAssociation < Association # :nodoc:
6
6
  # Implements the reader method, e.g. foo.bar for Foo.has_one :bar
7
7
  def reader
8
+ ensure_klass_exists!
9
+
8
10
  if !loaded? || stale_target?
9
11
  reload
10
12
  end
@@ -32,11 +34,15 @@ module ActiveRecord
32
34
 
33
35
  private
34
36
  def scope_for_create
35
- super.except!(klass.primary_key)
37
+ super.except!(*Array(klass.primary_key))
36
38
  end
37
39
 
38
40
  def find_target
39
- super.first
41
+ if disable_joins
42
+ scope.first
43
+ else
44
+ super.first
45
+ end
40
46
  end
41
47
 
42
48
  def replace(record)
@@ -3,10 +3,14 @@
3
3
  module ActiveRecord
4
4
  module Associations
5
5
  # = Active Record Through Association
6
- module ThroughAssociation #:nodoc:
6
+ module ThroughAssociation # :nodoc:
7
7
  delegate :source_reflection, to: :reflection
8
8
 
9
9
  private
10
+ def transaction(&block)
11
+ through_reflection.klass.transaction(&block)
12
+ end
13
+
10
14
  def through_reflection
11
15
  @through_reflection ||= begin
12
16
  refl = reflection.through_reflection
@@ -55,12 +59,11 @@ module ActiveRecord
55
59
 
56
60
  association_primary_key = source_reflection.association_primary_key(reflection.klass)
57
61
 
58
- if association_primary_key == reflection.klass.primary_key && !options[:source_type]
62
+ if Array(association_primary_key) == reflection.klass.composite_query_constraints_list && !options[:source_type]
59
63
  join_attributes = { source_reflection.name => records }
60
64
  else
61
- join_attributes = {
62
- source_reflection.foreign_key => records.map(&association_primary_key.to_sym)
63
- }
65
+ assoc_pk_values = records.map { |record| record._read_attribute(association_primary_key) }
66
+ join_attributes = { source_reflection.foreign_key => assoc_pk_values }
64
67
  end
65
68
 
66
69
  if options[:source_type]
@@ -74,16 +77,20 @@ module ActiveRecord
74
77
  end
75
78
  end
76
79
 
77
- # Note: this does not capture all cases, for example it would be crazy to try to
78
- # properly support stale-checking for nested associations.
80
+ # Note: this does not capture all cases, for example it would be impractical
81
+ # to try to properly support stale-checking for nested associations.
79
82
  def stale_state
80
83
  if through_reflection.belongs_to?
81
- owner[through_reflection.foreign_key] && owner[through_reflection.foreign_key].to_s
84
+ Array(through_reflection.foreign_key).filter_map do |foreign_key_column|
85
+ owner[foreign_key_column] && owner[foreign_key_column].to_s
86
+ end.presence
82
87
  end
83
88
  end
84
89
 
85
90
  def foreign_key_present?
86
- through_reflection.belongs_to? && !owner[through_reflection.foreign_key].nil?
91
+ through_reflection.belongs_to? && Array(through_reflection.foreign_key).all? do |foreign_key_column|
92
+ !owner[foreign_key_column].nil?
93
+ end
87
94
  end
88
95
 
89
96
  def ensure_mutable
@@ -107,11 +114,15 @@ module ActiveRecord
107
114
  end
108
115
 
109
116
  def build_record(attributes)
110
- inverse = source_reflection.inverse_of
111
- target = through_association.target
112
-
113
- if inverse && target && !target.is_a?(Array)
114
- attributes[inverse.foreign_key] = target.id
117
+ if source_reflection.collection?
118
+ inverse = source_reflection.inverse_of
119
+ target = through_association.target
120
+
121
+ if inverse && target && !target.is_a?(Array)
122
+ Array(target.id).zip(Array(inverse.foreign_key)).map do |primary_key_value, foreign_key_column|
123
+ attributes[foreign_key_column] = primary_key_value
124
+ end
125
+ end
115
126
  end
116
127
 
117
128
  super