activerecord 6.1.7 → 7.2.2

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 (333) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +616 -1290
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +31 -31
  5. data/examples/performance.rb +2 -2
  6. data/lib/active_record/aggregations.rb +17 -14
  7. data/lib/active_record/association_relation.rb +2 -12
  8. data/lib/active_record/associations/alias_tracker.rb +25 -19
  9. data/lib/active_record/associations/association.rb +60 -21
  10. data/lib/active_record/associations/association_scope.rb +17 -12
  11. data/lib/active_record/associations/belongs_to_association.rb +37 -11
  12. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +13 -4
  13. data/lib/active_record/associations/builder/association.rb +11 -5
  14. data/lib/active_record/associations/builder/belongs_to.rb +41 -14
  15. data/lib/active_record/associations/builder/collection_association.rb +10 -3
  16. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +3 -7
  17. data/lib/active_record/associations/builder/has_many.rb +4 -4
  18. data/lib/active_record/associations/builder/has_one.rb +4 -4
  19. data/lib/active_record/associations/builder/singular_association.rb +6 -2
  20. data/lib/active_record/associations/collection_association.rb +46 -36
  21. data/lib/active_record/associations/collection_proxy.rb +44 -16
  22. data/lib/active_record/associations/disable_joins_association_scope.rb +59 -0
  23. data/lib/active_record/associations/errors.rb +265 -0
  24. data/lib/active_record/associations/foreign_association.rb +10 -3
  25. data/lib/active_record/associations/has_many_association.rb +29 -19
  26. data/lib/active_record/associations/has_many_through_association.rb +19 -8
  27. data/lib/active_record/associations/has_one_association.rb +20 -10
  28. data/lib/active_record/associations/has_one_through_association.rb +1 -1
  29. data/lib/active_record/associations/join_dependency/join_association.rb +30 -27
  30. data/lib/active_record/associations/join_dependency.rb +28 -20
  31. data/lib/active_record/associations/nested_error.rb +47 -0
  32. data/lib/active_record/associations/preloader/association.rb +212 -53
  33. data/lib/active_record/associations/preloader/batch.rb +48 -0
  34. data/lib/active_record/associations/preloader/branch.rb +153 -0
  35. data/lib/active_record/associations/preloader/through_association.rb +50 -16
  36. data/lib/active_record/associations/preloader.rb +50 -121
  37. data/lib/active_record/associations/singular_association.rb +15 -3
  38. data/lib/active_record/associations/through_association.rb +25 -14
  39. data/lib/active_record/associations.rb +429 -522
  40. data/lib/active_record/asynchronous_queries_tracker.rb +60 -0
  41. data/lib/active_record/attribute_assignment.rb +1 -5
  42. data/lib/active_record/attribute_methods/before_type_cast.rb +24 -2
  43. data/lib/active_record/attribute_methods/composite_primary_key.rb +84 -0
  44. data/lib/active_record/attribute_methods/dirty.rb +73 -22
  45. data/lib/active_record/attribute_methods/primary_key.rb +47 -27
  46. data/lib/active_record/attribute_methods/query.rb +31 -19
  47. data/lib/active_record/attribute_methods/read.rb +14 -11
  48. data/lib/active_record/attribute_methods/serialization.rb +174 -37
  49. data/lib/active_record/attribute_methods/time_zone_conversion.rb +15 -9
  50. data/lib/active_record/attribute_methods/write.rb +12 -15
  51. data/lib/active_record/attribute_methods.rb +164 -52
  52. data/lib/active_record/attributes.rb +57 -54
  53. data/lib/active_record/autosave_association.rb +74 -57
  54. data/lib/active_record/base.rb +27 -5
  55. data/lib/active_record/callbacks.rb +19 -35
  56. data/lib/active_record/coders/column_serializer.rb +61 -0
  57. data/lib/active_record/coders/json.rb +1 -1
  58. data/lib/active_record/coders/yaml_column.rb +70 -46
  59. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +284 -0
  60. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +211 -0
  61. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +79 -0
  62. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +325 -604
  63. data/lib/active_record/connection_adapters/abstract/database_limits.rb +5 -17
  64. data/lib/active_record/connection_adapters/abstract/database_statements.rb +199 -60
  65. data/lib/active_record/connection_adapters/abstract/query_cache.rb +230 -64
  66. data/lib/active_record/connection_adapters/abstract/quoting.rb +119 -131
  67. data/lib/active_record/connection_adapters/abstract/savepoints.rb +4 -3
  68. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +21 -20
  69. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +186 -31
  70. data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +14 -1
  71. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +378 -143
  72. data/lib/active_record/connection_adapters/abstract/transaction.rb +361 -76
  73. data/lib/active_record/connection_adapters/abstract_adapter.rb +624 -163
  74. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +348 -165
  75. data/lib/active_record/connection_adapters/column.rb +13 -0
  76. data/lib/active_record/connection_adapters/mysql/column.rb +1 -0
  77. data/lib/active_record/connection_adapters/mysql/database_statements.rb +29 -130
  78. data/lib/active_record/connection_adapters/mysql/quoting.rb +81 -55
  79. data/lib/active_record/connection_adapters/mysql/schema_creation.rb +9 -0
  80. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +10 -1
  81. data/lib/active_record/connection_adapters/mysql/schema_dumper.rb +8 -2
  82. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +45 -14
  83. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +152 -0
  84. data/lib/active_record/connection_adapters/mysql2_adapter.rb +107 -68
  85. data/lib/active_record/connection_adapters/pool_config.rb +26 -16
  86. data/lib/active_record/connection_adapters/pool_manager.rb +19 -9
  87. data/lib/active_record/connection_adapters/postgresql/column.rb +30 -1
  88. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +114 -54
  89. data/lib/active_record/connection_adapters/postgresql/oid/array.rb +1 -1
  90. data/lib/active_record/connection_adapters/postgresql/oid/cidr.rb +6 -0
  91. data/lib/active_record/connection_adapters/postgresql/oid/date.rb +8 -0
  92. data/lib/active_record/connection_adapters/postgresql/oid/date_time.rb +5 -0
  93. data/lib/active_record/connection_adapters/postgresql/oid/hstore.rb +53 -14
  94. data/lib/active_record/connection_adapters/postgresql/oid/interval.rb +1 -1
  95. data/lib/active_record/connection_adapters/postgresql/oid/money.rb +3 -2
  96. data/lib/active_record/connection_adapters/postgresql/oid/range.rb +12 -3
  97. data/lib/active_record/connection_adapters/postgresql/oid/timestamp.rb +15 -0
  98. data/lib/active_record/connection_adapters/postgresql/oid/timestamp_with_time_zone.rb +30 -0
  99. data/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +18 -6
  100. data/lib/active_record/connection_adapters/postgresql/oid/uuid.rb +14 -4
  101. data/lib/active_record/connection_adapters/postgresql/oid.rb +2 -0
  102. data/lib/active_record/connection_adapters/postgresql/quoting.rb +137 -104
  103. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +28 -0
  104. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +92 -2
  105. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +173 -3
  106. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +78 -0
  107. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +403 -77
  108. data/lib/active_record/connection_adapters/postgresql/utils.rb +9 -10
  109. data/lib/active_record/connection_adapters/postgresql_adapter.rb +520 -253
  110. data/lib/active_record/connection_adapters/schema_cache.rb +326 -102
  111. data/lib/active_record/connection_adapters/sqlite3/column.rb +62 -0
  112. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +78 -55
  113. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +68 -54
  114. data/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +22 -0
  115. data/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb +20 -0
  116. data/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +16 -0
  117. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +66 -22
  118. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +372 -130
  119. data/lib/active_record/connection_adapters/statement_pool.rb +7 -0
  120. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +99 -0
  121. data/lib/active_record/connection_adapters/trilogy_adapter.rb +229 -0
  122. data/lib/active_record/connection_adapters.rb +130 -6
  123. data/lib/active_record/connection_handling.rb +132 -146
  124. data/lib/active_record/core.rb +310 -253
  125. data/lib/active_record/counter_cache.rb +68 -34
  126. data/lib/active_record/database_configurations/connection_url_resolver.rb +10 -4
  127. data/lib/active_record/database_configurations/database_config.rb +34 -10
  128. data/lib/active_record/database_configurations/hash_config.rb +107 -31
  129. data/lib/active_record/database_configurations/url_config.rb +38 -13
  130. data/lib/active_record/database_configurations.rb +96 -60
  131. data/lib/active_record/delegated_type.rb +90 -20
  132. data/lib/active_record/deprecator.rb +7 -0
  133. data/lib/active_record/destroy_association_async_job.rb +4 -2
  134. data/lib/active_record/disable_joins_association_relation.rb +39 -0
  135. data/lib/active_record/dynamic_matchers.rb +3 -3
  136. data/lib/active_record/encryption/auto_filtered_parameters.rb +66 -0
  137. data/lib/active_record/encryption/cipher/aes256_gcm.rb +101 -0
  138. data/lib/active_record/encryption/cipher.rb +53 -0
  139. data/lib/active_record/encryption/config.rb +68 -0
  140. data/lib/active_record/encryption/configurable.rb +60 -0
  141. data/lib/active_record/encryption/context.rb +42 -0
  142. data/lib/active_record/encryption/contexts.rb +76 -0
  143. data/lib/active_record/encryption/derived_secret_key_provider.rb +18 -0
  144. data/lib/active_record/encryption/deterministic_key_provider.rb +14 -0
  145. data/lib/active_record/encryption/encryptable_record.rb +230 -0
  146. data/lib/active_record/encryption/encrypted_attribute_type.rb +175 -0
  147. data/lib/active_record/encryption/encrypted_fixtures.rb +38 -0
  148. data/lib/active_record/encryption/encrypting_only_encryptor.rb +12 -0
  149. data/lib/active_record/encryption/encryptor.rb +170 -0
  150. data/lib/active_record/encryption/envelope_encryption_key_provider.rb +55 -0
  151. data/lib/active_record/encryption/errors.rb +15 -0
  152. data/lib/active_record/encryption/extended_deterministic_queries.rb +157 -0
  153. data/lib/active_record/encryption/extended_deterministic_uniqueness_validator.rb +28 -0
  154. data/lib/active_record/encryption/key.rb +28 -0
  155. data/lib/active_record/encryption/key_generator.rb +53 -0
  156. data/lib/active_record/encryption/key_provider.rb +46 -0
  157. data/lib/active_record/encryption/message.rb +33 -0
  158. data/lib/active_record/encryption/message_pack_message_serializer.rb +76 -0
  159. data/lib/active_record/encryption/message_serializer.rb +96 -0
  160. data/lib/active_record/encryption/null_encryptor.rb +25 -0
  161. data/lib/active_record/encryption/properties.rb +76 -0
  162. data/lib/active_record/encryption/read_only_null_encryptor.rb +28 -0
  163. data/lib/active_record/encryption/scheme.rb +100 -0
  164. data/lib/active_record/encryption.rb +58 -0
  165. data/lib/active_record/enum.rb +170 -62
  166. data/lib/active_record/errors.rb +210 -27
  167. data/lib/active_record/explain.rb +21 -12
  168. data/lib/active_record/explain_registry.rb +11 -6
  169. data/lib/active_record/explain_subscriber.rb +1 -1
  170. data/lib/active_record/fixture_set/file.rb +15 -1
  171. data/lib/active_record/fixture_set/model_metadata.rb +14 -4
  172. data/lib/active_record/fixture_set/render_context.rb +2 -0
  173. data/lib/active_record/fixture_set/table_row.rb +70 -14
  174. data/lib/active_record/fixture_set/table_rows.rb +4 -4
  175. data/lib/active_record/fixtures.rb +179 -112
  176. data/lib/active_record/future_result.rb +178 -0
  177. data/lib/active_record/gem_version.rb +4 -4
  178. data/lib/active_record/inheritance.rb +85 -31
  179. data/lib/active_record/insert_all.rb +148 -32
  180. data/lib/active_record/integration.rb +14 -10
  181. data/lib/active_record/internal_metadata.rb +123 -23
  182. data/lib/active_record/legacy_yaml_adapter.rb +2 -39
  183. data/lib/active_record/locking/optimistic.rb +43 -27
  184. data/lib/active_record/locking/pessimistic.rb +15 -6
  185. data/lib/active_record/log_subscriber.rb +41 -29
  186. data/lib/active_record/marshalling.rb +59 -0
  187. data/lib/active_record/message_pack.rb +124 -0
  188. data/lib/active_record/middleware/database_selector/resolver.rb +10 -10
  189. data/lib/active_record/middleware/database_selector.rb +23 -13
  190. data/lib/active_record/middleware/shard_selector.rb +62 -0
  191. data/lib/active_record/migration/command_recorder.rb +113 -16
  192. data/lib/active_record/migration/compatibility.rb +235 -46
  193. data/lib/active_record/migration/default_strategy.rb +22 -0
  194. data/lib/active_record/migration/execution_strategy.rb +19 -0
  195. data/lib/active_record/migration/join_table.rb +1 -1
  196. data/lib/active_record/migration/pending_migration_connection.rb +21 -0
  197. data/lib/active_record/migration.rb +374 -177
  198. data/lib/active_record/model_schema.rb +145 -158
  199. data/lib/active_record/nested_attributes.rb +61 -23
  200. data/lib/active_record/no_touching.rb +3 -3
  201. data/lib/active_record/normalization.rb +163 -0
  202. data/lib/active_record/persistence.rb +282 -283
  203. data/lib/active_record/promise.rb +84 -0
  204. data/lib/active_record/query_cache.rb +18 -25
  205. data/lib/active_record/query_logs.rb +189 -0
  206. data/lib/active_record/query_logs_formatter.rb +41 -0
  207. data/lib/active_record/querying.rb +44 -9
  208. data/lib/active_record/railtie.rb +229 -71
  209. data/lib/active_record/railties/controller_runtime.rb +25 -11
  210. data/lib/active_record/railties/databases.rake +189 -256
  211. data/lib/active_record/railties/job_runtime.rb +23 -0
  212. data/lib/active_record/readonly_attributes.rb +41 -3
  213. data/lib/active_record/reflection.rb +332 -103
  214. data/lib/active_record/relation/batches/batch_enumerator.rb +38 -9
  215. data/lib/active_record/relation/batches.rb +200 -65
  216. data/lib/active_record/relation/calculations.rb +301 -112
  217. data/lib/active_record/relation/delegation.rb +33 -22
  218. data/lib/active_record/relation/finder_methods.rb +123 -52
  219. data/lib/active_record/relation/merger.rb +26 -19
  220. data/lib/active_record/relation/predicate_builder/array_handler.rb +2 -2
  221. data/lib/active_record/relation/predicate_builder/association_query_value.rb +38 -4
  222. data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +10 -7
  223. data/lib/active_record/relation/predicate_builder/relation_handler.rb +5 -1
  224. data/lib/active_record/relation/predicate_builder.rb +29 -22
  225. data/lib/active_record/relation/query_attribute.rb +30 -12
  226. data/lib/active_record/relation/query_methods.rb +870 -163
  227. data/lib/active_record/relation/record_fetch_warning.rb +10 -9
  228. data/lib/active_record/relation/spawn_methods.rb +7 -6
  229. data/lib/active_record/relation/where_clause.rb +15 -36
  230. data/lib/active_record/relation.rb +736 -145
  231. data/lib/active_record/result.rb +67 -54
  232. data/lib/active_record/runtime_registry.rb +71 -13
  233. data/lib/active_record/sanitization.rb +84 -34
  234. data/lib/active_record/schema.rb +39 -23
  235. data/lib/active_record/schema_dumper.rb +90 -31
  236. data/lib/active_record/schema_migration.rb +74 -23
  237. data/lib/active_record/scoping/default.rb +72 -15
  238. data/lib/active_record/scoping/named.rb +6 -13
  239. data/lib/active_record/scoping.rb +65 -34
  240. data/lib/active_record/secure_password.rb +60 -0
  241. data/lib/active_record/secure_token.rb +21 -3
  242. data/lib/active_record/serialization.rb +6 -1
  243. data/lib/active_record/signed_id.rb +30 -9
  244. data/lib/active_record/statement_cache.rb +7 -7
  245. data/lib/active_record/store.rb +10 -10
  246. data/lib/active_record/suppressor.rb +13 -15
  247. data/lib/active_record/table_metadata.rb +7 -3
  248. data/lib/active_record/tasks/database_tasks.rb +288 -149
  249. data/lib/active_record/tasks/mysql_database_tasks.rb +16 -7
  250. data/lib/active_record/tasks/postgresql_database_tasks.rb +35 -26
  251. data/lib/active_record/tasks/sqlite_database_tasks.rb +16 -7
  252. data/lib/active_record/test_databases.rb +1 -1
  253. data/lib/active_record/test_fixtures.rb +173 -155
  254. data/lib/active_record/testing/query_assertions.rb +121 -0
  255. data/lib/active_record/timestamp.rb +32 -19
  256. data/lib/active_record/token_for.rb +123 -0
  257. data/lib/active_record/touch_later.rb +12 -7
  258. data/lib/active_record/transaction.rb +132 -0
  259. data/lib/active_record/transactions.rb +118 -41
  260. data/lib/active_record/translation.rb +3 -5
  261. data/lib/active_record/type/adapter_specific_registry.rb +32 -14
  262. data/lib/active_record/type/hash_lookup_type_map.rb +34 -1
  263. data/lib/active_record/type/internal/timezone.rb +7 -2
  264. data/lib/active_record/type/serialized.rb +9 -7
  265. data/lib/active_record/type/time.rb +4 -0
  266. data/lib/active_record/type/type_map.rb +17 -20
  267. data/lib/active_record/type.rb +1 -2
  268. data/lib/active_record/type_caster/connection.rb +4 -4
  269. data/lib/active_record/validations/absence.rb +1 -1
  270. data/lib/active_record/validations/associated.rb +13 -7
  271. data/lib/active_record/validations/numericality.rb +5 -4
  272. data/lib/active_record/validations/presence.rb +5 -28
  273. data/lib/active_record/validations/uniqueness.rb +65 -15
  274. data/lib/active_record/validations.rb +12 -5
  275. data/lib/active_record/version.rb +1 -1
  276. data/lib/active_record.rb +444 -32
  277. data/lib/arel/alias_predication.rb +1 -1
  278. data/lib/arel/attributes/attribute.rb +0 -8
  279. data/lib/arel/collectors/bind.rb +2 -0
  280. data/lib/arel/collectors/composite.rb +7 -0
  281. data/lib/arel/collectors/sql_string.rb +1 -1
  282. data/lib/arel/collectors/substitute_binds.rb +1 -1
  283. data/lib/arel/crud.rb +28 -22
  284. data/lib/arel/delete_manager.rb +18 -4
  285. data/lib/arel/errors.rb +10 -0
  286. data/lib/arel/factory_methods.rb +4 -0
  287. data/lib/arel/filter_predications.rb +9 -0
  288. data/lib/arel/insert_manager.rb +2 -3
  289. data/lib/arel/nodes/binary.rb +6 -7
  290. data/lib/arel/nodes/bound_sql_literal.rb +65 -0
  291. data/lib/arel/nodes/casted.rb +1 -1
  292. data/lib/arel/nodes/cte.rb +36 -0
  293. data/lib/arel/nodes/delete_statement.rb +12 -13
  294. data/lib/arel/nodes/filter.rb +10 -0
  295. data/lib/arel/nodes/fragments.rb +35 -0
  296. data/lib/arel/nodes/function.rb +1 -0
  297. data/lib/arel/nodes/homogeneous_in.rb +1 -9
  298. data/lib/arel/nodes/insert_statement.rb +2 -2
  299. data/lib/arel/nodes/leading_join.rb +8 -0
  300. data/lib/arel/nodes/{and.rb → nary.rb} +9 -2
  301. data/lib/arel/nodes/node.rb +115 -5
  302. data/lib/arel/nodes/select_core.rb +2 -2
  303. data/lib/arel/nodes/select_statement.rb +2 -2
  304. data/lib/arel/nodes/sql_literal.rb +13 -0
  305. data/lib/arel/nodes/table_alias.rb +4 -0
  306. data/lib/arel/nodes/update_statement.rb +8 -3
  307. data/lib/arel/nodes.rb +7 -2
  308. data/lib/arel/predications.rb +14 -4
  309. data/lib/arel/select_manager.rb +11 -5
  310. data/lib/arel/table.rb +9 -6
  311. data/lib/arel/tree_manager.rb +8 -15
  312. data/lib/arel/update_manager.rb +20 -5
  313. data/lib/arel/visitors/dot.rb +81 -90
  314. data/lib/arel/visitors/mysql.rb +23 -5
  315. data/lib/arel/visitors/postgresql.rb +1 -22
  316. data/lib/arel/visitors/sqlite.rb +25 -0
  317. data/lib/arel/visitors/to_sql.rb +170 -36
  318. data/lib/arel/visitors/visitor.rb +2 -2
  319. data/lib/arel.rb +23 -4
  320. data/lib/rails/generators/active_record/application_record/USAGE +8 -0
  321. data/lib/rails/generators/active_record/application_record/templates/application_record.rb.tt +1 -1
  322. data/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt +4 -1
  323. data/lib/rails/generators/active_record/migration.rb +3 -1
  324. data/lib/rails/generators/active_record/model/USAGE +113 -0
  325. data/lib/rails/generators/active_record/model/model_generator.rb +15 -6
  326. data/lib/rails/generators/active_record/model/templates/abstract_base_class.rb.tt +1 -1
  327. data/lib/rails/generators/active_record/model/templates/model.rb.tt +1 -1
  328. data/lib/rails/generators/active_record/model/templates/module.rb.tt +2 -2
  329. data/lib/rails/generators/active_record/multi_db/multi_db_generator.rb +16 -0
  330. data/lib/rails/generators/active_record/multi_db/templates/multi_db.rb.tt +44 -0
  331. metadata +103 -17
  332. data/lib/active_record/connection_adapters/legacy_pool_manager.rb +0 -35
  333. 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,153 @@
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 = if association
13
+ begin
14
+ @association = association.to_sym
15
+ rescue NoMethodError
16
+ raise ArgumentError, "Association names must be Symbol or String, got: #{association.class.name}"
17
+ end
18
+ end
19
+ @parent = parent
20
+ @scope = scope
21
+ @associate_by_default = associate_by_default
22
+
23
+ @children = build_children(children)
24
+ @loaders = nil
25
+ end
26
+
27
+ def future_classes
28
+ (immediate_future_classes + children.flat_map(&:future_classes)).uniq
29
+ end
30
+
31
+ def immediate_future_classes
32
+ if parent.done?
33
+ loaders.flat_map(&:future_classes).uniq
34
+ else
35
+ likely_reflections.reject(&:polymorphic?).flat_map do |reflection|
36
+ reflection.
37
+ chain.
38
+ map(&:klass)
39
+ end.uniq
40
+ end
41
+ end
42
+
43
+ def target_classes
44
+ if done?
45
+ preloaded_records.map(&:klass).uniq
46
+ elsif parent.done?
47
+ loaders.map(&:klass).uniq
48
+ else
49
+ likely_reflections.reject(&:polymorphic?).map(&:klass).uniq
50
+ end
51
+ end
52
+
53
+ def likely_reflections
54
+ parent_classes = parent.target_classes
55
+ parent_classes.filter_map do |parent_klass|
56
+ parent_klass._reflect_on_association(@association)
57
+ end
58
+ end
59
+
60
+ def root?
61
+ parent.nil?
62
+ end
63
+
64
+ def source_records
65
+ @parent.preloaded_records
66
+ end
67
+
68
+ def preloaded_records
69
+ @preloaded_records ||= loaders.flat_map(&:preloaded_records)
70
+ end
71
+
72
+ def done?
73
+ root? || (@loaders && @loaders.all?(&:run?))
74
+ end
75
+
76
+ def runnable_loaders
77
+ loaders.flat_map(&:runnable_loaders).reject(&:run?)
78
+ end
79
+
80
+ def grouped_records
81
+ h = {}
82
+ polymorphic_parent = !root? && parent.polymorphic?
83
+ source_records.each do |record|
84
+ reflection = record.class._reflect_on_association(association)
85
+ next if polymorphic_parent && !reflection || !record.association(association).klass
86
+ (h[reflection] ||= []) << record
87
+ end
88
+ h
89
+ end
90
+
91
+ def preloaders_for_reflection(reflection, reflection_records)
92
+ reflection_records.group_by do |record|
93
+ klass = record.association(association).klass
94
+
95
+ if reflection.scope && reflection.scope.arity != 0
96
+ # For instance dependent scopes, the scope is potentially
97
+ # different for each record. To allow this we'll group each
98
+ # object separately into its own preloader
99
+ reflection_scope = reflection.join_scopes(klass.arel_table, klass.predicate_builder, klass, record).inject(&:merge!)
100
+ end
101
+
102
+ [klass, reflection_scope]
103
+ end.map do |(rhs_klass, reflection_scope), rs|
104
+ preloader_for(reflection).new(rhs_klass, rs, reflection, scope, reflection_scope, associate_by_default)
105
+ end
106
+ end
107
+
108
+ def polymorphic?
109
+ return false if root?
110
+ return @polymorphic if defined?(@polymorphic)
111
+
112
+ @polymorphic = source_records.any? do |record|
113
+ reflection = record.class._reflect_on_association(association)
114
+ reflection && reflection.options[:polymorphic]
115
+ end
116
+ end
117
+
118
+ def loaders
119
+ @loaders ||=
120
+ grouped_records.flat_map do |reflection, reflection_records|
121
+ preloaders_for_reflection(reflection, reflection_records)
122
+ end
123
+ end
124
+
125
+ private
126
+ def build_children(children)
127
+ Array.wrap(children).flat_map { |association|
128
+ Array(association).flat_map { |parent, child|
129
+ Branch.new(
130
+ parent: self,
131
+ association: parent,
132
+ children: child,
133
+ associate_by_default: associate_by_default,
134
+ scope: scope
135
+ )
136
+ }
137
+ }
138
+ end
139
+
140
+ # Returns a class containing the logic needed to load preload the data
141
+ # and attach it to a relation. The class returned implements a `run` method
142
+ # that accepts a preloader.
143
+ def preloader_for(reflection)
144
+ if reflection.options[:through]
145
+ ThroughAssociation
146
+ else
147
+ Association
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
@@ -4,26 +4,20 @@ 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
- 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)
12
+ @records_by_owner ||= owners.each_with_object({}) do |owner, result|
13
+ if loaded?(owner)
14
+ result[owner] = target_for(owner)
15
+ next
16
+ end
22
17
 
23
- @records_by_owner = owners.each_with_object({}) do |owner, result|
24
18
  through_records = through_records_by_owner[owner] || []
25
19
 
26
- if @already_loaded
20
+ if owners.first.association(through_reflection.name).loaded?
27
21
  if source_type = reflection.options[:source_type]
28
22
  through_records = through_records.select do |record|
29
23
  record[reflection.foreign_type] == source_type
@@ -42,17 +36,47 @@ module ActiveRecord
42
36
  end
43
37
  end
44
38
 
39
+ def runnable_loaders
40
+ if data_available?
41
+ [self]
42
+ elsif through_preloaders.all?(&:run?)
43
+ source_preloaders.flat_map(&:runnable_loaders)
44
+ else
45
+ through_preloaders.flat_map(&:runnable_loaders)
46
+ end
47
+ end
48
+
49
+ def future_classes
50
+ if run?
51
+ []
52
+ elsif through_preloaders.all?(&:run?)
53
+ source_preloaders.flat_map(&:future_classes).uniq
54
+ else
55
+ through_classes = through_preloaders.flat_map(&:future_classes)
56
+ source_classes = source_reflection.
57
+ chain.
58
+ reject { |reflection| reflection.respond_to?(:polymorphic?) && reflection.polymorphic? }.
59
+ map(&:klass)
60
+ (through_classes + source_classes).uniq
61
+ end
62
+ end
63
+
45
64
  private
65
+ def data_available?
66
+ owners.all? { |owner| loaded?(owner) } ||
67
+ through_preloaders.all?(&:run?) && source_preloaders.all?(&:run?)
68
+ end
69
+
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
51
- through_preloaders.flat_map(&:preloaded_records)
75
+ through_records_by_owner.values.flatten
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
@@ -63,6 +87,14 @@ module ActiveRecord
63
87
  reflection.source_reflection
64
88
  end
65
89
 
90
+ def source_records_by_owner
91
+ @source_records_by_owner ||= source_preloaders.map(&:records_by_owner).reduce(:merge)
92
+ end
93
+
94
+ def through_records_by_owner
95
+ @through_records_by_owner ||= through_preloaders.map(&:records_by_owner).reduce(:merge)
96
+ end
97
+
66
98
  def preload_index
67
99
  @preload_index ||= preloaded_records.each_with_object({}).with_index do |(record, result), index|
68
100
  result[record] = index
@@ -73,6 +105,8 @@ module ActiveRecord
73
105
  scope = through_reflection.klass.unscoped
74
106
  options = reflection.options
75
107
 
108
+ return scope if options[:disable_joins]
109
+
76
110
  values = reflection_scope.values
77
111
  if annotations = values[:annotate]
78
112
  scope.annotate!(*annotations)
@@ -108,7 +142,7 @@ module ActiveRecord
108
142
  end
109
143
  end
110
144
 
111
- scope
145
+ cascade_strict_loading(scope)
112
146
  end
113
147
  end
114
148
  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
@@ -12,6 +14,12 @@ module ActiveRecord
12
14
  target
13
15
  end
14
16
 
17
+ # Resets the \loaded flag to +false+ and sets the \target to +nil+.
18
+ def reset
19
+ super
20
+ @target = nil
21
+ end
22
+
15
23
  # Implements the writer method, e.g. foo.bar= for Foo.belongs_to :bar
16
24
  def writer(record)
17
25
  replace(record)
@@ -32,11 +40,15 @@ module ActiveRecord
32
40
 
33
41
  private
34
42
  def scope_for_create
35
- super.except!(klass.primary_key)
43
+ super.except!(*Array(klass.primary_key))
36
44
  end
37
45
 
38
46
  def find_target
39
- super.first
47
+ if disable_joins
48
+ scope.first
49
+ else
50
+ super.first
51
+ end
40
52
  end
41
53
 
42
54
  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]
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