activerecord 7.0.8.7 → 7.2.2.1

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 (279) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +631 -1944
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +29 -29
  5. data/examples/performance.rb +2 -2
  6. data/lib/active_record/aggregations.rb +16 -13
  7. data/lib/active_record/association_relation.rb +2 -2
  8. data/lib/active_record/associations/alias_tracker.rb +25 -19
  9. data/lib/active_record/associations/association.rb +35 -12
  10. data/lib/active_record/associations/association_scope.rb +16 -9
  11. data/lib/active_record/associations/belongs_to_association.rb +23 -8
  12. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +3 -2
  13. data/lib/active_record/associations/builder/association.rb +3 -3
  14. data/lib/active_record/associations/builder/belongs_to.rb +22 -8
  15. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +3 -7
  16. data/lib/active_record/associations/builder/has_many.rb +3 -4
  17. data/lib/active_record/associations/builder/has_one.rb +3 -4
  18. data/lib/active_record/associations/builder/singular_association.rb +4 -0
  19. data/lib/active_record/associations/collection_association.rb +26 -14
  20. data/lib/active_record/associations/collection_proxy.rb +29 -11
  21. data/lib/active_record/associations/errors.rb +265 -0
  22. data/lib/active_record/associations/foreign_association.rb +10 -3
  23. data/lib/active_record/associations/has_many_association.rb +21 -14
  24. data/lib/active_record/associations/has_many_through_association.rb +17 -7
  25. data/lib/active_record/associations/has_one_association.rb +10 -3
  26. data/lib/active_record/associations/join_dependency/join_association.rb +30 -27
  27. data/lib/active_record/associations/join_dependency.rb +10 -10
  28. data/lib/active_record/associations/nested_error.rb +47 -0
  29. data/lib/active_record/associations/preloader/association.rb +33 -8
  30. data/lib/active_record/associations/preloader/branch.rb +7 -1
  31. data/lib/active_record/associations/preloader/through_association.rb +1 -3
  32. data/lib/active_record/associations/preloader.rb +13 -10
  33. data/lib/active_record/associations/singular_association.rb +7 -1
  34. data/lib/active_record/associations/through_association.rb +22 -11
  35. data/lib/active_record/associations.rb +354 -485
  36. data/lib/active_record/attribute_assignment.rb +0 -4
  37. data/lib/active_record/attribute_methods/before_type_cast.rb +17 -0
  38. data/lib/active_record/attribute_methods/composite_primary_key.rb +84 -0
  39. data/lib/active_record/attribute_methods/dirty.rb +53 -35
  40. data/lib/active_record/attribute_methods/primary_key.rb +45 -25
  41. data/lib/active_record/attribute_methods/query.rb +28 -16
  42. data/lib/active_record/attribute_methods/read.rb +8 -7
  43. data/lib/active_record/attribute_methods/serialization.rb +131 -32
  44. data/lib/active_record/attribute_methods/time_zone_conversion.rb +11 -6
  45. data/lib/active_record/attribute_methods/write.rb +6 -6
  46. data/lib/active_record/attribute_methods.rb +148 -33
  47. data/lib/active_record/attributes.rb +64 -50
  48. data/lib/active_record/autosave_association.rb +69 -37
  49. data/lib/active_record/base.rb +9 -5
  50. data/lib/active_record/callbacks.rb +11 -25
  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 -42
  54. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +123 -131
  55. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +2 -0
  56. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +4 -1
  57. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +323 -88
  58. data/lib/active_record/connection_adapters/abstract/database_limits.rb +5 -0
  59. data/lib/active_record/connection_adapters/abstract/database_statements.rb +160 -45
  60. data/lib/active_record/connection_adapters/abstract/query_cache.rb +217 -63
  61. data/lib/active_record/connection_adapters/abstract/quoting.rb +72 -63
  62. data/lib/active_record/connection_adapters/abstract/savepoints.rb +4 -3
  63. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +18 -4
  64. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +137 -11
  65. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +307 -129
  66. data/lib/active_record/connection_adapters/abstract/transaction.rb +367 -75
  67. data/lib/active_record/connection_adapters/abstract_adapter.rb +510 -111
  68. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +278 -125
  69. data/lib/active_record/connection_adapters/column.rb +9 -0
  70. data/lib/active_record/connection_adapters/mysql/column.rb +1 -0
  71. data/lib/active_record/connection_adapters/mysql/database_statements.rb +26 -139
  72. data/lib/active_record/connection_adapters/mysql/quoting.rb +53 -54
  73. data/lib/active_record/connection_adapters/mysql/schema_creation.rb +9 -0
  74. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +6 -0
  75. data/lib/active_record/connection_adapters/mysql/schema_dumper.rb +1 -1
  76. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +25 -13
  77. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +152 -0
  78. data/lib/active_record/connection_adapters/mysql2_adapter.rb +101 -68
  79. data/lib/active_record/connection_adapters/pool_config.rb +20 -10
  80. data/lib/active_record/connection_adapters/pool_manager.rb +19 -9
  81. data/lib/active_record/connection_adapters/postgresql/column.rb +14 -3
  82. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +100 -43
  83. data/lib/active_record/connection_adapters/postgresql/oid/cidr.rb +6 -0
  84. data/lib/active_record/connection_adapters/postgresql/oid/interval.rb +1 -1
  85. data/lib/active_record/connection_adapters/postgresql/oid/money.rb +3 -2
  86. data/lib/active_record/connection_adapters/postgresql/oid/range.rb +11 -2
  87. data/lib/active_record/connection_adapters/postgresql/oid/timestamp_with_time_zone.rb +1 -1
  88. data/lib/active_record/connection_adapters/postgresql/oid/uuid.rb +14 -4
  89. data/lib/active_record/connection_adapters/postgresql/quoting.rb +65 -61
  90. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +3 -9
  91. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +76 -6
  92. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +151 -2
  93. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +53 -0
  94. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +370 -63
  95. data/lib/active_record/connection_adapters/postgresql_adapter.rb +367 -201
  96. data/lib/active_record/connection_adapters/schema_cache.rb +302 -79
  97. data/lib/active_record/connection_adapters/sqlite3/column.rb +62 -0
  98. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +60 -43
  99. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +45 -46
  100. data/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +22 -0
  101. data/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb +14 -0
  102. data/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +16 -0
  103. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +50 -8
  104. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +290 -110
  105. data/lib/active_record/connection_adapters/statement_pool.rb +7 -0
  106. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +99 -0
  107. data/lib/active_record/connection_adapters/trilogy_adapter.rb +229 -0
  108. data/lib/active_record/connection_adapters.rb +124 -1
  109. data/lib/active_record/connection_handling.rb +96 -104
  110. data/lib/active_record/core.rb +251 -176
  111. data/lib/active_record/counter_cache.rb +68 -34
  112. data/lib/active_record/database_configurations/connection_url_resolver.rb +8 -3
  113. data/lib/active_record/database_configurations/database_config.rb +26 -5
  114. data/lib/active_record/database_configurations/hash_config.rb +52 -34
  115. data/lib/active_record/database_configurations/url_config.rb +37 -12
  116. data/lib/active_record/database_configurations.rb +87 -34
  117. data/lib/active_record/delegated_type.rb +39 -10
  118. data/lib/active_record/deprecator.rb +7 -0
  119. data/lib/active_record/destroy_association_async_job.rb +3 -1
  120. data/lib/active_record/dynamic_matchers.rb +2 -2
  121. data/lib/active_record/encryption/auto_filtered_parameters.rb +66 -0
  122. data/lib/active_record/encryption/cipher/aes256_gcm.rb +4 -1
  123. data/lib/active_record/encryption/config.rb +25 -1
  124. data/lib/active_record/encryption/configurable.rb +12 -19
  125. data/lib/active_record/encryption/context.rb +10 -3
  126. data/lib/active_record/encryption/contexts.rb +5 -1
  127. data/lib/active_record/encryption/derived_secret_key_provider.rb +8 -2
  128. data/lib/active_record/encryption/encryptable_record.rb +45 -21
  129. data/lib/active_record/encryption/encrypted_attribute_type.rb +47 -12
  130. data/lib/active_record/encryption/encryptor.rb +18 -3
  131. data/lib/active_record/encryption/extended_deterministic_queries.rb +66 -69
  132. data/lib/active_record/encryption/extended_deterministic_uniqueness_validator.rb +3 -3
  133. data/lib/active_record/encryption/key_generator.rb +12 -1
  134. data/lib/active_record/encryption/key_provider.rb +1 -1
  135. data/lib/active_record/encryption/message_pack_message_serializer.rb +76 -0
  136. data/lib/active_record/encryption/message_serializer.rb +6 -0
  137. data/lib/active_record/encryption/null_encryptor.rb +4 -0
  138. data/lib/active_record/encryption/properties.rb +3 -3
  139. data/lib/active_record/encryption/read_only_null_encryptor.rb +4 -0
  140. data/lib/active_record/encryption/scheme.rb +22 -21
  141. data/lib/active_record/encryption.rb +3 -0
  142. data/lib/active_record/enum.rb +129 -28
  143. data/lib/active_record/errors.rb +151 -31
  144. data/lib/active_record/explain.rb +21 -12
  145. data/lib/active_record/fixture_set/model_metadata.rb +14 -4
  146. data/lib/active_record/fixture_set/render_context.rb +2 -0
  147. data/lib/active_record/fixture_set/table_row.rb +29 -8
  148. data/lib/active_record/fixtures.rb +167 -97
  149. data/lib/active_record/future_result.rb +47 -8
  150. data/lib/active_record/gem_version.rb +4 -4
  151. data/lib/active_record/inheritance.rb +34 -18
  152. data/lib/active_record/insert_all.rb +72 -22
  153. data/lib/active_record/integration.rb +11 -8
  154. data/lib/active_record/internal_metadata.rb +124 -20
  155. data/lib/active_record/locking/optimistic.rb +8 -7
  156. data/lib/active_record/locking/pessimistic.rb +5 -2
  157. data/lib/active_record/log_subscriber.rb +18 -22
  158. data/lib/active_record/marshalling.rb +59 -0
  159. data/lib/active_record/message_pack.rb +124 -0
  160. data/lib/active_record/middleware/database_selector/resolver.rb +4 -0
  161. data/lib/active_record/middleware/database_selector.rb +6 -8
  162. data/lib/active_record/middleware/shard_selector.rb +3 -1
  163. data/lib/active_record/migration/command_recorder.rb +106 -8
  164. data/lib/active_record/migration/compatibility.rb +147 -5
  165. data/lib/active_record/migration/default_strategy.rb +22 -0
  166. data/lib/active_record/migration/execution_strategy.rb +19 -0
  167. data/lib/active_record/migration/pending_migration_connection.rb +21 -0
  168. data/lib/active_record/migration.rb +234 -117
  169. data/lib/active_record/model_schema.rb +90 -102
  170. data/lib/active_record/nested_attributes.rb +48 -11
  171. data/lib/active_record/normalization.rb +163 -0
  172. data/lib/active_record/persistence.rb +168 -339
  173. data/lib/active_record/promise.rb +84 -0
  174. data/lib/active_record/query_cache.rb +18 -25
  175. data/lib/active_record/query_logs.rb +92 -52
  176. data/lib/active_record/query_logs_formatter.rb +41 -0
  177. data/lib/active_record/querying.rb +33 -8
  178. data/lib/active_record/railtie.rb +129 -85
  179. data/lib/active_record/railties/controller_runtime.rb +22 -7
  180. data/lib/active_record/railties/databases.rake +145 -154
  181. data/lib/active_record/railties/job_runtime.rb +23 -0
  182. data/lib/active_record/readonly_attributes.rb +32 -5
  183. data/lib/active_record/reflection.rb +267 -69
  184. data/lib/active_record/relation/batches/batch_enumerator.rb +20 -5
  185. data/lib/active_record/relation/batches.rb +198 -63
  186. data/lib/active_record/relation/calculations.rb +250 -93
  187. data/lib/active_record/relation/delegation.rb +30 -19
  188. data/lib/active_record/relation/finder_methods.rb +93 -18
  189. data/lib/active_record/relation/merger.rb +6 -6
  190. data/lib/active_record/relation/predicate_builder/array_handler.rb +2 -2
  191. data/lib/active_record/relation/predicate_builder/association_query_value.rb +18 -3
  192. data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +10 -7
  193. data/lib/active_record/relation/predicate_builder/relation_handler.rb +5 -1
  194. data/lib/active_record/relation/predicate_builder.rb +28 -16
  195. data/lib/active_record/relation/query_attribute.rb +2 -1
  196. data/lib/active_record/relation/query_methods.rb +576 -107
  197. data/lib/active_record/relation/record_fetch_warning.rb +3 -0
  198. data/lib/active_record/relation/spawn_methods.rb +5 -4
  199. data/lib/active_record/relation/where_clause.rb +7 -19
  200. data/lib/active_record/relation.rb +580 -90
  201. data/lib/active_record/result.rb +49 -48
  202. data/lib/active_record/runtime_registry.rb +63 -1
  203. data/lib/active_record/sanitization.rb +70 -25
  204. data/lib/active_record/schema.rb +8 -7
  205. data/lib/active_record/schema_dumper.rb +63 -14
  206. data/lib/active_record/schema_migration.rb +75 -24
  207. data/lib/active_record/scoping/default.rb +15 -5
  208. data/lib/active_record/scoping/named.rb +3 -2
  209. data/lib/active_record/scoping.rb +2 -1
  210. data/lib/active_record/secure_password.rb +60 -0
  211. data/lib/active_record/secure_token.rb +21 -3
  212. data/lib/active_record/signed_id.rb +27 -6
  213. data/lib/active_record/statement_cache.rb +7 -7
  214. data/lib/active_record/store.rb +8 -8
  215. data/lib/active_record/suppressor.rb +3 -1
  216. data/lib/active_record/table_metadata.rb +1 -1
  217. data/lib/active_record/tasks/database_tasks.rb +190 -118
  218. data/lib/active_record/tasks/mysql_database_tasks.rb +15 -6
  219. data/lib/active_record/tasks/postgresql_database_tasks.rb +16 -13
  220. data/lib/active_record/tasks/sqlite_database_tasks.rb +16 -7
  221. data/lib/active_record/test_fixtures.rb +170 -155
  222. data/lib/active_record/testing/query_assertions.rb +121 -0
  223. data/lib/active_record/timestamp.rb +31 -17
  224. data/lib/active_record/token_for.rb +123 -0
  225. data/lib/active_record/touch_later.rb +12 -7
  226. data/lib/active_record/transaction.rb +132 -0
  227. data/lib/active_record/transactions.rb +106 -24
  228. data/lib/active_record/translation.rb +0 -2
  229. data/lib/active_record/type/adapter_specific_registry.rb +1 -8
  230. data/lib/active_record/type/internal/timezone.rb +7 -2
  231. data/lib/active_record/type/serialized.rb +1 -3
  232. data/lib/active_record/type/time.rb +4 -0
  233. data/lib/active_record/type_caster/connection.rb +4 -4
  234. data/lib/active_record/validations/absence.rb +1 -1
  235. data/lib/active_record/validations/associated.rb +9 -3
  236. data/lib/active_record/validations/numericality.rb +5 -4
  237. data/lib/active_record/validations/presence.rb +5 -28
  238. data/lib/active_record/validations/uniqueness.rb +61 -11
  239. data/lib/active_record/validations.rb +12 -5
  240. data/lib/active_record/version.rb +1 -1
  241. data/lib/active_record.rb +247 -33
  242. data/lib/arel/alias_predication.rb +1 -1
  243. data/lib/arel/collectors/bind.rb +2 -0
  244. data/lib/arel/collectors/composite.rb +7 -0
  245. data/lib/arel/collectors/sql_string.rb +1 -1
  246. data/lib/arel/collectors/substitute_binds.rb +1 -1
  247. data/lib/arel/errors.rb +10 -0
  248. data/lib/arel/factory_methods.rb +4 -0
  249. data/lib/arel/nodes/binary.rb +6 -7
  250. data/lib/arel/nodes/bound_sql_literal.rb +65 -0
  251. data/lib/arel/nodes/cte.rb +36 -0
  252. data/lib/arel/nodes/fragments.rb +35 -0
  253. data/lib/arel/nodes/homogeneous_in.rb +1 -9
  254. data/lib/arel/nodes/leading_join.rb +8 -0
  255. data/lib/arel/nodes/{and.rb → nary.rb} +5 -2
  256. data/lib/arel/nodes/node.rb +115 -5
  257. data/lib/arel/nodes/sql_literal.rb +13 -0
  258. data/lib/arel/nodes/table_alias.rb +4 -0
  259. data/lib/arel/nodes.rb +6 -2
  260. data/lib/arel/predications.rb +3 -1
  261. data/lib/arel/select_manager.rb +1 -1
  262. data/lib/arel/table.rb +9 -5
  263. data/lib/arel/tree_manager.rb +8 -3
  264. data/lib/arel/update_manager.rb +2 -1
  265. data/lib/arel/visitors/dot.rb +1 -0
  266. data/lib/arel/visitors/mysql.rb +17 -5
  267. data/lib/arel/visitors/postgresql.rb +1 -12
  268. data/lib/arel/visitors/sqlite.rb +25 -0
  269. data/lib/arel/visitors/to_sql.rb +112 -34
  270. data/lib/arel/visitors/visitor.rb +2 -2
  271. data/lib/arel.rb +21 -3
  272. data/lib/rails/generators/active_record/application_record/USAGE +8 -0
  273. data/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt +4 -1
  274. data/lib/rails/generators/active_record/migration.rb +3 -1
  275. data/lib/rails/generators/active_record/model/USAGE +113 -0
  276. data/lib/rails/generators/active_record/model/model_generator.rb +15 -6
  277. metadata +54 -12
  278. data/lib/active_record/connection_adapters/legacy_pool_manager.rb +0 -35
  279. data/lib/active_record/null_relation.rb +0 -63
@@ -3,6 +3,7 @@
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
@@ -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
39
+ ids = target.collect { |assoc| primary_key_column.map { |col| assoc.public_send(col) } }
40
+ else
41
+ primary_key_column = association_class.primary_key
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
@@ -73,7 +78,7 @@ module ActiveRecord
73
78
  # If the collection is empty the target is set to an empty array and
74
79
  # the loaded flag is set to true as well.
75
80
  def count_records
76
- count = if reflection.has_cached_counter?
81
+ count = if reflection.has_active_cached_counter?
77
82
  owner.read_attribute(reflection.counter_cache_column).to_i
78
83
  else
79
84
  scope.count(:all)
@@ -124,7 +129,9 @@ module ActiveRecord
124
129
  records.each(&:destroy!)
125
130
  update_counter(-records.length) unless reflection.inverse_updates_counter_cache?
126
131
  else
127
- 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)
128
135
  update_counter(-delete_count(method, scope))
129
136
  end
130
137
  end
@@ -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)
@@ -89,7 +93,13 @@ module ActiveRecord
89
93
  @through_scope = scope
90
94
  record = super
91
95
 
92
- inverse = source_reflection.inverse_of
96
+ inverse =
97
+ if source_reflection.polymorphic?
98
+ source_reflection.polymorphic_inverse_of(record.class)
99
+ else
100
+ source_reflection.inverse_of
101
+ end
102
+
93
103
  if inverse
94
104
  if inverse.collection?
95
105
  record.send(inverse.name) << build_through_record(record)
@@ -109,7 +119,7 @@ module ActiveRecord
109
119
  end
110
120
 
111
121
  def target_reflection_has_associated_record?
112
- !(through_reflection.belongs_to? && owner[through_reflection.foreign_key].blank?)
122
+ !(through_reflection.belongs_to? && Array(through_reflection.foreign_key).all? { |foreign_key_column| owner[foreign_key_column].blank? })
113
123
  end
114
124
 
115
125
  def update_through_counter?(method)
@@ -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
38
+ id = primary_key_column.map { |col| target.public_send(col) }
39
+ else
40
+ primary_key_column = target.class.primary_key
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,
@@ -112,7 +117,9 @@ module ActiveRecord
112
117
  end
113
118
 
114
119
  def nullify_owner_attributes(record)
115
- 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
116
123
  end
117
124
 
118
125
  def transaction_if(value, &block)
@@ -25,8 +25,9 @@ module ActiveRecord
25
25
  joins = []
26
26
  chain = []
27
27
 
28
- reflection.chain.each do |reflection|
29
- table, terminated = yield reflection
28
+ reflection_chain = reflection.chain
29
+ reflection_chain.each_with_index do |reflection, index|
30
+ table, terminated = yield reflection, reflection_chain[index..]
30
31
  @table ||= table
31
32
 
32
33
  if terminated
@@ -37,39 +38,41 @@ module ActiveRecord
37
38
  chain << [reflection, table]
38
39
  end
39
40
 
40
- # The chain starts with the target table, but we want to end with it here (makes
41
- # more sense in this context), so we reverse
42
- chain.reverse_each do |reflection, table|
43
- klass = reflection.klass
41
+ base_klass.with_connection do |connection|
42
+ # The chain starts with the target table, but we want to end with it here (makes
43
+ # more sense in this context), so we reverse
44
+ chain.reverse_each do |reflection, table|
45
+ klass = reflection.klass
44
46
 
45
- scope = reflection.join_scope(table, foreign_table, foreign_klass)
47
+ scope = reflection.join_scope(table, foreign_table, foreign_klass)
46
48
 
47
- unless scope.references_values.empty?
48
- associations = scope.eager_load_values | scope.includes_values
49
+ unless scope.references_values.empty?
50
+ associations = scope.eager_load_values | scope.includes_values
49
51
 
50
- unless associations.empty?
51
- scope.joins! scope.construct_join_dependency(associations, Arel::Nodes::OuterJoin)
52
+ unless associations.empty?
53
+ scope.joins! scope.construct_join_dependency(associations, Arel::Nodes::OuterJoin)
54
+ end
52
55
  end
53
- end
54
56
 
55
- arel = scope.arel(alias_tracker.aliases)
56
- nodes = arel.constraints.first
57
+ arel = scope.arel(alias_tracker.aliases)
58
+ nodes = arel.constraints.first
57
59
 
58
- if nodes.is_a?(Arel::Nodes::And)
59
- others = nodes.children.extract! do |node|
60
- !Arel.fetch_attribute(node) { |attr| attr.relation.name == table.name }
60
+ if nodes.is_a?(Arel::Nodes::And)
61
+ others = nodes.children.extract! do |node|
62
+ !Arel.fetch_attribute(node) { |attr| attr.relation.name == table.name }
63
+ end
61
64
  end
62
- end
63
65
 
64
- joins << join_type.new(table, Arel::Nodes::On.new(nodes))
66
+ joins << join_type.new(table, Arel::Nodes::On.new(nodes))
65
67
 
66
- if others && !others.empty?
67
- joins.concat arel.join_sources
68
- append_constraints(joins.last, others)
69
- end
68
+ if others && !others.empty?
69
+ joins.concat arel.join_sources
70
+ append_constraints(connection, joins.last, others)
71
+ end
70
72
 
71
- # The current table in this iteration becomes the foreign table in the next
72
- foreign_table, foreign_klass = table, klass
73
+ # The current table in this iteration becomes the foreign table in the next
74
+ foreign_table, foreign_klass = table, klass
75
+ end
73
76
  end
74
77
 
75
78
  joins
@@ -88,10 +91,10 @@ module ActiveRecord
88
91
  end
89
92
 
90
93
  private
91
- def append_constraints(join, constraints)
94
+ def append_constraints(connection, join, constraints)
92
95
  if join.is_a?(Arel::Nodes::StringJoin)
93
96
  join_string = Arel::Nodes::And.new(constraints.unshift join.left)
94
- join.left = Arel.sql(base_klass.connection.visitor.compile(join_string))
97
+ join.left = Arel.sql(connection.visitor.compile(join_string))
95
98
  else
96
99
  right = join.right
97
100
  right.expr = Arel::Nodes::And.new(constraints.unshift right.expr)
@@ -61,7 +61,7 @@ module ActiveRecord
61
61
  when Hash
62
62
  associations.each do |k, v|
63
63
  cache = hash[k] ||= {}
64
- walk_tree v, cache
64
+ walk_tree v, cache if v
65
65
  end
66
66
  else
67
67
  raise ConfigurationError, associations.inspect
@@ -190,12 +190,12 @@ module ActiveRecord
190
190
  def make_constraints(parent, child, join_type)
191
191
  foreign_table = parent.table
192
192
  foreign_klass = parent.base_klass
193
- child.join_constraints(foreign_table, foreign_klass, join_type, alias_tracker) do |reflection|
194
- table, terminated = @joined_tables[reflection]
193
+ child.join_constraints(foreign_table, foreign_klass, join_type, alias_tracker) do |reflection, remaining_reflection_chain|
194
+ table, terminated = @joined_tables[remaining_reflection_chain]
195
195
  root = reflection == child.reflection
196
196
 
197
197
  if table && (!root || !terminated)
198
- @joined_tables[reflection] = [table, root] if root
198
+ @joined_tables[remaining_reflection_chain] = [table, root] if root
199
199
  next table, true
200
200
  end
201
201
 
@@ -206,7 +206,7 @@ module ActiveRecord
206
206
  root ? name : "#{name}_join"
207
207
  end
208
208
 
209
- @joined_tables[reflection] ||= [table, root] if join_type == Arel::Nodes::OuterJoin
209
+ @joined_tables[remaining_reflection_chain] ||= [table, root] if join_type == Arel::Nodes::OuterJoin
210
210
  table
211
211
  end.concat child.children.flat_map { |c| make_constraints(child, c, join_type) }
212
212
  end
@@ -253,14 +253,14 @@ module ActiveRecord
253
253
  end
254
254
 
255
255
  if node.primary_key
256
- key = aliases.column_alias(node, node.primary_key)
257
- id = row[key]
256
+ keys = Array(node.primary_key).map { |column| aliases.column_alias(node, column) }
257
+ id = keys.map { |key| row[key] }
258
258
  else
259
- key = aliases.column_alias(node, node.reflection.join_primary_key.to_s)
260
- id = nil # Avoid id-based model caching.
259
+ keys = Array(node.reflection.join_primary_key).map { |column| aliases.column_alias(node, column.to_s) }
260
+ id = keys.map { nil } # Avoid id-based model caching.
261
261
  end
262
262
 
263
- if row[key].nil?
263
+ if keys.any? { |key| row[key].nil? }
264
264
  nil_association = ar_parent.association(node.reflection.name)
265
265
  nil_association.loaded!
266
266
  next
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Validation error class to wrap association records' errors,
4
+ # with index_errors support.
5
+ module ActiveRecord
6
+ module Associations
7
+ class NestedError < ::ActiveModel::NestedError
8
+ def initialize(association, inner_error)
9
+ @base = association.owner
10
+ @association = association
11
+ @inner_error = inner_error
12
+ super(@base, inner_error, { attribute: compute_attribute(inner_error) })
13
+ end
14
+
15
+ private
16
+ attr_reader :association
17
+
18
+ def compute_attribute(inner_error)
19
+ association_name = association.reflection.name
20
+
21
+ if association.collection? && index_errors_setting && index
22
+ "#{association_name}[#{index}].#{inner_error.attribute}".to_sym
23
+ else
24
+ "#{association_name}.#{inner_error.attribute}".to_sym
25
+ end
26
+ end
27
+
28
+ def index_errors_setting
29
+ @index_errors_setting ||=
30
+ association.options.fetch(:index_errors, ActiveRecord.index_nested_attribute_errors)
31
+ end
32
+
33
+ def index
34
+ @index ||= ordered_records&.find_index(inner_error.base)
35
+ end
36
+
37
+ def ordered_records
38
+ case index_errors_setting
39
+ when true # default is association order
40
+ association.target
41
+ when :nested_attributes_order
42
+ association.nested_attributes_target
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # :enddoc:
4
+
3
5
  module ActiveRecord
4
6
  module Associations
5
7
  class Preloader
@@ -15,11 +17,12 @@ module ActiveRecord
15
17
  def eql?(other)
16
18
  association_key_name == other.association_key_name &&
17
19
  scope.table_name == other.scope.table_name &&
20
+ scope.connection_specification_name == other.scope.connection_specification_name &&
18
21
  scope.values_for_queries == other.scope.values_for_queries
19
22
  end
20
23
 
21
24
  def hash
22
- [association_key_name, scope.table_name, scope.values_for_queries].hash
25
+ [association_key_name, scope.table_name, scope.connection_specification_name, scope.values_for_queries].hash
23
26
  end
24
27
 
25
28
  def records_for(loaders)
@@ -36,7 +39,21 @@ module ActiveRecord
36
39
  end
37
40
 
38
41
  def load_records_for_keys(keys, &block)
39
- scope.where(association_key_name => keys).load(&block)
42
+ return [] if keys.empty?
43
+
44
+ if association_key_name.is_a?(Array)
45
+ query_constraints = Hash.new { |hsh, key| hsh[key] = Set.new }
46
+
47
+ keys.each_with_object(query_constraints) do |values_set, constraints|
48
+ association_key_name.zip(values_set).each do |key_name, value|
49
+ constraints[key_name] << value
50
+ end
51
+ end
52
+
53
+ scope.where(query_constraints)
54
+ else
55
+ scope.where(association_key_name => keys)
56
+ end.load(&block)
40
57
  end
41
58
  end
42
59
 
@@ -151,7 +168,7 @@ module ActiveRecord
151
168
 
152
169
  def owners_by_key
153
170
  @owners_by_key ||= owners.each_with_object({}) do |owner, result|
154
- key = convert_key(owner[owner_key_name])
171
+ key = derive_key(owner, owner_key_name)
155
172
  (result[key] ||= []) << owner if key
156
173
  end
157
174
  end
@@ -169,7 +186,7 @@ module ActiveRecord
169
186
  end
170
187
 
171
188
  def set_inverse(record)
172
- if owners = owners_by_key[convert_key(record[association_key_name])]
189
+ if owners = owners_by_key[derive_key(record, association_key_name)]
173
190
  # Processing only the first owner
174
191
  # because the record is modified but not an owner
175
192
  association = owners.first.association(reflection.name)
@@ -182,11 +199,10 @@ module ActiveRecord
182
199
  # #compare_by_identity makes such owners different hash keys
183
200
  @records_by_owner = {}.compare_by_identity
184
201
  raw_records ||= loader_query.records_for([self])
185
-
186
202
  @preloaded_records = raw_records.select do |record|
187
203
  assignments = false
188
204
 
189
- owners_by_key[convert_key(record[association_key_name])]&.each do |owner|
205
+ owners_by_key[derive_key(record, association_key_name)]&.each do |owner|
190
206
  entries = (@records_by_owner[owner] ||= [])
191
207
 
192
208
  if reflection.collection? || entries.empty?
@@ -206,7 +222,7 @@ module ActiveRecord
206
222
  return if reflection.collection?
207
223
 
208
224
  unscoped_records.select { |r| r[association_key_name].present? }.each do |record|
209
- owners = owners_by_key[convert_key(record[association_key_name])]
225
+ owners = owners_by_key[derive_key(record, association_key_name)]
210
226
  owners&.each_with_index do |owner, i|
211
227
  association = owner.association(reflection.name)
212
228
  association.target = record
@@ -232,7 +248,8 @@ module ActiveRecord
232
248
  association = owner.association(reflection.name)
233
249
 
234
250
  if reflection.collection?
235
- association.target = records
251
+ not_persisted_records = association.target.reject(&:persisted?)
252
+ association.target = records + not_persisted_records
236
253
  else
237
254
  association.target = records.first
238
255
  end
@@ -246,6 +263,14 @@ module ActiveRecord
246
263
  @key_conversion_required
247
264
  end
248
265
 
266
+ def derive_key(owner, key)
267
+ if key.is_a?(Array)
268
+ key.map { |k| convert_key(owner._read_attribute(k)) }
269
+ else
270
+ convert_key(owner._read_attribute(key))
271
+ end
272
+ end
273
+
249
274
  def convert_key(key)
250
275
  if key_conversion_required?
251
276
  key.to_s
@@ -9,7 +9,13 @@ module ActiveRecord
9
9
  attr_writer :preloaded_records
10
10
 
11
11
  def initialize(association:, children:, parent:, associate_by_default:, scope:)
12
- @association = association
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
13
19
  @parent = parent
14
20
  @scope = scope
15
21
  @associate_by_default = associate_by_default
@@ -9,9 +9,7 @@ module ActiveRecord
9
9
  end
10
10
 
11
11
  def records_by_owner
12
- return @records_by_owner if defined?(@records_by_owner)
13
-
14
- @records_by_owner = owners.each_with_object({}) do |owner, result|
12
+ @records_by_owner ||= owners.each_with_object({}) do |owner, result|
15
13
  if loaded?(owner)
16
14
  result[owner] = target_for(owner)
17
15
  next
@@ -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,11 +35,11 @@ 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.
@@ -73,15 +75,16 @@ module ActiveRecord
73
75
  # for an Author.
74
76
  # - an Array which specifies multiple association names. This array
75
77
  # is processed recursively. For example, specifying <tt>[:avatar, :books]</tt>
76
- # 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
77
79
  # books.
78
80
  # - a Hash which specifies multiple association names, as well as
79
81
  # association names for the to-be-preloaded association objects. For
80
82
  # example, specifying <tt>{ author: :avatar }</tt> will preload a
81
83
  # book's author, as well as that author's avatar.
82
84
  #
83
- # +:associations+ has the same format as the +:include+ method in
84
- # <tt>ActiveRecord::QueryMethods</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:
85
88
  #
86
89
  # :books
87
90
  # [ :books, :author ]
@@ -14,6 +14,12 @@ module ActiveRecord
14
14
  target
15
15
  end
16
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
+
17
23
  # Implements the writer method, e.g. foo.bar= for Foo.belongs_to :bar
18
24
  def writer(record)
19
25
  replace(record)
@@ -34,7 +40,7 @@ module ActiveRecord
34
40
 
35
41
  private
36
42
  def scope_for_create
37
- super.except!(klass.primary_key)
43
+ super.except!(*Array(klass.primary_key))
38
44
  end
39
45
 
40
46
  def find_target
@@ -7,6 +7,10 @@ module ActiveRecord
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]
@@ -78,12 +81,16 @@ module ActiveRecord
78
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