activerecord 7.1.6 → 7.2.3

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 (193) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +839 -2248
  3. data/README.rdoc +16 -16
  4. data/examples/performance.rb +2 -2
  5. data/lib/active_record/association_relation.rb +1 -1
  6. data/lib/active_record/associations/alias_tracker.rb +31 -23
  7. data/lib/active_record/associations/association.rb +15 -8
  8. data/lib/active_record/associations/belongs_to_association.rb +31 -8
  9. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +3 -2
  10. data/lib/active_record/associations/builder/belongs_to.rb +1 -0
  11. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +2 -2
  12. data/lib/active_record/associations/builder/has_many.rb +3 -4
  13. data/lib/active_record/associations/builder/has_one.rb +3 -4
  14. data/lib/active_record/associations/collection_association.rb +16 -8
  15. data/lib/active_record/associations/collection_proxy.rb +14 -1
  16. data/lib/active_record/associations/errors.rb +265 -0
  17. data/lib/active_record/associations/has_many_association.rb +1 -1
  18. data/lib/active_record/associations/has_many_through_association.rb +7 -1
  19. data/lib/active_record/associations/join_dependency/join_association.rb +1 -1
  20. data/lib/active_record/associations/nested_error.rb +47 -0
  21. data/lib/active_record/associations/preloader/association.rb +2 -1
  22. data/lib/active_record/associations/preloader/branch.rb +7 -1
  23. data/lib/active_record/associations/preloader/through_association.rb +1 -3
  24. data/lib/active_record/associations/singular_association.rb +6 -0
  25. data/lib/active_record/associations/through_association.rb +1 -1
  26. data/lib/active_record/associations.rb +59 -292
  27. data/lib/active_record/attribute_assignment.rb +0 -2
  28. data/lib/active_record/attribute_methods/composite_primary_key.rb +84 -0
  29. data/lib/active_record/attribute_methods/primary_key.rb +23 -55
  30. data/lib/active_record/attribute_methods/read.rb +1 -13
  31. data/lib/active_record/attribute_methods/serialization.rb +5 -25
  32. data/lib/active_record/attribute_methods/time_zone_conversion.rb +7 -6
  33. data/lib/active_record/attribute_methods.rb +51 -60
  34. data/lib/active_record/attributes.rb +93 -68
  35. data/lib/active_record/autosave_association.rb +25 -32
  36. data/lib/active_record/base.rb +4 -5
  37. data/lib/active_record/callbacks.rb +1 -1
  38. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +24 -107
  39. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +1 -0
  40. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +294 -72
  41. data/lib/active_record/connection_adapters/abstract/database_statements.rb +34 -17
  42. data/lib/active_record/connection_adapters/abstract/query_cache.rb +201 -75
  43. data/lib/active_record/connection_adapters/abstract/quoting.rb +65 -91
  44. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +6 -2
  45. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +18 -6
  46. data/lib/active_record/connection_adapters/abstract/transaction.rb +125 -62
  47. data/lib/active_record/connection_adapters/abstract_adapter.rb +46 -44
  48. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +53 -15
  49. data/lib/active_record/connection_adapters/mysql/database_statements.rb +9 -1
  50. data/lib/active_record/connection_adapters/mysql/quoting.rb +43 -48
  51. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +6 -0
  52. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +19 -18
  53. data/lib/active_record/connection_adapters/mysql2_adapter.rb +12 -23
  54. data/lib/active_record/connection_adapters/pool_config.rb +7 -6
  55. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +27 -4
  56. data/lib/active_record/connection_adapters/postgresql/oid/interval.rb +1 -1
  57. data/lib/active_record/connection_adapters/postgresql/oid/uuid.rb +14 -4
  58. data/lib/active_record/connection_adapters/postgresql/quoting.rb +58 -58
  59. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +30 -8
  60. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +1 -1
  61. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +16 -12
  62. data/lib/active_record/connection_adapters/postgresql_adapter.rb +36 -26
  63. data/lib/active_record/connection_adapters/schema_cache.rb +123 -128
  64. data/lib/active_record/connection_adapters/sqlite3/column.rb +14 -1
  65. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +10 -6
  66. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +57 -46
  67. data/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +22 -0
  68. data/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb +13 -0
  69. data/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +16 -0
  70. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +26 -2
  71. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +133 -78
  72. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +15 -15
  73. data/lib/active_record/connection_adapters/trilogy_adapter.rb +19 -48
  74. data/lib/active_record/connection_adapters.rb +121 -0
  75. data/lib/active_record/connection_handling.rb +68 -49
  76. data/lib/active_record/core.rb +112 -44
  77. data/lib/active_record/counter_cache.rb +19 -10
  78. data/lib/active_record/database_configurations/connection_url_resolver.rb +9 -2
  79. data/lib/active_record/database_configurations/database_config.rb +19 -4
  80. data/lib/active_record/database_configurations/hash_config.rb +38 -34
  81. data/lib/active_record/database_configurations/url_config.rb +20 -1
  82. data/lib/active_record/database_configurations.rb +1 -1
  83. data/lib/active_record/delegated_type.rb +42 -18
  84. data/lib/active_record/dynamic_matchers.rb +2 -2
  85. data/lib/active_record/encryption/encryptable_record.rb +4 -4
  86. data/lib/active_record/encryption/encrypted_attribute_type.rb +25 -5
  87. data/lib/active_record/encryption/encryptor.rb +35 -19
  88. data/lib/active_record/encryption/key_provider.rb +1 -1
  89. data/lib/active_record/encryption/message_pack_message_serializer.rb +76 -0
  90. data/lib/active_record/encryption/message_serializer.rb +4 -0
  91. data/lib/active_record/encryption/null_encryptor.rb +4 -0
  92. data/lib/active_record/encryption/read_only_null_encryptor.rb +4 -0
  93. data/lib/active_record/enum.rb +31 -13
  94. data/lib/active_record/errors.rb +49 -23
  95. data/lib/active_record/explain.rb +13 -24
  96. data/lib/active_record/fixture_set/table_row.rb +19 -2
  97. data/lib/active_record/fixtures.rb +37 -31
  98. data/lib/active_record/future_result.rb +8 -4
  99. data/lib/active_record/gem_version.rb +2 -2
  100. data/lib/active_record/inheritance.rb +4 -2
  101. data/lib/active_record/insert_all.rb +18 -15
  102. data/lib/active_record/integration.rb +4 -1
  103. data/lib/active_record/internal_metadata.rb +48 -34
  104. data/lib/active_record/locking/optimistic.rb +7 -6
  105. data/lib/active_record/log_subscriber.rb +0 -21
  106. data/lib/active_record/message_pack.rb +1 -1
  107. data/lib/active_record/migration/command_recorder.rb +2 -3
  108. data/lib/active_record/migration/compatibility.rb +5 -3
  109. data/lib/active_record/migration/default_strategy.rb +4 -5
  110. data/lib/active_record/migration/pending_migration_connection.rb +2 -2
  111. data/lib/active_record/migration.rb +87 -77
  112. data/lib/active_record/model_schema.rb +31 -68
  113. data/lib/active_record/nested_attributes.rb +11 -3
  114. data/lib/active_record/normalization.rb +3 -7
  115. data/lib/active_record/persistence.rb +30 -352
  116. data/lib/active_record/query_cache.rb +19 -8
  117. data/lib/active_record/query_logs.rb +19 -0
  118. data/lib/active_record/querying.rb +25 -13
  119. data/lib/active_record/railtie.rb +39 -57
  120. data/lib/active_record/railties/controller_runtime.rb +13 -4
  121. data/lib/active_record/railties/databases.rake +42 -44
  122. data/lib/active_record/reflection.rb +98 -36
  123. data/lib/active_record/relation/batches/batch_enumerator.rb +15 -2
  124. data/lib/active_record/relation/batches.rb +14 -8
  125. data/lib/active_record/relation/calculations.rb +127 -89
  126. data/lib/active_record/relation/delegation.rb +8 -11
  127. data/lib/active_record/relation/finder_methods.rb +26 -12
  128. data/lib/active_record/relation/merger.rb +4 -6
  129. data/lib/active_record/relation/predicate_builder/array_handler.rb +2 -2
  130. data/lib/active_record/relation/predicate_builder/association_query_value.rb +10 -2
  131. data/lib/active_record/relation/predicate_builder.rb +3 -3
  132. data/lib/active_record/relation/query_attribute.rb +1 -1
  133. data/lib/active_record/relation/query_methods.rb +238 -65
  134. data/lib/active_record/relation/record_fetch_warning.rb +3 -0
  135. data/lib/active_record/relation/spawn_methods.rb +2 -18
  136. data/lib/active_record/relation/where_clause.rb +15 -21
  137. data/lib/active_record/relation.rb +508 -74
  138. data/lib/active_record/result.rb +31 -44
  139. data/lib/active_record/runtime_registry.rb +39 -0
  140. data/lib/active_record/sanitization.rb +24 -19
  141. data/lib/active_record/schema.rb +8 -6
  142. data/lib/active_record/schema_dumper.rb +48 -20
  143. data/lib/active_record/schema_migration.rb +30 -14
  144. data/lib/active_record/scoping/named.rb +1 -0
  145. data/lib/active_record/secure_token.rb +3 -3
  146. data/lib/active_record/signed_id.rb +27 -7
  147. data/lib/active_record/statement_cache.rb +7 -7
  148. data/lib/active_record/table_metadata.rb +1 -10
  149. data/lib/active_record/tasks/database_tasks.rb +69 -41
  150. data/lib/active_record/tasks/mysql_database_tasks.rb +1 -1
  151. data/lib/active_record/tasks/postgresql_database_tasks.rb +8 -1
  152. data/lib/active_record/tasks/sqlite_database_tasks.rb +2 -1
  153. data/lib/active_record/test_fixtures.rb +86 -89
  154. data/lib/active_record/testing/query_assertions.rb +121 -0
  155. data/lib/active_record/timestamp.rb +2 -2
  156. data/lib/active_record/token_for.rb +22 -12
  157. data/lib/active_record/touch_later.rb +1 -1
  158. data/lib/active_record/transaction.rb +132 -0
  159. data/lib/active_record/transactions.rb +73 -15
  160. data/lib/active_record/translation.rb +0 -2
  161. data/lib/active_record/type/serialized.rb +1 -3
  162. data/lib/active_record/type_caster/connection.rb +4 -4
  163. data/lib/active_record/validations/associated.rb +9 -3
  164. data/lib/active_record/validations/uniqueness.rb +15 -10
  165. data/lib/active_record/validations.rb +4 -1
  166. data/lib/active_record.rb +148 -39
  167. data/lib/arel/alias_predication.rb +1 -1
  168. data/lib/arel/collectors/bind.rb +3 -1
  169. data/lib/arel/collectors/composite.rb +7 -0
  170. data/lib/arel/collectors/sql_string.rb +1 -1
  171. data/lib/arel/collectors/substitute_binds.rb +1 -1
  172. data/lib/arel/crud.rb +2 -0
  173. data/lib/arel/delete_manager.rb +5 -0
  174. data/lib/arel/nodes/binary.rb +0 -6
  175. data/lib/arel/nodes/bound_sql_literal.rb +9 -5
  176. data/lib/arel/nodes/delete_statement.rb +4 -2
  177. data/lib/arel/nodes/{and.rb → nary.rb} +5 -2
  178. data/lib/arel/nodes/node.rb +4 -3
  179. data/lib/arel/nodes/sql_literal.rb +7 -0
  180. data/lib/arel/nodes/update_statement.rb +4 -2
  181. data/lib/arel/nodes.rb +2 -2
  182. data/lib/arel/predications.rb +1 -1
  183. data/lib/arel/select_manager.rb +7 -3
  184. data/lib/arel/tree_manager.rb +3 -2
  185. data/lib/arel/update_manager.rb +7 -1
  186. data/lib/arel/visitors/dot.rb +3 -0
  187. data/lib/arel/visitors/mysql.rb +9 -4
  188. data/lib/arel/visitors/postgresql.rb +1 -12
  189. data/lib/arel/visitors/sqlite.rb +25 -0
  190. data/lib/arel/visitors/to_sql.rb +31 -16
  191. data/lib/arel.rb +7 -3
  192. data/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt +4 -1
  193. metadata +16 -10
@@ -72,10 +72,26 @@ module ActiveRecord
72
72
  # # INNER JOIN "authors" ON "authors"."id" = "posts"."author_id"
73
73
  # # INNER JOIN "comments" ON "comments"."post_id" = "posts"."id"
74
74
  # # WHERE "authors"."id" IS NOT NULL AND "comments"."id" IS NOT NULL
75
+ #
76
+ # You can define join type in the scope and +associated+ will not use `JOIN` by default.
77
+ #
78
+ # Post.left_joins(:author).where.associated(:author)
79
+ # # SELECT "posts".* FROM "posts"
80
+ # # LEFT OUTER JOIN "authors" "authors"."id" = "posts"."author_id"
81
+ # # WHERE "authors"."id" IS NOT NULL
82
+ #
83
+ # Post.left_joins(:comments).where.associated(:author)
84
+ # # SELECT "posts".* FROM "posts"
85
+ # # INNER JOIN "authors" ON "authors"."id" = "posts"."author_id"
86
+ # # LEFT OUTER JOIN "comments" ON "comments"."post_id" = "posts"."id"
87
+ # # WHERE "author"."id" IS NOT NULL
75
88
  def associated(*associations)
76
89
  associations.each do |association|
77
90
  reflection = scope_association_reflection(association)
78
- @scope.joins!(association)
91
+ unless @scope.joins_values.include?(reflection.name) || @scope.left_outer_joins_values.include?(reflection.name)
92
+ @scope.joins!(association)
93
+ end
94
+
79
95
  if reflection.options[:class_name]
80
96
  self.not(association => { reflection.association_primary_key => nil })
81
97
  else
@@ -157,7 +173,7 @@ module ActiveRecord
157
173
  end # end
158
174
 
159
175
  def #{method_name}=(value) # def includes_values=(value)
160
- assert_mutability! # assert_mutability!
176
+ assert_modifiable! # assert_modifiable!
161
177
  @values[:#{name}] = value # @values[:includes] = value
162
178
  end # end
163
179
  CODE
@@ -419,6 +435,17 @@ module ActiveRecord
419
435
  # # )
420
436
  # # SELECT * FROM posts
421
437
  #
438
+ # You can also pass an array of sub-queries to be joined in a +UNION ALL+.
439
+ #
440
+ # Post.with(posts_with_tags_or_comments: [Post.where("tags_count > ?", 0), Post.where("comments_count > ?", 0)])
441
+ # # => ActiveRecord::Relation
442
+ # # WITH posts_with_tags_or_comments AS (
443
+ # # (SELECT * FROM posts WHERE (tags_count > 0))
444
+ # # UNION ALL
445
+ # # (SELECT * FROM posts WHERE (comments_count > 0))
446
+ # # )
447
+ # # SELECT * FROM posts
448
+ #
422
449
  # Once you define Common Table Expression you can use custom +FROM+ value or +JOIN+ to reference it.
423
450
  #
424
451
  # Post.with(posts_with_tags: Post.where("tags_count > ?", 0)).from("posts_with_tags AS posts")
@@ -457,13 +484,40 @@ module ActiveRecord
457
484
  # .with(posts_with_comments: Post.where("comments_count > ?", 0))
458
485
  # .with(posts_with_tags: Post.where("tags_count > ?", 0))
459
486
  def with(*args)
487
+ raise ArgumentError, "ActiveRecord::Relation#with does not accept a block" if block_given?
460
488
  check_if_method_has_arguments!(__callee__, args)
461
489
  spawn.with!(*args)
462
490
  end
463
491
 
464
492
  # Like #with, but modifies relation in place.
465
493
  def with!(*args) # :nodoc:
466
- self.with_values += args
494
+ args = process_with_args(args)
495
+ self.with_values |= args
496
+ self
497
+ end
498
+
499
+ # Add a recursive Common Table Expression (CTE) that you can then reference within another SELECT statement.
500
+ #
501
+ # Post.with_recursive(post_and_replies: [Post.where(id: 42), Post.joins('JOIN post_and_replies ON posts.in_reply_to_id = post_and_replies.id')])
502
+ # # => ActiveRecord::Relation
503
+ # # WITH post_and_replies AS (
504
+ # # (SELECT * FROM posts WHERE id = 42)
505
+ # # UNION ALL
506
+ # # (SELECT * FROM posts JOIN post_and_replies ON posts.in_reply_to_id = post_and_replies.id)
507
+ # # )
508
+ # # SELECT * FROM posts
509
+ #
510
+ # See `#with` for more information.
511
+ def with_recursive(*args)
512
+ check_if_method_has_arguments!(__callee__, args)
513
+ spawn.with_recursive!(*args)
514
+ end
515
+
516
+ # Like #with_recursive but modifies the relation in place.
517
+ def with_recursive!(*args) # :nodoc:
518
+ args = process_with_args(args)
519
+ self.with_values |= args
520
+ @with_is_recursive = true
467
521
  self
468
522
  end
469
523
 
@@ -606,7 +660,8 @@ module ActiveRecord
606
660
  self
607
661
  end
608
662
 
609
- # Allows to specify an order by a specific set of values.
663
+ # Applies an <tt>ORDER BY</tt> clause based on a given +column+,
664
+ # ordered and filtered by a specific set of +values+.
610
665
  #
611
666
  # User.in_order_of(:id, [1, 5, 3])
612
667
  # # SELECT "users".* FROM "users"
@@ -617,8 +672,34 @@ module ActiveRecord
617
672
  # # WHEN "users"."id" = 3 THEN 3
618
673
  # # END ASC
619
674
  #
675
+ # +column+ can point to an enum column; the actual query generated may be different depending
676
+ # on the database adapter and the column definition.
677
+ #
678
+ # class Conversation < ActiveRecord::Base
679
+ # enum :status, [ :active, :archived ]
680
+ # end
681
+ #
682
+ # Conversation.in_order_of(:status, [:archived, :active])
683
+ # # SELECT "conversations".* FROM "conversations"
684
+ # # WHERE "conversations"."status" IN (1, 0)
685
+ # # ORDER BY CASE
686
+ # # WHEN "conversations"."status" = 1 THEN 1
687
+ # # WHEN "conversations"."status" = 0 THEN 2
688
+ # # END ASC
689
+ #
690
+ # +values+ can also include +nil+.
691
+ #
692
+ # Conversation.in_order_of(:status, [nil, :archived, :active])
693
+ # # SELECT "conversations".* FROM "conversations"
694
+ # # WHERE ("conversations"."status" IN (1, 0) OR "conversations"."status" IS NULL)
695
+ # # ORDER BY CASE
696
+ # # WHEN "conversations"."status" IS NULL THEN 1
697
+ # # WHEN "conversations"."status" = 1 THEN 2
698
+ # # WHEN "conversations"."status" = 0 THEN 3
699
+ # # END ASC
700
+ #
620
701
  def in_order_of(column, values)
621
- klass.disallow_raw_sql!([column], permit: connection.column_name_with_order_matcher)
702
+ klass.disallow_raw_sql!([column], permit: model.adapter_class.column_name_with_order_matcher)
622
703
  return spawn.none! if values.empty?
623
704
 
624
705
  references = column_references([column])
@@ -667,7 +748,7 @@ module ActiveRecord
667
748
  VALID_UNSCOPING_VALUES = Set.new([:where, :select, :group, :order, :lock,
668
749
  :limit, :offset, :joins, :left_outer_joins, :annotate,
669
750
  :includes, :eager_load, :preload, :from, :readonly,
670
- :having, :optimizer_hints])
751
+ :having, :optimizer_hints, :with])
671
752
 
672
753
  # Removes an unwanted relation that is already defined on a chain of relations.
673
754
  # This is useful when passing around chains of relations and would like to
@@ -717,7 +798,7 @@ module ActiveRecord
717
798
  if !VALID_UNSCOPING_VALUES.include?(scope)
718
799
  raise ArgumentError, "Called unscope() with invalid unscoping argument ':#{scope}'. Valid arguments are :#{VALID_UNSCOPING_VALUES.to_a.join(", :")}."
719
800
  end
720
- assert_mutability!
801
+ assert_modifiable!
721
802
  @values.delete(scope)
722
803
  when Hash
723
804
  scope.each do |key, target_value|
@@ -1082,7 +1163,7 @@ module ActiveRecord
1082
1163
  raise ArgumentError, "Relation passed to #or must be structurally compatible. Incompatible values: #{incompatible_values}"
1083
1164
  end
1084
1165
 
1085
- self.where_clause = self.where_clause.or(other.where_clause)
1166
+ self.where_clause = where_clause.or(other.where_clause)
1086
1167
  self.having_clause = having_clause.or(other.having_clause)
1087
1168
  self.references_values |= other.references_values
1088
1169
 
@@ -1198,13 +1279,13 @@ module ActiveRecord
1198
1279
  #
1199
1280
  # users = User.readonly
1200
1281
  # users.first.save
1201
- # => ActiveRecord::ReadOnlyRecord: User is marked as readonly
1282
+ # # => ActiveRecord::ReadOnlyRecord: User is marked as readonly
1202
1283
  #
1203
1284
  # To make a readonly relation writable, pass +false+.
1204
1285
  #
1205
1286
  # users.readonly(false)
1206
1287
  # users.first.save
1207
- # => true
1288
+ # # => true
1208
1289
  def readonly(value = true)
1209
1290
  spawn.readonly!(value)
1210
1291
  end
@@ -1219,7 +1300,7 @@ module ActiveRecord
1219
1300
  #
1220
1301
  # user = User.strict_loading.first
1221
1302
  # user.comments.to_a
1222
- # => ActiveRecord::StrictLoadingViolationError
1303
+ # # => ActiveRecord::StrictLoadingViolationError
1223
1304
  def strict_loading(value = true)
1224
1305
  spawn.strict_loading!(value)
1225
1306
  end
@@ -1453,6 +1534,9 @@ module ActiveRecord
1453
1534
  # Post.excluding(post_one, post_two)
1454
1535
  # # SELECT "posts".* FROM "posts" WHERE "posts"."id" NOT IN (1, 2)
1455
1536
  #
1537
+ # Post.excluding(Post.drafts)
1538
+ # # SELECT "posts".* FROM "posts" WHERE "posts"."id" NOT IN (3, 4, 5)
1539
+ #
1456
1540
  # This can also be called on associations. As with the above example, either
1457
1541
  # a single record of collection thereof may be specified:
1458
1542
  #
@@ -1468,14 +1552,15 @@ module ActiveRecord
1468
1552
  # is passed in) are not instances of the same model that the relation is
1469
1553
  # scoping.
1470
1554
  def excluding(*records)
1555
+ relations = records.extract! { |element| element.is_a?(Relation) }
1471
1556
  records.flatten!(1)
1472
1557
  records.compact!
1473
1558
 
1474
- unless records.all?(klass)
1559
+ unless records.all?(klass) && relations.all? { |relation| relation.klass == klass }
1475
1560
  raise ArgumentError, "You must only pass a single or collection of #{klass.name} objects to ##{__callee__}."
1476
1561
  end
1477
1562
 
1478
- spawn.excluding!(records)
1563
+ spawn.excluding!(records + relations.flat_map(&:ids))
1479
1564
  end
1480
1565
  alias :without :excluding
1481
1566
 
@@ -1487,7 +1572,7 @@ module ActiveRecord
1487
1572
 
1488
1573
  # Returns the Arel object associated with the relation.
1489
1574
  def arel(aliases = nil) # :nodoc:
1490
- @arel ||= build_arel(aliases)
1575
+ @arel ||= with_connection { |c| build_arel(c, aliases) }
1491
1576
  end
1492
1577
 
1493
1578
  def construct_join_dependency(associations, join_type) # :nodoc:
@@ -1508,9 +1593,21 @@ module ActiveRecord
1508
1593
  def build_where_clause(opts, rest = []) # :nodoc:
1509
1594
  opts = sanitize_forbidden_attributes(opts)
1510
1595
 
1596
+ if opts.is_a?(Array)
1597
+ opts, *rest = opts
1598
+ end
1599
+
1511
1600
  case opts
1512
- when String, Array
1513
- parts = [klass.sanitize_sql(rest.empty? ? opts : [opts, *rest])]
1601
+ when String
1602
+ if rest.empty?
1603
+ parts = [Arel.sql(opts)]
1604
+ elsif rest.first.is_a?(Hash) && /:\w+/.match?(opts)
1605
+ parts = [build_named_bound_sql_literal(opts, rest.first)]
1606
+ elsif opts.include?("?")
1607
+ parts = [build_bound_sql_literal(opts, rest)]
1608
+ else
1609
+ parts = [klass.sanitize_sql(rest.empty? ? opts : [opts, *rest])]
1610
+ end
1514
1611
  when Hash
1515
1612
  opts = opts.transform_keys do |key|
1516
1613
  if key.is_a?(Array)
@@ -1541,11 +1638,71 @@ module ActiveRecord
1541
1638
  self
1542
1639
  end
1543
1640
 
1641
+ protected
1642
+ def arel_columns(columns)
1643
+ columns.flat_map do |field|
1644
+ case field
1645
+ when Symbol
1646
+ arel_column(field.to_s) do |attr_name|
1647
+ adapter_class.quote_table_name(attr_name)
1648
+ end
1649
+ when String
1650
+ arel_column(field, &:itself)
1651
+ when Proc
1652
+ field.call
1653
+ when Hash
1654
+ arel_columns_from_hash(field)
1655
+ else
1656
+ field
1657
+ end
1658
+ end
1659
+ end
1660
+
1544
1661
  private
1545
1662
  def async
1546
1663
  spawn.async!
1547
1664
  end
1548
1665
 
1666
+ def build_named_bound_sql_literal(statement, values)
1667
+ bound_values = values.transform_values do |value|
1668
+ if ActiveRecord::Relation === value
1669
+ Arel.sql(value.to_sql)
1670
+ elsif value.respond_to?(:map) && !value.acts_like?(:string)
1671
+ values = value.map { |v| v.respond_to?(:id_for_database) ? v.id_for_database : v }
1672
+ values.empty? ? nil : values
1673
+ else
1674
+ value = value.id_for_database if value.respond_to?(:id_for_database)
1675
+ value
1676
+ end
1677
+ end
1678
+
1679
+ begin
1680
+ Arel::Nodes::BoundSqlLiteral.new("(#{statement})", nil, bound_values)
1681
+ rescue Arel::BindError => error
1682
+ raise ActiveRecord::PreparedStatementInvalid, error.message
1683
+ end
1684
+ end
1685
+
1686
+ def build_bound_sql_literal(statement, values)
1687
+ bound_values = values.map do |value|
1688
+ if ActiveRecord::Relation === value
1689
+ Arel.sql(value.to_sql)
1690
+ elsif value.respond_to?(:map) && !value.acts_like?(:string)
1691
+ values = value.map { |v| v.respond_to?(:id_for_database) ? v.id_for_database : v }
1692
+ values.empty? ? nil : values
1693
+ else
1694
+ value = value.id_for_database if value.respond_to?(:id_for_database)
1695
+ value
1696
+ end
1697
+ end
1698
+
1699
+ begin
1700
+ Arel::Nodes::BoundSqlLiteral.new("(#{statement})", bound_values, nil)
1701
+ rescue Arel::BindError => error
1702
+ raise ActiveRecord::PreparedStatementInvalid, error.message
1703
+ end
1704
+ end
1705
+
1549
1706
  def lookup_table_klass_from_join_dependencies(table_name)
1550
1707
  each_join_dependencies do |join|
1551
1708
  return join.base_klass if table_name == join.table_name
@@ -1570,12 +1727,11 @@ module ActiveRecord
1570
1727
  )
1571
1728
  end
1572
1729
 
1573
- def assert_mutability!
1574
- raise ImmutableRelation if @loaded
1575
- raise ImmutableRelation if defined?(@arel) && @arel
1730
+ def assert_modifiable!
1731
+ raise UnmodifiableRelation if @loaded || @arel
1576
1732
  end
1577
1733
 
1578
- def build_arel(aliases = nil)
1734
+ def build_arel(connection, aliases = nil)
1579
1735
  arel = Arel::SelectManager.new(table)
1580
1736
 
1581
1737
  build_joins(arel.join_sources, aliases)
@@ -1742,25 +1898,40 @@ module ActiveRecord
1742
1898
  return if with_values.empty?
1743
1899
 
1744
1900
  with_statements = with_values.map do |with_value|
1745
- raise ArgumentError, "Unsupported argument type: #{with_value} #{with_value.class}" unless with_value.is_a?(Hash)
1746
-
1747
1901
  build_with_value_from_hash(with_value)
1748
1902
  end
1749
1903
 
1750
- arel.with(with_statements)
1904
+ @with_is_recursive ? arel.with(:recursive, with_statements) : arel.with(with_statements)
1751
1905
  end
1752
1906
 
1753
1907
  def build_with_value_from_hash(hash)
1754
1908
  hash.map do |name, value|
1755
- expression =
1756
- case value
1757
- when Arel::Nodes::SqlLiteral then Arel::Nodes::Grouping.new(value)
1758
- when ActiveRecord::Relation then value.arel
1759
- when Arel::SelectManager then value
1760
- else
1761
- raise ArgumentError, "Unsupported argument type: `#{value}` #{value.class}"
1762
- end
1763
- Arel::Nodes::TableAlias.new(expression, name)
1909
+ Arel::Nodes::TableAlias.new(build_with_expression_from_value(value), name)
1910
+ end
1911
+ end
1912
+
1913
+ def build_with_expression_from_value(value, nested = false)
1914
+ case value
1915
+ when Arel::Nodes::SqlLiteral then Arel::Nodes::Grouping.new(value)
1916
+ when ActiveRecord::Relation
1917
+ if nested
1918
+ value.arel.ast
1919
+ else
1920
+ value.arel
1921
+ end
1922
+ when Arel::SelectManager then value
1923
+ when Array
1924
+ return build_with_expression_from_value(value.first, false) if value.size == 1
1925
+
1926
+ parts = value.map do |query|
1927
+ build_with_expression_from_value(query, true)
1928
+ end
1929
+
1930
+ parts.reduce do |result, value|
1931
+ Arel::Nodes::UnionAll.new(result, value)
1932
+ end
1933
+ else
1934
+ raise ArgumentError, "Unsupported argument type: `#{value}` #{value.class}"
1764
1935
  end
1765
1936
  end
1766
1937
 
@@ -1772,31 +1943,14 @@ module ActiveRecord
1772
1943
  ).join_sources.first
1773
1944
  end
1774
1945
 
1775
- def arel_columns(columns)
1776
- columns.flat_map do |field|
1777
- case field
1778
- when Symbol
1779
- arel_column(field.to_s) do |attr_name|
1780
- connection.quote_table_name(attr_name)
1781
- end
1782
- when String
1783
- arel_column(field, &:itself)
1784
- when Proc
1785
- field.call
1786
- else
1787
- field
1788
- end
1789
- end
1790
- end
1791
-
1792
1946
  def arel_column(field)
1793
1947
  field = klass.attribute_aliases[field] || field
1794
1948
  from = from_clause.name || from_clause.value
1795
1949
 
1796
1950
  if klass.columns_hash.key?(field) && (!from || table_name_matches?(from))
1797
1951
  table[field]
1798
- elsif field.match?(/\A\w+\.\w+\z/)
1799
- table, column = field.split(".")
1952
+ elsif /\A(?<table>(?:\w+\.)?\w+)\.(?<column>\w+)\z/ =~ field
1953
+ self.references_values |= [Arel.sql(table, retryable: true)]
1800
1954
  predicate_builder.resolve_arel_attribute(table, column) do
1801
1955
  lookup_table_klass_from_join_dependencies(table)
1802
1956
  end
@@ -1807,7 +1961,7 @@ module ActiveRecord
1807
1961
 
1808
1962
  def table_name_matches?(from)
1809
1963
  table_name = Regexp.escape(table.name)
1810
- quoted_table_name = Regexp.escape(connection.quote_table_name(table.name))
1964
+ quoted_table_name = Regexp.escape(adapter_class.quote_table_name(table.name))
1811
1965
  /(?:\A|(?<!FROM)\s)(?:\b#{table_name}\b|#{quoted_table_name})(?!\.)/i.match?(from.to_s)
1812
1966
  end
1813
1967
 
@@ -1863,7 +2017,9 @@ module ActiveRecord
1863
2017
  args.each do |arg|
1864
2018
  next unless arg.is_a?(Hash)
1865
2019
  arg.each do |_key, value|
1866
- unless VALID_DIRECTIONS.include?(value)
2020
+ if value.is_a?(Hash)
2021
+ validate_order_args([value])
2022
+ elsif VALID_DIRECTIONS.exclude?(value)
1867
2023
  raise ArgumentError,
1868
2024
  "Direction \"#{value}\" is invalid. Valid directions are: #{VALID_DIRECTIONS.to_a.inspect}"
1869
2025
  end
@@ -1871,10 +2027,14 @@ module ActiveRecord
1871
2027
  end
1872
2028
  end
1873
2029
 
2030
+ def flattened_args(args)
2031
+ args.flat_map { |e| (e.is_a?(Hash) || e.is_a?(Array)) ? flattened_args(e.to_a) : e }
2032
+ end
2033
+
1874
2034
  def preprocess_order_args(order_args)
1875
2035
  @klass.disallow_raw_sql!(
1876
- order_args.flat_map { |a| a.is_a?(Hash) ? a.keys : a },
1877
- permit: connection.column_name_with_order_matcher
2036
+ flattened_args(order_args),
2037
+ permit: model.adapter_class.column_name_with_order_matcher
1878
2038
  )
1879
2039
 
1880
2040
  validate_order_args(order_args)
@@ -1888,14 +2048,20 @@ module ActiveRecord
1888
2048
  when Symbol
1889
2049
  order_column(arg.to_s).asc
1890
2050
  when Hash
1891
- arg.map { |field, dir|
1892
- case field
1893
- when Arel::Nodes::SqlLiteral, Arel::Nodes::Node, Arel::Attribute
1894
- field.public_send(dir.downcase)
2051
+ arg.map do |key, value|
2052
+ if value.is_a?(Hash)
2053
+ value.map do |field, dir|
2054
+ order_column([key.to_s, field.to_s].join(".")).public_send(dir.downcase)
2055
+ end
1895
2056
  else
1896
- order_column(field.to_s).public_send(dir.downcase)
2057
+ case key
2058
+ when Arel::Nodes::SqlLiteral, Arel::Nodes::Node, Arel::Attribute
2059
+ key.public_send(value.downcase)
2060
+ else
2061
+ order_column(key.to_s).public_send(value.downcase)
2062
+ end
1897
2063
  end
1898
- }
2064
+ end
1899
2065
  else
1900
2066
  arg
1901
2067
  end
@@ -1930,7 +2096,7 @@ module ActiveRecord
1930
2096
  arg.expr.relation.name
1931
2097
  end
1932
2098
  end
1933
- end.compact
2099
+ end.filter_map { |ref| Arel.sql(ref, retryable: true) if ref }
1934
2100
  end
1935
2101
 
1936
2102
  def extract_table_name_from(string)
@@ -1942,7 +2108,7 @@ module ActiveRecord
1942
2108
  if attr_name == "count" && !group_values.empty?
1943
2109
  table[attr_name]
1944
2110
  else
1945
- Arel.sql(connection.quote_table_name(attr_name))
2111
+ Arel.sql(adapter_class.quote_table_name(attr_name), retryable: true)
1946
2112
  end
1947
2113
  end
1948
2114
  end
@@ -2010,14 +2176,14 @@ module ActiveRecord
2010
2176
  def process_select_args(fields)
2011
2177
  fields.flat_map do |field|
2012
2178
  if field.is_a?(Hash)
2013
- transform_select_hash_values(field)
2179
+ arel_columns_from_hash(field)
2014
2180
  else
2015
2181
  field
2016
2182
  end
2017
2183
  end
2018
2184
  end
2019
2185
 
2020
- def transform_select_hash_values(fields)
2186
+ def arel_columns_from_hash(fields)
2021
2187
  fields.flat_map do |key, columns_aliases|
2022
2188
  case columns_aliases
2023
2189
  when Hash
@@ -2042,6 +2208,13 @@ module ActiveRecord
2042
2208
  end
2043
2209
  end
2044
2210
 
2211
+ def process_with_args(args)
2212
+ args.flat_map do |arg|
2213
+ raise ArgumentError, "Unsupported argument type: #{arg} #{arg.class}" unless arg.is_a?(Hash)
2214
+ arg.map { |k, v| { k => v } }
2215
+ end
2216
+ end
2217
+
2045
2218
  STRUCTURAL_VALUE_METHODS = (
2046
2219
  Relation::VALUE_METHODS -
2047
2220
  [:extending, :where, :having, :unscope, :references, :annotate, :optimizer_hints]
@@ -3,6 +3,9 @@
3
3
  module ActiveRecord
4
4
  class Relation
5
5
  module RecordFetchWarning
6
+ # Deprecated: subscribe to sql.active_record notifications and
7
+ # access the row count field to detect large result set sizes.
8
+ #
6
9
  # When this module is prepended to ActiveRecord::Relation and
7
10
  # +config.active_record.warn_on_records_fetched_greater_than+ is
8
11
  # set to an integer, if the number of records a query returns is
@@ -41,26 +41,10 @@ module ActiveRecord
41
41
  end
42
42
 
43
43
  def merge!(other, *rest) # :nodoc:
44
- options = rest.extract_options!
45
-
46
- if options.key?(:rewhere)
47
- if options[:rewhere]
48
- ActiveRecord.deprecator.warn(<<-MSG.squish)
49
- Specifying `Relation#merge(rewhere: true)` is deprecated, as that has now been
50
- the default since Rails 7.0. Setting the rewhere option will error in Rails 7.2
51
- MSG
52
- else
53
- ActiveRecord.deprecator.warn(<<-MSG.squish)
54
- `Relation#merge(rewhere: false)` is deprecated without replacement,
55
- and will be removed in Rails 7.2
56
- MSG
57
- end
58
- end
59
-
60
44
  if other.is_a?(Hash)
61
- Relation::HashMerger.new(self, other, options[:rewhere]).merge
45
+ Relation::HashMerger.new(self, other).merge
62
46
  elsif other.is_a?(Relation)
63
- Relation::Merger.new(self, other, options[:rewhere]).merge
47
+ Relation::Merger.new(self, other).merge
64
48
  elsif other.respond_to?(:to_proc)
65
49
  instance_exec(&other)
66
50
  else
@@ -23,12 +23,8 @@ module ActiveRecord
23
23
  WhereClause.new(predicates | other.predicates)
24
24
  end
25
25
 
26
- def merge(other, rewhere = nil)
27
- predicates = if rewhere
28
- except_predicates(other.extract_attributes)
29
- else
30
- predicates_unreferenced_by(other)
31
- end
26
+ def merge(other)
27
+ predicates = except_predicates(other.extract_attributes)
32
28
 
33
29
  WhereClause.new(predicates | other.predicates)
34
30
  end
@@ -51,7 +47,11 @@ module ActiveRecord
51
47
  right = right.ast
52
48
  right = right.expr if right.is_a?(Arel::Nodes::Grouping)
53
49
 
54
- or_clause = Arel::Nodes::Or.new(left, right)
50
+ or_clause = if left.is_a?(Arel::Nodes::Or)
51
+ Arel::Nodes::Or.new(left.children + [right])
52
+ else
53
+ Arel::Nodes::Or.new([left, right])
54
+ end
55
55
 
56
56
  common.predicates << Arel::Nodes::Grouping.new(or_clause)
57
57
  common
@@ -135,10 +135,14 @@ module ActiveRecord
135
135
 
136
136
  def extract_attribute(node)
137
137
  attr_node = nil
138
- Arel.fetch_attribute(node) do |attr|
139
- return if attr_node&.!= attr # all attr nodes should be the same
138
+
139
+ valid_attrs = Arel.fetch_attribute(node) do |attr|
140
+ !attr_node || attr_node == attr # all attr nodes should be the same
141
+ ensure
140
142
  attr_node = attr
141
143
  end
144
+ return unless valid_attrs # all nested nodes should yield an attribute
145
+
142
146
  attr_node
143
147
  end
144
148
 
@@ -156,18 +160,6 @@ module ActiveRecord
156
160
  equalities
157
161
  end
158
162
 
159
- def predicates_unreferenced_by(other)
160
- referenced_columns = other.referenced_columns
161
-
162
- predicates.reject do |node|
163
- attr = extract_attribute(node) || begin
164
- node.left if equality_node?(node) && node.left.is_a?(Arel::Predications)
165
- end
166
-
167
- attr && referenced_columns[attr]
168
- end
169
- end
170
-
171
163
  def equality_node?(node)
172
164
  !node.is_a?(String) && node.equality?
173
165
  end
@@ -184,6 +176,8 @@ module ActiveRecord
184
176
  end
185
177
 
186
178
  def except_predicates(columns)
179
+ return predicates if columns.empty?
180
+
187
181
  attrs = columns.extract! { |node| node.is_a?(Arel::Attribute) }
188
182
  non_attrs = columns.extract! { |node| node.is_a?(Arel::Predications) }
189
183