activerecord 6.1.7 → 7.1.0

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 (307) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1516 -1019
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +17 -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 +50 -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 +35 -31
  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.rb +26 -16
  27. data/lib/active_record/associations/preloader/association.rb +207 -52
  28. data/lib/active_record/associations/preloader/batch.rb +48 -0
  29. data/lib/active_record/associations/preloader/branch.rb +147 -0
  30. data/lib/active_record/associations/preloader/through_association.rb +50 -14
  31. data/lib/active_record/associations/preloader.rb +50 -121
  32. data/lib/active_record/associations/singular_association.rb +9 -3
  33. data/lib/active_record/associations/through_association.rb +25 -14
  34. data/lib/active_record/associations.rb +423 -289
  35. data/lib/active_record/asynchronous_queries_tracker.rb +60 -0
  36. data/lib/active_record/attribute_assignment.rb +1 -3
  37. data/lib/active_record/attribute_methods/before_type_cast.rb +24 -2
  38. data/lib/active_record/attribute_methods/dirty.rb +61 -14
  39. data/lib/active_record/attribute_methods/primary_key.rb +78 -26
  40. data/lib/active_record/attribute_methods/query.rb +31 -19
  41. data/lib/active_record/attribute_methods/read.rb +25 -10
  42. data/lib/active_record/attribute_methods/serialization.rb +194 -37
  43. data/lib/active_record/attribute_methods/time_zone_conversion.rb +4 -3
  44. data/lib/active_record/attribute_methods/write.rb +10 -13
  45. data/lib/active_record/attribute_methods.rb +121 -40
  46. data/lib/active_record/attributes.rb +27 -38
  47. data/lib/active_record/autosave_association.rb +61 -30
  48. data/lib/active_record/base.rb +25 -2
  49. data/lib/active_record/callbacks.rb +18 -34
  50. data/lib/active_record/coders/column_serializer.rb +61 -0
  51. data/lib/active_record/coders/json.rb +1 -1
  52. data/lib/active_record/coders/yaml_column.rb +70 -46
  53. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +367 -0
  54. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +211 -0
  55. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +78 -0
  56. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +96 -590
  57. data/lib/active_record/connection_adapters/abstract/database_limits.rb +5 -17
  58. data/lib/active_record/connection_adapters/abstract/database_statements.rb +171 -51
  59. data/lib/active_record/connection_adapters/abstract/query_cache.rb +77 -27
  60. data/lib/active_record/connection_adapters/abstract/quoting.rb +87 -73
  61. data/lib/active_record/connection_adapters/abstract/savepoints.rb +4 -3
  62. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +21 -20
  63. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +186 -31
  64. data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +14 -1
  65. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +360 -136
  66. data/lib/active_record/connection_adapters/abstract/transaction.rb +281 -59
  67. data/lib/active_record/connection_adapters/abstract_adapter.rb +622 -149
  68. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +285 -156
  69. data/lib/active_record/connection_adapters/column.rb +13 -0
  70. data/lib/active_record/connection_adapters/mysql/column.rb +1 -0
  71. data/lib/active_record/connection_adapters/mysql/database_statements.rb +25 -134
  72. data/lib/active_record/connection_adapters/mysql/quoting.rb +56 -25
  73. data/lib/active_record/connection_adapters/mysql/schema_creation.rb +9 -0
  74. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +10 -1
  75. data/lib/active_record/connection_adapters/mysql/schema_dumper.rb +8 -2
  76. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +38 -14
  77. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +148 -0
  78. data/lib/active_record/connection_adapters/mysql2_adapter.rb +104 -53
  79. data/lib/active_record/connection_adapters/pool_config.rb +20 -11
  80. data/lib/active_record/connection_adapters/pool_manager.rb +19 -9
  81. data/lib/active_record/connection_adapters/postgresql/column.rb +18 -1
  82. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +86 -52
  83. data/lib/active_record/connection_adapters/postgresql/oid/array.rb +1 -1
  84. data/lib/active_record/connection_adapters/postgresql/oid/date.rb +8 -0
  85. data/lib/active_record/connection_adapters/postgresql/oid/date_time.rb +5 -0
  86. data/lib/active_record/connection_adapters/postgresql/oid/hstore.rb +53 -14
  87. data/lib/active_record/connection_adapters/postgresql/oid/range.rb +12 -3
  88. data/lib/active_record/connection_adapters/postgresql/oid/timestamp.rb +15 -0
  89. data/lib/active_record/connection_adapters/postgresql/oid/timestamp_with_time_zone.rb +30 -0
  90. data/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +18 -6
  91. data/lib/active_record/connection_adapters/postgresql/oid.rb +2 -0
  92. data/lib/active_record/connection_adapters/postgresql/quoting.rb +89 -56
  93. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +28 -0
  94. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +92 -2
  95. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +153 -3
  96. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +78 -0
  97. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +381 -69
  98. data/lib/active_record/connection_adapters/postgresql/utils.rb +9 -10
  99. data/lib/active_record/connection_adapters/postgresql_adapter.rb +492 -230
  100. data/lib/active_record/connection_adapters/schema_cache.rb +319 -90
  101. data/lib/active_record/connection_adapters/sqlite3/column.rb +49 -0
  102. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +65 -53
  103. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +37 -21
  104. data/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb +7 -0
  105. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +43 -22
  106. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +294 -102
  107. data/lib/active_record/connection_adapters/statement_pool.rb +7 -0
  108. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +98 -0
  109. data/lib/active_record/connection_adapters/trilogy_adapter.rb +254 -0
  110. data/lib/active_record/connection_adapters.rb +9 -6
  111. data/lib/active_record/connection_handling.rb +107 -136
  112. data/lib/active_record/core.rb +194 -224
  113. data/lib/active_record/counter_cache.rb +46 -25
  114. data/lib/active_record/database_configurations/connection_url_resolver.rb +2 -1
  115. data/lib/active_record/database_configurations/database_config.rb +21 -12
  116. data/lib/active_record/database_configurations/hash_config.rb +84 -16
  117. data/lib/active_record/database_configurations/url_config.rb +18 -12
  118. data/lib/active_record/database_configurations.rb +95 -59
  119. data/lib/active_record/delegated_type.rb +61 -15
  120. data/lib/active_record/deprecator.rb +7 -0
  121. data/lib/active_record/destroy_association_async_job.rb +3 -1
  122. data/lib/active_record/disable_joins_association_relation.rb +39 -0
  123. data/lib/active_record/dynamic_matchers.rb +1 -1
  124. data/lib/active_record/encryption/auto_filtered_parameters.rb +66 -0
  125. data/lib/active_record/encryption/cipher/aes256_gcm.rb +101 -0
  126. data/lib/active_record/encryption/cipher.rb +53 -0
  127. data/lib/active_record/encryption/config.rb +68 -0
  128. data/lib/active_record/encryption/configurable.rb +60 -0
  129. data/lib/active_record/encryption/context.rb +42 -0
  130. data/lib/active_record/encryption/contexts.rb +76 -0
  131. data/lib/active_record/encryption/derived_secret_key_provider.rb +18 -0
  132. data/lib/active_record/encryption/deterministic_key_provider.rb +14 -0
  133. data/lib/active_record/encryption/encryptable_record.rb +224 -0
  134. data/lib/active_record/encryption/encrypted_attribute_type.rb +151 -0
  135. data/lib/active_record/encryption/encrypted_fixtures.rb +38 -0
  136. data/lib/active_record/encryption/encrypting_only_encryptor.rb +12 -0
  137. data/lib/active_record/encryption/encryptor.rb +155 -0
  138. data/lib/active_record/encryption/envelope_encryption_key_provider.rb +55 -0
  139. data/lib/active_record/encryption/errors.rb +15 -0
  140. data/lib/active_record/encryption/extended_deterministic_queries.rb +172 -0
  141. data/lib/active_record/encryption/extended_deterministic_uniqueness_validator.rb +28 -0
  142. data/lib/active_record/encryption/key.rb +28 -0
  143. data/lib/active_record/encryption/key_generator.rb +53 -0
  144. data/lib/active_record/encryption/key_provider.rb +46 -0
  145. data/lib/active_record/encryption/message.rb +33 -0
  146. data/lib/active_record/encryption/message_serializer.rb +92 -0
  147. data/lib/active_record/encryption/null_encryptor.rb +21 -0
  148. data/lib/active_record/encryption/properties.rb +76 -0
  149. data/lib/active_record/encryption/read_only_null_encryptor.rb +24 -0
  150. data/lib/active_record/encryption/scheme.rb +96 -0
  151. data/lib/active_record/encryption.rb +56 -0
  152. data/lib/active_record/enum.rb +156 -62
  153. data/lib/active_record/errors.rb +171 -15
  154. data/lib/active_record/explain.rb +23 -3
  155. data/lib/active_record/explain_registry.rb +11 -6
  156. data/lib/active_record/explain_subscriber.rb +1 -1
  157. data/lib/active_record/fixture_set/file.rb +15 -1
  158. data/lib/active_record/fixture_set/model_metadata.rb +14 -4
  159. data/lib/active_record/fixture_set/render_context.rb +2 -0
  160. data/lib/active_record/fixture_set/table_row.rb +70 -14
  161. data/lib/active_record/fixture_set/table_rows.rb +4 -4
  162. data/lib/active_record/fixtures.rb +131 -86
  163. data/lib/active_record/future_result.rb +164 -0
  164. data/lib/active_record/gem_version.rb +3 -3
  165. data/lib/active_record/inheritance.rb +81 -29
  166. data/lib/active_record/insert_all.rb +133 -20
  167. data/lib/active_record/integration.rb +11 -10
  168. data/lib/active_record/internal_metadata.rb +117 -33
  169. data/lib/active_record/legacy_yaml_adapter.rb +2 -39
  170. data/lib/active_record/locking/optimistic.rb +36 -21
  171. data/lib/active_record/locking/pessimistic.rb +15 -6
  172. data/lib/active_record/log_subscriber.rb +52 -19
  173. data/lib/active_record/marshalling.rb +56 -0
  174. data/lib/active_record/message_pack.rb +124 -0
  175. data/lib/active_record/middleware/database_selector/resolver.rb +10 -10
  176. data/lib/active_record/middleware/database_selector.rb +23 -13
  177. data/lib/active_record/middleware/shard_selector.rb +62 -0
  178. data/lib/active_record/migration/command_recorder.rb +108 -13
  179. data/lib/active_record/migration/compatibility.rb +221 -48
  180. data/lib/active_record/migration/default_strategy.rb +23 -0
  181. data/lib/active_record/migration/execution_strategy.rb +19 -0
  182. data/lib/active_record/migration/join_table.rb +1 -1
  183. data/lib/active_record/migration.rb +355 -171
  184. data/lib/active_record/model_schema.rb +116 -97
  185. data/lib/active_record/nested_attributes.rb +36 -15
  186. data/lib/active_record/no_touching.rb +3 -3
  187. data/lib/active_record/normalization.rb +159 -0
  188. data/lib/active_record/persistence.rb +405 -85
  189. data/lib/active_record/promise.rb +84 -0
  190. data/lib/active_record/query_cache.rb +3 -21
  191. data/lib/active_record/query_logs.rb +174 -0
  192. data/lib/active_record/query_logs_formatter.rb +41 -0
  193. data/lib/active_record/querying.rb +29 -6
  194. data/lib/active_record/railtie.rb +219 -43
  195. data/lib/active_record/railties/controller_runtime.rb +13 -9
  196. data/lib/active_record/railties/databases.rake +185 -249
  197. data/lib/active_record/railties/job_runtime.rb +23 -0
  198. data/lib/active_record/readonly_attributes.rb +41 -3
  199. data/lib/active_record/reflection.rb +229 -80
  200. data/lib/active_record/relation/batches/batch_enumerator.rb +23 -7
  201. data/lib/active_record/relation/batches.rb +192 -63
  202. data/lib/active_record/relation/calculations.rb +211 -90
  203. data/lib/active_record/relation/delegation.rb +27 -13
  204. data/lib/active_record/relation/finder_methods.rb +108 -51
  205. data/lib/active_record/relation/merger.rb +22 -13
  206. data/lib/active_record/relation/predicate_builder/association_query_value.rb +31 -3
  207. data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +4 -6
  208. data/lib/active_record/relation/predicate_builder/relation_handler.rb +5 -1
  209. data/lib/active_record/relation/predicate_builder.rb +27 -20
  210. data/lib/active_record/relation/query_attribute.rb +30 -12
  211. data/lib/active_record/relation/query_methods.rb +654 -127
  212. data/lib/active_record/relation/record_fetch_warning.rb +7 -9
  213. data/lib/active_record/relation/spawn_methods.rb +20 -3
  214. data/lib/active_record/relation/where_clause.rb +10 -19
  215. data/lib/active_record/relation.rb +262 -120
  216. data/lib/active_record/result.rb +37 -11
  217. data/lib/active_record/runtime_registry.rb +18 -13
  218. data/lib/active_record/sanitization.rb +65 -20
  219. data/lib/active_record/schema.rb +36 -22
  220. data/lib/active_record/schema_dumper.rb +73 -24
  221. data/lib/active_record/schema_migration.rb +68 -33
  222. data/lib/active_record/scoping/default.rb +72 -15
  223. data/lib/active_record/scoping/named.rb +5 -13
  224. data/lib/active_record/scoping.rb +65 -34
  225. data/lib/active_record/secure_password.rb +60 -0
  226. data/lib/active_record/secure_token.rb +21 -3
  227. data/lib/active_record/serialization.rb +6 -1
  228. data/lib/active_record/signed_id.rb +10 -8
  229. data/lib/active_record/store.rb +10 -10
  230. data/lib/active_record/suppressor.rb +13 -15
  231. data/lib/active_record/table_metadata.rb +16 -3
  232. data/lib/active_record/tasks/database_tasks.rb +225 -136
  233. data/lib/active_record/tasks/mysql_database_tasks.rb +16 -7
  234. data/lib/active_record/tasks/postgresql_database_tasks.rb +35 -26
  235. data/lib/active_record/tasks/sqlite_database_tasks.rb +15 -7
  236. data/lib/active_record/test_databases.rb +1 -1
  237. data/lib/active_record/test_fixtures.rb +116 -96
  238. data/lib/active_record/timestamp.rb +28 -17
  239. data/lib/active_record/token_for.rb +113 -0
  240. data/lib/active_record/touch_later.rb +11 -6
  241. data/lib/active_record/transactions.rb +48 -27
  242. data/lib/active_record/translation.rb +3 -3
  243. data/lib/active_record/type/adapter_specific_registry.rb +32 -14
  244. data/lib/active_record/type/hash_lookup_type_map.rb +34 -1
  245. data/lib/active_record/type/internal/timezone.rb +7 -2
  246. data/lib/active_record/type/serialized.rb +9 -5
  247. data/lib/active_record/type/time.rb +4 -0
  248. data/lib/active_record/type/type_map.rb +17 -20
  249. data/lib/active_record/type.rb +1 -2
  250. data/lib/active_record/validations/absence.rb +1 -1
  251. data/lib/active_record/validations/associated.rb +4 -4
  252. data/lib/active_record/validations/numericality.rb +5 -4
  253. data/lib/active_record/validations/presence.rb +5 -28
  254. data/lib/active_record/validations/uniqueness.rb +51 -6
  255. data/lib/active_record/validations.rb +8 -4
  256. data/lib/active_record/version.rb +1 -1
  257. data/lib/active_record.rb +335 -32
  258. data/lib/arel/attributes/attribute.rb +0 -8
  259. data/lib/arel/crud.rb +28 -22
  260. data/lib/arel/delete_manager.rb +18 -4
  261. data/lib/arel/errors.rb +10 -0
  262. data/lib/arel/factory_methods.rb +4 -0
  263. data/lib/arel/filter_predications.rb +9 -0
  264. data/lib/arel/insert_manager.rb +2 -3
  265. data/lib/arel/nodes/and.rb +4 -0
  266. data/lib/arel/nodes/binary.rb +6 -1
  267. data/lib/arel/nodes/bound_sql_literal.rb +61 -0
  268. data/lib/arel/nodes/casted.rb +1 -1
  269. data/lib/arel/nodes/cte.rb +36 -0
  270. data/lib/arel/nodes/delete_statement.rb +12 -13
  271. data/lib/arel/nodes/filter.rb +10 -0
  272. data/lib/arel/nodes/fragments.rb +35 -0
  273. data/lib/arel/nodes/function.rb +1 -0
  274. data/lib/arel/nodes/homogeneous_in.rb +0 -8
  275. data/lib/arel/nodes/insert_statement.rb +2 -2
  276. data/lib/arel/nodes/leading_join.rb +8 -0
  277. data/lib/arel/nodes/node.rb +111 -2
  278. data/lib/arel/nodes/select_core.rb +2 -2
  279. data/lib/arel/nodes/select_statement.rb +2 -2
  280. data/lib/arel/nodes/sql_literal.rb +6 -0
  281. data/lib/arel/nodes/table_alias.rb +4 -0
  282. data/lib/arel/nodes/update_statement.rb +8 -3
  283. data/lib/arel/nodes.rb +5 -0
  284. data/lib/arel/predications.rb +13 -3
  285. data/lib/arel/select_manager.rb +10 -4
  286. data/lib/arel/table.rb +9 -6
  287. data/lib/arel/tree_manager.rb +0 -12
  288. data/lib/arel/update_manager.rb +18 -4
  289. data/lib/arel/visitors/dot.rb +80 -90
  290. data/lib/arel/visitors/mysql.rb +16 -3
  291. data/lib/arel/visitors/postgresql.rb +0 -10
  292. data/lib/arel/visitors/to_sql.rb +139 -19
  293. data/lib/arel/visitors/visitor.rb +2 -2
  294. data/lib/arel.rb +18 -3
  295. data/lib/rails/generators/active_record/application_record/USAGE +8 -0
  296. data/lib/rails/generators/active_record/application_record/templates/application_record.rb.tt +1 -1
  297. data/lib/rails/generators/active_record/migration.rb +3 -1
  298. data/lib/rails/generators/active_record/model/USAGE +113 -0
  299. data/lib/rails/generators/active_record/model/model_generator.rb +15 -6
  300. data/lib/rails/generators/active_record/model/templates/abstract_base_class.rb.tt +1 -1
  301. data/lib/rails/generators/active_record/model/templates/model.rb.tt +1 -1
  302. data/lib/rails/generators/active_record/model/templates/module.rb.tt +2 -2
  303. data/lib/rails/generators/active_record/multi_db/multi_db_generator.rb +16 -0
  304. data/lib/rails/generators/active_record/multi_db/templates/multi_db.rb.tt +44 -0
  305. metadata +92 -13
  306. data/lib/active_record/connection_adapters/legacy_pool_manager.rb +0 -35
  307. data/lib/active_record/null_relation.rb +0 -67
@@ -3,11 +3,12 @@
3
3
  module ActiveRecord
4
4
  module Associations
5
5
  # = Active Record Has Many Association
6
+ #
6
7
  # This is the proxy that handles a has many association.
7
8
  #
8
9
  # If the association has a <tt>:through</tt> option further specialization
9
10
  # is provided by its child HasManyThroughAssociation.
10
- class HasManyAssociation < CollectionAssociation #:nodoc:
11
+ class HasManyAssociation < CollectionAssociation # :nodoc:
11
12
  include ForeignAssociation
12
13
 
13
14
  def handle_dependency
@@ -33,20 +34,24 @@ module ActiveRecord
33
34
 
34
35
  unless target.empty?
35
36
  association_class = target.first.class
36
- primary_key_column = association_class.primary_key.to_sym
37
-
38
- ids = target.collect do |assoc|
39
- assoc.public_send(primary_key_column)
37
+ if association_class.query_constraints_list
38
+ primary_key_column = association_class.query_constraints_list.map(&:to_sym)
39
+ ids = target.collect { |assoc| primary_key_column.map { |col| assoc.public_send(col) } }
40
+ else
41
+ primary_key_column = association_class.primary_key.to_sym
42
+ ids = target.collect { |assoc| assoc.public_send(primary_key_column) }
40
43
  end
41
44
 
42
- enqueue_destroy_association(
43
- owner_model_name: owner.class.to_s,
44
- owner_id: owner.id,
45
- association_class: reflection.klass.to_s,
46
- association_ids: ids,
47
- association_primary_key_column: primary_key_column,
48
- ensuring_owner_was_method: options.fetch(:ensuring_owner_was, nil)
49
- )
45
+ ids.each_slice(owner.class.destroy_association_async_batch_size || ids.size) do |ids_batch|
46
+ enqueue_destroy_association(
47
+ owner_model_name: owner.class.to_s,
48
+ owner_id: owner.id,
49
+ association_class: reflection.klass.to_s,
50
+ association_ids: ids_batch,
51
+ association_primary_key_column: primary_key_column,
52
+ ensuring_owner_was_method: options.fetch(:ensuring_owner_was, nil)
53
+ )
54
+ end
50
55
  end
51
56
  else
52
57
  delete_all
@@ -79,10 +84,13 @@ module ActiveRecord
79
84
  scope.count(:all)
80
85
  end
81
86
 
82
- # If there's nothing in the database and @target has no new records
83
- # we are certain the current target is an empty array. This is a
84
- # documented side-effect of the method that may avoid an extra SELECT.
85
- loaded! if count == 0
87
+ # If there's nothing in the database, @target should only contain new
88
+ # records or be an empty array. This is a documented side-effect of
89
+ # the method that may avoid an extra SELECT.
90
+ if count == 0
91
+ target.select!(&:new_record?)
92
+ loaded!
93
+ end
86
94
 
87
95
  [association_scope.limit_value, count].compact.min
88
96
  end
@@ -121,7 +129,9 @@ module ActiveRecord
121
129
  records.each(&:destroy!)
122
130
  update_counter(-records.length) unless reflection.inverse_updates_counter_cache?
123
131
  else
124
- scope = self.scope.where(reflection.klass.primary_key => records)
132
+ query_constraints = reflection.klass.composite_query_constraints_list
133
+ values = records.map { |r| query_constraints.map { |col| r._read_attribute(col) } }
134
+ scope = self.scope.where(query_constraints => values)
125
135
  update_counter(-delete_count(method, scope))
126
136
  end
127
137
  end
@@ -3,7 +3,7 @@
3
3
  module ActiveRecord
4
4
  module Associations
5
5
  # = Active Record Has Many Through Association
6
- class HasManyThroughAssociation < HasManyAssociation #:nodoc:
6
+ class HasManyThroughAssociation < HasManyAssociation # :nodoc:
7
7
  include ThroughAssociation
8
8
 
9
9
  def initialize(owner, reflection)
@@ -59,9 +59,10 @@ module ActiveRecord
59
59
 
60
60
  attributes = through_scope_attributes
61
61
  attributes[source_reflection.name] = record
62
- attributes[source_reflection.foreign_type] = options[:source_type] if options[:source_type]
63
62
 
64
- through_association.build(attributes)
63
+ through_association.build(attributes).tap do |new_record|
64
+ new_record.send("#{source_reflection.foreign_type}=", options[:source_type]) if options[:source_type]
65
+ end
65
66
  end
66
67
  end
67
68
 
@@ -69,9 +70,12 @@ module ActiveRecord
69
70
 
70
71
  def through_scope_attributes
71
72
  scope = through_scope || self.scope
72
- scope.where_values_hash(through_association.reflection.name.to_s).
73
- except!(through_association.reflection.foreign_key,
74
- through_association.reflection.klass.inheritance_column)
73
+ attributes = scope.where_values_hash(through_association.reflection.klass.table_name)
74
+ except_keys = [
75
+ *Array(through_association.reflection.foreign_key),
76
+ through_association.reflection.klass.inheritance_column
77
+ ]
78
+ attributes.except!(*except_keys)
75
79
  end
76
80
 
77
81
  def save_through_record(record)
@@ -109,7 +113,7 @@ module ActiveRecord
109
113
  end
110
114
 
111
115
  def target_reflection_has_associated_record?
112
- !(through_reflection.belongs_to? && owner[through_reflection.foreign_key].blank?)
116
+ !(through_reflection.belongs_to? && Array(through_reflection.foreign_key).all? { |foreign_key_column| owner[foreign_key_column].blank? })
113
117
  end
114
118
 
115
119
  def update_through_counter?(method)
@@ -214,6 +218,7 @@ module ActiveRecord
214
218
 
215
219
  def find_target
216
220
  return [] unless target_reflection_has_associated_record?
221
+ return scope.to_a if disable_joins
217
222
  super
218
223
  end
219
224
 
@@ -3,7 +3,7 @@
3
3
  module ActiveRecord
4
4
  module Associations
5
5
  # = Active Record Has One Association
6
- class HasOneAssociation < SingularAssociation #:nodoc:
6
+ class HasOneAssociation < SingularAssociation # :nodoc:
7
7
  include ForeignAssociation
8
8
 
9
9
  def handle_dependency
@@ -33,8 +33,13 @@ module ActiveRecord
33
33
  target.destroy
34
34
  throw(:abort) unless target.destroyed?
35
35
  when :destroy_async
36
- primary_key_column = target.class.primary_key.to_sym
37
- id = target.public_send(primary_key_column)
36
+ if target.class.query_constraints_list
37
+ primary_key_column = target.class.query_constraints_list.map(&:to_sym)
38
+ id = primary_key_column.map { |col| target.public_send(col) }
39
+ else
40
+ primary_key_column = target.class.primary_key.to_sym
41
+ id = target.public_send(primary_key_column)
42
+ end
38
43
 
39
44
  enqueue_destroy_association(
40
45
  owner_model_name: owner.class.to_s,
@@ -70,7 +75,7 @@ module ActiveRecord
70
75
  if save && !record.save
71
76
  nullify_owner_attributes(record)
72
77
  set_owner_attributes(target) if target
73
- raise RecordNotSaved, "Failed to save the new associated #{reflection.name}."
78
+ raise RecordNotSaved.new("Failed to save the new associated #{reflection.name}.", record)
74
79
  end
75
80
  end
76
81
  end
@@ -102,19 +107,24 @@ module ActiveRecord
102
107
 
103
108
  if target.persisted? && owner.persisted? && !target.save
104
109
  set_owner_attributes(target)
105
- raise RecordNotSaved, "Failed to remove the existing associated #{reflection.name}. " \
106
- "The record failed to save after its foreign key was set to nil."
110
+ raise RecordNotSaved.new(
111
+ "Failed to remove the existing associated #{reflection.name}. " \
112
+ "The record failed to save after its foreign key was set to nil.",
113
+ target
114
+ )
107
115
  end
108
116
  end
109
117
  end
110
118
 
111
119
  def nullify_owner_attributes(record)
112
- record[reflection.foreign_key] = nil
120
+ Array(reflection.foreign_key).each do |foreign_key_column|
121
+ record[foreign_key_column] = nil unless foreign_key_column.in?(Array(record.class.primary_key))
122
+ end
113
123
  end
114
124
 
115
- def transaction_if(value)
125
+ def transaction_if(value, &block)
116
126
  if value
117
- reflection.klass.transaction { yield }
127
+ reflection.klass.transaction(&block)
118
128
  else
119
129
  yield
120
130
  end
@@ -122,7 +132,7 @@ module ActiveRecord
122
132
 
123
133
  def _create_record(attributes, raise_error = false, &block)
124
134
  unless owner.persisted?
125
- raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved"
135
+ raise ActiveRecord::RecordNotSaved.new("You cannot call create unless the parent is saved", owner)
126
136
  end
127
137
 
128
138
  super
@@ -3,7 +3,7 @@
3
3
  module ActiveRecord
4
4
  module Associations
5
5
  # = Active Record Has One Through Association
6
- class HasOneThroughAssociation < HasOneAssociation #:nodoc:
6
+ class HasOneThroughAssociation < HasOneAssociation # :nodoc:
7
7
  include ThroughAssociation
8
8
 
9
9
  private
@@ -3,8 +3,12 @@
3
3
  module ActiveRecord
4
4
  module Associations
5
5
  class JoinDependency # :nodoc:
6
- autoload :JoinBase, "active_record/associations/join_dependency/join_base"
7
- autoload :JoinAssociation, "active_record/associations/join_dependency/join_association"
6
+ extend ActiveSupport::Autoload
7
+
8
+ eager_autoload do
9
+ autoload :JoinBase
10
+ autoload :JoinAssociation
11
+ end
8
12
 
9
13
  class Aliases # :nodoc:
10
14
  def initialize(tables)
@@ -248,35 +252,41 @@ module ActiveRecord
248
252
  next
249
253
  end
250
254
 
251
- key = aliases.column_alias(node, node.primary_key)
252
- id = row[key]
253
- if id.nil?
255
+ if node.primary_key
256
+ keys = Array(node.primary_key).map { |column| aliases.column_alias(node, column) }
257
+ ids = keys.map { |key| row[key] }
258
+ else
259
+ keys = Array(node.reflection.join_primary_key).map { |column| aliases.column_alias(node, column.to_s) }
260
+ ids = keys.map { nil } # Avoid id-based model caching.
261
+ end
262
+
263
+ if keys.any? { |key| row[key].nil? }
254
264
  nil_association = ar_parent.association(node.reflection.name)
255
265
  nil_association.loaded!
256
266
  next
257
267
  end
258
268
 
259
- model = seen[ar_parent][node][id]
260
-
261
- if model
262
- construct(model, node, row, seen, model_cache, strict_loading_value)
263
- else
264
- model = construct_model(ar_parent, node, row, model_cache, id, strict_loading_value)
265
-
266
- seen[ar_parent][node][id] = model
267
- construct(model, node, row, seen, model_cache, strict_loading_value)
269
+ ids.each do |id|
270
+ unless model = seen[ar_parent][node][id]
271
+ model = construct_model(ar_parent, node, row, model_cache, id, strict_loading_value)
272
+ seen[ar_parent][node][id] = model if id
273
+ end
268
274
  end
275
+
276
+ construct(model, node, row, seen, model_cache, strict_loading_value)
269
277
  end
270
278
  end
271
279
 
272
280
  def construct_model(record, node, row, model_cache, id, strict_loading_value)
273
281
  other = record.association(node.reflection.name)
274
282
 
275
- model = model_cache[node][id] ||=
276
- node.instantiate(row, aliases.column_aliases(node)) do |m|
283
+ unless model = model_cache[node][id]
284
+ model = node.instantiate(row, aliases.column_aliases(node)) do |m|
277
285
  m.strict_loading! if strict_loading_value
278
286
  other.set_inverse_instance(m)
279
287
  end
288
+ model_cache[node][id] = model if id
289
+ end
280
290
 
281
291
  if node.reflection.collection?
282
292
  other.target.push(model)
@@ -1,19 +1,138 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # :enddoc:
4
+
3
5
  module ActiveRecord
4
6
  module Associations
5
7
  class Preloader
6
- class Association #:nodoc:
7
- def initialize(klass, owners, reflection, preload_scope, associate_by_default = true)
8
+ class Association # :nodoc:
9
+ class LoaderQuery
10
+ attr_reader :scope, :association_key_name
11
+
12
+ def initialize(scope, association_key_name)
13
+ @scope = scope
14
+ @association_key_name = association_key_name
15
+ end
16
+
17
+ def eql?(other)
18
+ association_key_name == other.association_key_name &&
19
+ scope.table_name == other.scope.table_name &&
20
+ scope.values_for_queries == other.scope.values_for_queries
21
+ end
22
+
23
+ def hash
24
+ [association_key_name, scope.table_name, scope.values_for_queries].hash
25
+ end
26
+
27
+ def records_for(loaders)
28
+ LoaderRecords.new(loaders, self).records
29
+ end
30
+
31
+ def load_records_in_batch(loaders)
32
+ raw_records = records_for(loaders)
33
+
34
+ loaders.each do |loader|
35
+ loader.load_records(raw_records)
36
+ loader.run
37
+ end
38
+ end
39
+
40
+ def load_records_for_keys(keys, &block)
41
+ if association_key_name.is_a?(Array)
42
+ query_constraints = Hash.new { |hsh, key| hsh[key] = Set.new }
43
+
44
+ keys.each_with_object(query_constraints) do |values_set, constraints|
45
+ association_key_name.zip(values_set).each do |key_name, value|
46
+ constraints[key_name] << value
47
+ end
48
+ end
49
+
50
+ scope.where(query_constraints)
51
+ else
52
+ scope.where(association_key_name => keys)
53
+ end.load(&block)
54
+ end
55
+ end
56
+
57
+ class LoaderRecords
58
+ def initialize(loaders, loader_query)
59
+ @loader_query = loader_query
60
+ @loaders = loaders
61
+ @keys_to_load = Set.new
62
+ @already_loaded_records_by_key = {}
63
+
64
+ populate_keys_to_load_and_already_loaded_records
65
+ end
66
+
67
+ def records
68
+ load_records + already_loaded_records
69
+ end
70
+
71
+ private
72
+ attr_reader :loader_query, :loaders, :keys_to_load, :already_loaded_records_by_key
73
+
74
+ def populate_keys_to_load_and_already_loaded_records
75
+ loaders.each do |loader|
76
+ loader.owners_by_key.each do |key, owners|
77
+ if loaded_owner = owners.find { |owner| loader.loaded?(owner) }
78
+ already_loaded_records_by_key[key] = loader.target_for(loaded_owner)
79
+ else
80
+ keys_to_load << key
81
+ end
82
+ end
83
+ end
84
+
85
+ @keys_to_load.subtract(already_loaded_records_by_key.keys)
86
+ end
87
+
88
+ def load_records
89
+ loader_query.load_records_for_keys(keys_to_load) do |record|
90
+ loaders.each { |l| l.set_inverse(record) }
91
+ end
92
+ end
93
+
94
+ def already_loaded_records
95
+ already_loaded_records_by_key.values.flatten
96
+ end
97
+ end
98
+
99
+ attr_reader :klass
100
+
101
+ def initialize(klass, owners, reflection, preload_scope, reflection_scope, associate_by_default)
8
102
  @klass = klass
9
103
  @owners = owners.uniq(&:__id__)
10
104
  @reflection = reflection
11
105
  @preload_scope = preload_scope
106
+ @reflection_scope = reflection_scope
12
107
  @associate = associate_by_default || !preload_scope || preload_scope.empty_scope?
13
108
  @model = owners.first && owners.first.class
109
+ @run = false
110
+ end
111
+
112
+ def table_name
113
+ @klass.table_name
114
+ end
115
+
116
+ def future_classes
117
+ if run?
118
+ []
119
+ else
120
+ [@klass]
121
+ end
122
+ end
123
+
124
+ def runnable_loaders
125
+ [self]
126
+ end
127
+
128
+ def run?
129
+ @run
14
130
  end
15
131
 
16
132
  def run
133
+ return self if run?
134
+ @run = true
135
+
17
136
  records = records_by_owner
18
137
 
19
138
  owners.each do |owner|
@@ -35,35 +154,85 @@ module ActiveRecord
35
154
  @preloaded_records
36
155
  end
37
156
 
38
- private
39
- attr_reader :owners, :reflection, :preload_scope, :model, :klass
157
+ # The name of the key on the associated records
158
+ def association_key_name
159
+ reflection.join_primary_key(klass)
160
+ end
161
+
162
+ def loader_query
163
+ LoaderQuery.new(scope, association_key_name)
164
+ end
165
+
166
+ def owners_by_key
167
+ @owners_by_key ||= owners.each_with_object({}) do |owner, result|
168
+ key = derive_key(owner, owner_key_name)
169
+ (result[key] ||= []) << owner if key
170
+ end
171
+ end
40
172
 
41
- def load_records
42
- # owners can be duplicated when a relation has a collection association join
43
- # #compare_by_identity makes such owners different hash keys
44
- @records_by_owner = {}.compare_by_identity
45
- raw_records = owner_keys.empty? ? [] : records_for(owner_keys)
173
+ def loaded?(owner)
174
+ owner.association(reflection.name).loaded?
175
+ end
46
176
 
47
- @preloaded_records = raw_records.select do |record|
48
- assignments = false
177
+ def target_for(owner)
178
+ Array.wrap(owner.association(reflection.name).target)
179
+ end
49
180
 
50
- owners_by_key[convert_key(record[association_key_name])].each do |owner|
51
- entries = (@records_by_owner[owner] ||= [])
181
+ def scope
182
+ @scope ||= build_scope
183
+ end
52
184
 
53
- if reflection.collection? || entries.empty?
54
- entries << record
55
- assignments = true
56
- end
57
- end
185
+ def set_inverse(record)
186
+ if owners = owners_by_key[derive_key(record, association_key_name)]
187
+ # Processing only the first owner
188
+ # because the record is modified but not an owner
189
+ association = owners.first.association(reflection.name)
190
+ association.set_inverse_instance(record)
191
+ end
192
+ end
193
+
194
+ def load_records(raw_records = nil)
195
+ # owners can be duplicated when a relation has a collection association join
196
+ # #compare_by_identity makes such owners different hash keys
197
+ @records_by_owner = {}.compare_by_identity
198
+ raw_records ||= loader_query.records_for([self])
199
+ @preloaded_records = raw_records.select do |record|
200
+ assignments = false
58
201
 
59
- assignments
202
+ owners_by_key[derive_key(record, association_key_name)]&.each do |owner|
203
+ entries = (@records_by_owner[owner] ||= [])
204
+
205
+ if reflection.collection? || entries.empty?
206
+ entries << record
207
+ assignments = true
208
+ end
60
209
  end
210
+
211
+ assignments
61
212
  end
213
+ end
214
+
215
+ def associate_records_from_unscoped(unscoped_records)
216
+ return if unscoped_records.nil? || unscoped_records.empty?
217
+ return if !reflection_scope.empty_scope?
218
+ return if preload_scope && !preload_scope.empty_scope?
219
+ return if reflection.collection?
220
+
221
+ unscoped_records.select { |r| r[association_key_name].present? }.each do |record|
222
+ owners = owners_by_key[derive_key(record, association_key_name)]
223
+ owners&.each_with_index do |owner, i|
224
+ association = owner.association(reflection.name)
225
+ association.target = record
62
226
 
63
- # The name of the key on the associated records
64
- def association_key_name
65
- reflection.join_primary_key(klass)
227
+ if i == 0 # Set inverse on first owner
228
+ association.set_inverse_instance(record)
229
+ end
230
+ end
66
231
  end
232
+ end
233
+
234
+ private
235
+ attr_reader :owners, :reflection, :preload_scope, :model
67
236
 
68
237
  # The name of the key on the model which declares the association
69
238
  def owner_key_name
@@ -71,7 +240,10 @@ module ActiveRecord
71
240
  end
72
241
 
73
242
  def associate_records_to_owner(owner, records)
243
+ return if loaded?(owner)
244
+
74
245
  association = owner.association(reflection.name)
246
+
75
247
  if reflection.collection?
76
248
  association.target = records
77
249
  else
@@ -79,17 +251,6 @@ module ActiveRecord
79
251
  end
80
252
  end
81
253
 
82
- def owner_keys
83
- @owner_keys ||= owners_by_key.keys
84
- end
85
-
86
- def owners_by_key
87
- @owners_by_key ||= owners.each_with_object({}) do |owner, result|
88
- key = convert_key(owner[owner_key_name])
89
- (result[key] ||= []) << owner if key
90
- end
91
- end
92
-
93
254
  def key_conversion_required?
94
255
  unless defined?(@key_conversion_required)
95
256
  @key_conversion_required = (association_key_type != owner_key_type)
@@ -98,6 +259,14 @@ module ActiveRecord
98
259
  @key_conversion_required
99
260
  end
100
261
 
262
+ def derive_key(owner, key)
263
+ if key.is_a?(Array)
264
+ key.map { |k| convert_key(owner._read_attribute(k)) }
265
+ else
266
+ convert_key(owner._read_attribute(key))
267
+ end
268
+ end
269
+
101
270
  def convert_key(key)
102
271
  if key_conversion_required?
103
272
  key.to_s
@@ -114,20 +283,6 @@ module ActiveRecord
114
283
  @model.type_for_attribute(owner_key_name).type
115
284
  end
116
285
 
117
- def records_for(ids)
118
- scope.where(association_key_name => ids).load do |record|
119
- # Processing only the first owner
120
- # because the record is modified but not an owner
121
- owner = owners_by_key[convert_key(record[association_key_name])].first
122
- association = owner.association(reflection.name)
123
- association.set_inverse_instance(record)
124
- end
125
- end
126
-
127
- def scope
128
- @scope ||= build_scope
129
- end
130
-
131
286
  def reflection_scope
132
287
  @reflection_scope ||= reflection.join_scopes(klass.arel_table, klass.predicate_builder, klass).inject(klass.unscoped, &:merge!)
133
288
  end
@@ -145,11 +300,11 @@ module ActiveRecord
145
300
  scope.merge!(preload_scope)
146
301
  end
147
302
 
148
- if preload_scope && preload_scope.strict_loading_value
149
- scope.strict_loading
150
- else
151
- scope
152
- end
303
+ cascade_strict_loading(scope)
304
+ end
305
+
306
+ def cascade_strict_loading(scope)
307
+ preload_scope&.strict_loading_value ? scope.strict_loading : scope
153
308
  end
154
309
  end
155
310
  end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Associations
5
+ class Preloader
6
+ class Batch # :nodoc:
7
+ def initialize(preloaders, available_records:)
8
+ @preloaders = preloaders.reject(&:empty?)
9
+ @available_records = available_records.flatten.group_by { |r| r.class.base_class }
10
+ end
11
+
12
+ def call
13
+ branches = @preloaders.flat_map(&:branches)
14
+ until branches.empty?
15
+ loaders = branches.flat_map(&:runnable_loaders)
16
+
17
+ loaders.each { |loader| loader.associate_records_from_unscoped(@available_records[loader.klass.base_class]) }
18
+
19
+ if loaders.any?
20
+ future_tables = branches.flat_map do |branch|
21
+ branch.future_classes - branch.runnable_loaders.map(&:klass)
22
+ end.map(&:table_name).uniq
23
+
24
+ target_loaders = loaders.reject { |l| future_tables.include?(l.table_name) }
25
+ target_loaders = loaders if target_loaders.empty?
26
+
27
+ group_and_load_similar(target_loaders)
28
+ target_loaders.each(&:run)
29
+ end
30
+
31
+ finished, in_progress = branches.partition(&:done?)
32
+
33
+ branches = in_progress + finished.flat_map(&:children)
34
+ end
35
+ end
36
+
37
+ private
38
+ attr_reader :loaders
39
+
40
+ def group_and_load_similar(loaders)
41
+ loaders.grep_v(ThroughAssociation).group_by(&:loader_query).each_pair do |query, similar_loaders|
42
+ query.load_records_in_batch(similar_loaders)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end