activerecord 7.0.0 → 7.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 (289) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +515 -1268
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +31 -31
  5. data/examples/performance.rb +2 -2
  6. data/lib/active_record/aggregations.rb +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 +28 -17
  20. data/lib/active_record/associations/collection_proxy.rb +36 -13
  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 +28 -18
  24. data/lib/active_record/associations/has_many_through_association.rb +10 -6
  25. data/lib/active_record/associations/has_one_association.rb +10 -3
  26. data/lib/active_record/associations/join_dependency/join_association.rb +27 -25
  27. data/lib/active_record/associations/join_dependency.rb +18 -14
  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 +2 -4
  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 +378 -491
  36. data/lib/active_record/attribute_assignment.rb +1 -13
  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 +153 -70
  44. data/lib/active_record/attribute_methods/time_zone_conversion.rb +7 -6
  45. data/lib/active_record/attribute_methods/write.rb +6 -6
  46. data/lib/active_record/attribute_methods.rb +153 -40
  47. data/lib/active_record/attributes.rb +63 -48
  48. data/lib/active_record/autosave_association.rb +70 -38
  49. data/lib/active_record/base.rb +12 -8
  50. data/lib/active_record/callbacks.rb +16 -32
  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 -34
  54. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +124 -132
  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 +297 -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 +215 -63
  61. data/lib/active_record/connection_adapters/abstract/quoting.rb +83 -65
  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 +163 -29
  65. data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +14 -1
  66. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +319 -135
  67. data/lib/active_record/connection_adapters/abstract/transaction.rb +367 -75
  68. data/lib/active_record/connection_adapters/abstract_adapter.rb +512 -126
  69. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +282 -119
  70. data/lib/active_record/connection_adapters/column.rb +9 -0
  71. data/lib/active_record/connection_adapters/mysql/column.rb +1 -0
  72. data/lib/active_record/connection_adapters/mysql/database_statements.rb +27 -140
  73. data/lib/active_record/connection_adapters/mysql/quoting.rb +64 -52
  74. data/lib/active_record/connection_adapters/mysql/schema_creation.rb +9 -0
  75. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +10 -1
  76. data/lib/active_record/connection_adapters/mysql/schema_dumper.rb +8 -2
  77. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +45 -14
  78. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +152 -0
  79. data/lib/active_record/connection_adapters/mysql2_adapter.rb +101 -68
  80. data/lib/active_record/connection_adapters/pool_config.rb +20 -10
  81. data/lib/active_record/connection_adapters/pool_manager.rb +19 -9
  82. data/lib/active_record/connection_adapters/postgresql/column.rb +16 -3
  83. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +101 -48
  84. data/lib/active_record/connection_adapters/postgresql/oid/array.rb +1 -1
  85. data/lib/active_record/connection_adapters/postgresql/oid/cidr.rb +6 -0
  86. data/lib/active_record/connection_adapters/postgresql/oid/hstore.rb +2 -2
  87. data/lib/active_record/connection_adapters/postgresql/oid/interval.rb +1 -1
  88. data/lib/active_record/connection_adapters/postgresql/oid/money.rb +3 -2
  89. data/lib/active_record/connection_adapters/postgresql/oid/range.rb +11 -2
  90. data/lib/active_record/connection_adapters/postgresql/oid/timestamp_with_time_zone.rb +4 -2
  91. data/lib/active_record/connection_adapters/postgresql/oid/uuid.rb +14 -4
  92. data/lib/active_record/connection_adapters/postgresql/quoting.rb +94 -61
  93. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +6 -10
  94. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +76 -6
  95. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +151 -2
  96. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +53 -0
  97. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +379 -66
  98. data/lib/active_record/connection_adapters/postgresql/utils.rb +9 -10
  99. data/lib/active_record/connection_adapters/postgresql_adapter.rb +370 -203
  100. data/lib/active_record/connection_adapters/schema_cache.rb +302 -79
  101. data/lib/active_record/connection_adapters/sqlite3/column.rb +62 -0
  102. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +60 -43
  103. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +61 -46
  104. data/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +22 -0
  105. data/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb +20 -0
  106. data/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +16 -0
  107. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +64 -22
  108. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +321 -110
  109. data/lib/active_record/connection_adapters/statement_pool.rb +7 -0
  110. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +99 -0
  111. data/lib/active_record/connection_adapters/trilogy_adapter.rb +229 -0
  112. data/lib/active_record/connection_adapters.rb +124 -1
  113. data/lib/active_record/connection_handling.rb +98 -106
  114. data/lib/active_record/core.rb +220 -177
  115. data/lib/active_record/counter_cache.rb +68 -34
  116. data/lib/active_record/database_configurations/connection_url_resolver.rb +8 -2
  117. data/lib/active_record/database_configurations/database_config.rb +26 -5
  118. data/lib/active_record/database_configurations/hash_config.rb +52 -34
  119. data/lib/active_record/database_configurations/url_config.rb +37 -12
  120. data/lib/active_record/database_configurations.rb +88 -35
  121. data/lib/active_record/delegated_type.rb +40 -11
  122. data/lib/active_record/deprecator.rb +7 -0
  123. data/lib/active_record/destroy_association_async_job.rb +3 -1
  124. data/lib/active_record/disable_joins_association_relation.rb +1 -1
  125. data/lib/active_record/dynamic_matchers.rb +2 -2
  126. data/lib/active_record/encryption/auto_filtered_parameters.rb +66 -0
  127. data/lib/active_record/encryption/cipher/aes256_gcm.rb +4 -1
  128. data/lib/active_record/encryption/config.rb +25 -1
  129. data/lib/active_record/encryption/configurable.rb +13 -14
  130. data/lib/active_record/encryption/context.rb +10 -3
  131. data/lib/active_record/encryption/contexts.rb +8 -4
  132. data/lib/active_record/encryption/derived_secret_key_provider.rb +9 -3
  133. data/lib/active_record/encryption/deterministic_key_provider.rb +1 -1
  134. data/lib/active_record/encryption/encryptable_record.rb +47 -25
  135. data/lib/active_record/encryption/encrypted_attribute_type.rb +49 -14
  136. data/lib/active_record/encryption/encryptor.rb +25 -10
  137. data/lib/active_record/encryption/envelope_encryption_key_provider.rb +3 -3
  138. data/lib/active_record/encryption/extended_deterministic_queries.rb +83 -86
  139. data/lib/active_record/encryption/extended_deterministic_uniqueness_validator.rb +3 -3
  140. data/lib/active_record/encryption/key_generator.rb +12 -1
  141. data/lib/active_record/encryption/message.rb +1 -1
  142. data/lib/active_record/encryption/message_pack_message_serializer.rb +76 -0
  143. data/lib/active_record/encryption/message_serializer.rb +6 -0
  144. data/lib/active_record/encryption/null_encryptor.rb +4 -0
  145. data/lib/active_record/encryption/properties.rb +4 -4
  146. data/lib/active_record/encryption/read_only_null_encryptor.rb +4 -0
  147. data/lib/active_record/encryption/scheme.rb +23 -22
  148. data/lib/active_record/encryption.rb +1 -0
  149. data/lib/active_record/enum.rb +131 -27
  150. data/lib/active_record/errors.rb +151 -31
  151. data/lib/active_record/explain.rb +21 -12
  152. data/lib/active_record/explain_subscriber.rb +1 -1
  153. data/lib/active_record/fixture_set/model_metadata.rb +14 -4
  154. data/lib/active_record/fixture_set/render_context.rb +2 -0
  155. data/lib/active_record/fixture_set/table_row.rb +29 -8
  156. data/lib/active_record/fixtures.rb +169 -99
  157. data/lib/active_record/future_result.rb +47 -8
  158. data/lib/active_record/gem_version.rb +3 -3
  159. data/lib/active_record/inheritance.rb +34 -18
  160. data/lib/active_record/insert_all.rb +72 -22
  161. data/lib/active_record/integration.rb +13 -10
  162. data/lib/active_record/internal_metadata.rb +124 -20
  163. data/lib/active_record/locking/optimistic.rb +39 -24
  164. data/lib/active_record/locking/pessimistic.rb +8 -5
  165. data/lib/active_record/log_subscriber.rb +28 -27
  166. data/lib/active_record/marshalling.rb +56 -0
  167. data/lib/active_record/message_pack.rb +124 -0
  168. data/lib/active_record/middleware/database_selector/resolver.rb +4 -0
  169. data/lib/active_record/middleware/database_selector.rb +18 -13
  170. data/lib/active_record/middleware/shard_selector.rb +7 -5
  171. data/lib/active_record/migration/command_recorder.rb +110 -13
  172. data/lib/active_record/migration/compatibility.rb +174 -64
  173. data/lib/active_record/migration/default_strategy.rb +22 -0
  174. data/lib/active_record/migration/execution_strategy.rb +19 -0
  175. data/lib/active_record/migration/pending_migration_connection.rb +21 -0
  176. data/lib/active_record/migration.rb +292 -125
  177. data/lib/active_record/model_schema.rb +113 -112
  178. data/lib/active_record/nested_attributes.rb +35 -9
  179. data/lib/active_record/normalization.rb +163 -0
  180. data/lib/active_record/persistence.rb +177 -345
  181. data/lib/active_record/promise.rb +84 -0
  182. data/lib/active_record/query_cache.rb +19 -25
  183. data/lib/active_record/query_logs.rb +102 -51
  184. data/lib/active_record/query_logs_formatter.rb +41 -0
  185. data/lib/active_record/querying.rb +34 -9
  186. data/lib/active_record/railtie.rb +153 -100
  187. data/lib/active_record/railties/controller_runtime.rb +24 -10
  188. data/lib/active_record/railties/databases.rake +148 -152
  189. data/lib/active_record/railties/job_runtime.rb +23 -0
  190. data/lib/active_record/readonly_attributes.rb +32 -5
  191. data/lib/active_record/reflection.rb +278 -69
  192. data/lib/active_record/relation/batches/batch_enumerator.rb +20 -5
  193. data/lib/active_record/relation/batches.rb +198 -63
  194. data/lib/active_record/relation/calculations.rb +293 -108
  195. data/lib/active_record/relation/delegation.rb +31 -20
  196. data/lib/active_record/relation/finder_methods.rb +93 -18
  197. data/lib/active_record/relation/merger.rb +6 -6
  198. data/lib/active_record/relation/predicate_builder/array_handler.rb +2 -2
  199. data/lib/active_record/relation/predicate_builder/association_query_value.rb +38 -4
  200. data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +10 -7
  201. data/lib/active_record/relation/predicate_builder/relation_handler.rb +5 -1
  202. data/lib/active_record/relation/predicate_builder.rb +28 -16
  203. data/lib/active_record/relation/query_attribute.rb +25 -1
  204. data/lib/active_record/relation/query_methods.rb +625 -107
  205. data/lib/active_record/relation/record_fetch_warning.rb +3 -0
  206. data/lib/active_record/relation/spawn_methods.rb +5 -4
  207. data/lib/active_record/relation/where_clause.rb +7 -19
  208. data/lib/active_record/relation.rb +602 -96
  209. data/lib/active_record/result.rb +55 -52
  210. data/lib/active_record/runtime_registry.rb +63 -1
  211. data/lib/active_record/sanitization.rb +76 -30
  212. data/lib/active_record/schema.rb +39 -23
  213. data/lib/active_record/schema_dumper.rb +82 -30
  214. data/lib/active_record/schema_migration.rb +75 -24
  215. data/lib/active_record/scoping/default.rb +20 -12
  216. data/lib/active_record/scoping/named.rb +3 -2
  217. data/lib/active_record/scoping.rb +2 -1
  218. data/lib/active_record/secure_password.rb +60 -0
  219. data/lib/active_record/secure_token.rb +21 -3
  220. data/lib/active_record/serialization.rb +5 -0
  221. data/lib/active_record/signed_id.rb +29 -8
  222. data/lib/active_record/statement_cache.rb +7 -7
  223. data/lib/active_record/store.rb +16 -11
  224. data/lib/active_record/suppressor.rb +3 -1
  225. data/lib/active_record/table_metadata.rb +7 -3
  226. data/lib/active_record/tasks/database_tasks.rb +191 -121
  227. data/lib/active_record/tasks/mysql_database_tasks.rb +15 -6
  228. data/lib/active_record/tasks/postgresql_database_tasks.rb +17 -15
  229. data/lib/active_record/tasks/sqlite_database_tasks.rb +16 -7
  230. data/lib/active_record/test_fixtures.rb +174 -152
  231. data/lib/active_record/testing/query_assertions.rb +121 -0
  232. data/lib/active_record/timestamp.rb +31 -17
  233. data/lib/active_record/token_for.rb +123 -0
  234. data/lib/active_record/touch_later.rb +12 -7
  235. data/lib/active_record/transaction.rb +132 -0
  236. data/lib/active_record/transactions.rb +109 -27
  237. data/lib/active_record/translation.rb +1 -3
  238. data/lib/active_record/type/adapter_specific_registry.rb +1 -8
  239. data/lib/active_record/type/internal/timezone.rb +7 -2
  240. data/lib/active_record/type/serialized.rb +9 -7
  241. data/lib/active_record/type/time.rb +4 -0
  242. data/lib/active_record/type_caster/connection.rb +4 -4
  243. data/lib/active_record/validations/absence.rb +1 -1
  244. data/lib/active_record/validations/associated.rb +12 -6
  245. data/lib/active_record/validations/numericality.rb +5 -4
  246. data/lib/active_record/validations/presence.rb +5 -28
  247. data/lib/active_record/validations/uniqueness.rb +63 -14
  248. data/lib/active_record/validations.rb +12 -5
  249. data/lib/active_record/version.rb +1 -1
  250. data/lib/active_record.rb +266 -30
  251. data/lib/arel/alias_predication.rb +1 -1
  252. data/lib/arel/collectors/bind.rb +2 -0
  253. data/lib/arel/collectors/composite.rb +7 -0
  254. data/lib/arel/collectors/sql_string.rb +1 -1
  255. data/lib/arel/collectors/substitute_binds.rb +1 -1
  256. data/lib/arel/errors.rb +10 -0
  257. data/lib/arel/factory_methods.rb +4 -0
  258. data/lib/arel/filter_predications.rb +1 -1
  259. data/lib/arel/nodes/binary.rb +6 -7
  260. data/lib/arel/nodes/bound_sql_literal.rb +65 -0
  261. data/lib/arel/nodes/cte.rb +36 -0
  262. data/lib/arel/nodes/filter.rb +1 -1
  263. data/lib/arel/nodes/fragments.rb +35 -0
  264. data/lib/arel/nodes/homogeneous_in.rb +1 -9
  265. data/lib/arel/nodes/leading_join.rb +8 -0
  266. data/lib/arel/nodes/{and.rb → nary.rb} +9 -2
  267. data/lib/arel/nodes/node.rb +115 -5
  268. data/lib/arel/nodes/sql_literal.rb +13 -0
  269. data/lib/arel/nodes/table_alias.rb +4 -0
  270. data/lib/arel/nodes.rb +6 -2
  271. data/lib/arel/predications.rb +3 -1
  272. data/lib/arel/select_manager.rb +1 -1
  273. data/lib/arel/table.rb +9 -5
  274. data/lib/arel/tree_manager.rb +8 -3
  275. data/lib/arel/update_manager.rb +2 -1
  276. data/lib/arel/visitors/dot.rb +1 -0
  277. data/lib/arel/visitors/mysql.rb +17 -5
  278. data/lib/arel/visitors/postgresql.rb +1 -12
  279. data/lib/arel/visitors/to_sql.rb +112 -34
  280. data/lib/arel/visitors/visitor.rb +2 -2
  281. data/lib/arel.rb +21 -3
  282. data/lib/rails/generators/active_record/application_record/USAGE +8 -0
  283. data/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt +4 -1
  284. data/lib/rails/generators/active_record/migration.rb +3 -1
  285. data/lib/rails/generators/active_record/model/USAGE +113 -0
  286. data/lib/rails/generators/active_record/model/model_generator.rb +15 -6
  287. metadata +59 -17
  288. data/lib/active_record/connection_adapters/legacy_pool_manager.rb +0 -35
  289. data/lib/active_record/null_relation.rb +0 -63
@@ -3,17 +3,16 @@
3
3
  require "active_record/relation/from_clause"
4
4
  require "active_record/relation/query_attribute"
5
5
  require "active_record/relation/where_clause"
6
- require "active_model/forbidden_attributes_protection"
7
6
  require "active_support/core_ext/array/wrap"
8
7
 
9
8
  module ActiveRecord
10
9
  module QueryMethods
11
10
  include ActiveModel::ForbiddenAttributesProtection
12
11
 
13
- # WhereChain objects act as placeholder for queries in which #where does not have any parameter.
14
- # In this case, #where must be chained with #not to return a new relation.
12
+ # +WhereChain+ objects act as placeholder for queries in which +where+ does not have any parameter.
13
+ # In this case, +where+ can be chained to return a new relation.
15
14
  class WhereChain
16
- def initialize(scope)
15
+ def initialize(scope) # :nodoc:
17
16
  @scope = scope
18
17
  end
19
18
 
@@ -39,7 +38,14 @@ module ActiveRecord
39
38
  # # SELECT * FROM users WHERE name NOT IN ('Ko1', 'Nobu')
40
39
  #
41
40
  # User.where.not(name: "Jon", role: "admin")
42
- # # SELECT * FROM users WHERE NOT (name == 'Jon' AND role == 'admin')
41
+ # # SELECT * FROM users WHERE NOT (name = 'Jon' AND role = 'admin')
42
+ #
43
+ # If there is a non-nil condition on a nullable column in the hash condition, the records that have
44
+ # nil values on the nullable column won't be returned.
45
+ # User.create!(nullable_country: nil)
46
+ # User.where.not(nullable_country: "UK")
47
+ # # SELECT * FROM users WHERE NOT (nullable_country = 'UK')
48
+ # # => []
43
49
  def not(opts, *rest)
44
50
  where_clause = @scope.send(:build_where_clause, opts, rest)
45
51
 
@@ -66,11 +72,31 @@ module ActiveRecord
66
72
  # # INNER JOIN "authors" ON "authors"."id" = "posts"."author_id"
67
73
  # # INNER JOIN "comments" ON "comments"."post_id" = "posts"."id"
68
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
69
88
  def associated(*associations)
70
89
  associations.each do |association|
71
- reflection = @scope.klass._reflect_on_association(association)
72
- @scope.joins!(association)
73
- self.not(reflection.table_name => { reflection.association_primary_key => nil })
90
+ reflection = scope_association_reflection(association)
91
+ unless @scope.joins_values.include?(reflection.name) || @scope.left_outer_joins_values.include?(reflection.name)
92
+ @scope.joins!(association)
93
+ end
94
+
95
+ if reflection.options[:class_name]
96
+ self.not(association => { reflection.association_primary_key => nil })
97
+ else
98
+ self.not(reflection.table_name => { reflection.association_primary_key => nil })
99
+ end
74
100
  end
75
101
 
76
102
  @scope
@@ -96,13 +122,35 @@ module ActiveRecord
96
122
  # # WHERE "authors"."id" IS NULL AND "comments"."id" IS NULL
97
123
  def missing(*associations)
98
124
  associations.each do |association|
99
- reflection = @scope.klass._reflect_on_association(association)
125
+ reflection = scope_association_reflection(association)
100
126
  @scope.left_outer_joins!(association)
101
- @scope.where!(reflection.table_name => { reflection.association_primary_key => nil })
127
+ if reflection.options[:class_name]
128
+ @scope.where!(association => { reflection.association_primary_key => nil })
129
+ else
130
+ @scope.where!(reflection.table_name => { reflection.association_primary_key => nil })
131
+ end
102
132
  end
103
133
 
104
134
  @scope
105
135
  end
136
+
137
+ private
138
+ def scope_association_reflection(association)
139
+ reflection = @scope.klass._reflect_on_association(association)
140
+ unless reflection
141
+ raise ArgumentError.new("An association named `:#{association}` does not exist on the model `#{@scope.name}`.")
142
+ end
143
+ reflection
144
+ end
145
+ end
146
+
147
+ # A wrapper to distinguish CTE joins from other nodes.
148
+ class CTEJoin # :nodoc:
149
+ attr_reader :name
150
+
151
+ def initialize(name)
152
+ @name = name
153
+ end
106
154
  end
107
155
 
108
156
  FROZEN_EMPTY_ARRAY = [].freeze
@@ -125,7 +173,7 @@ module ActiveRecord
125
173
  end # end
126
174
 
127
175
  def #{method_name}=(value) # def includes_values=(value)
128
- assert_mutability! # assert_mutability!
176
+ assert_modifiable! # assert_modifiable!
129
177
  @values[:#{name}] = value # @values[:includes] = value
130
178
  end # end
131
179
  CODE
@@ -133,45 +181,69 @@ module ActiveRecord
133
181
 
134
182
  alias extensions extending_values
135
183
 
136
- # Specify relationships to be included in the result set. For
137
- # example:
184
+ # Specify associations +args+ to be eager loaded to prevent N + 1 queries.
185
+ # A separate query is performed for each association, unless a join is
186
+ # required by conditions.
138
187
  #
139
- # users = User.includes(:address)
188
+ # For example:
189
+ #
190
+ # users = User.includes(:address).limit(5)
140
191
  # users.each do |user|
141
192
  # user.address.city
142
193
  # end
143
194
  #
144
- # allows you to access the +address+ attribute of the +User+ model without
145
- # firing an additional query. This will often result in a
146
- # performance improvement over a simple join.
195
+ # # SELECT "users".* FROM "users" LIMIT 5
196
+ # # SELECT "addresses".* FROM "addresses" WHERE "addresses"."id" IN (1,2,3,4,5)
197
+ #
198
+ # Instead of loading the 5 addresses with 5 separate queries, all addresses
199
+ # are loaded with a single query.
147
200
  #
148
- # You can also specify multiple relationships, like this:
201
+ # Loading the associations in a separate query will often result in a
202
+ # performance improvement over a simple join, as a join can result in many
203
+ # rows that contain redundant data and it performs poorly at scale.
149
204
  #
150
- # users = User.includes(:address, :friends)
205
+ # You can also specify multiple associations. Each association will result
206
+ # in an additional query:
151
207
  #
152
- # Loading nested relationships is possible using a Hash:
208
+ # User.includes(:address, :friends).to_a
209
+ # # SELECT "users".* FROM "users"
210
+ # # SELECT "addresses".* FROM "addresses" WHERE "addresses"."id" IN (1,2,3,4,5)
211
+ # # SELECT "friends".* FROM "friends" WHERE "friends"."user_id" IN (1,2,3,4,5)
153
212
  #
154
- # users = User.includes(:address, friends: [:address, :followers])
213
+ # Loading nested associations is possible using a Hash:
155
214
  #
156
- # === conditions
215
+ # User.includes(:address, friends: [:address, :followers])
216
+ #
217
+ # === Conditions
157
218
  #
158
219
  # If you want to add string conditions to your included models, you'll have
159
220
  # to explicitly reference them. For example:
160
221
  #
161
- # User.includes(:posts).where('posts.name = ?', 'example')
222
+ # User.includes(:posts).where('posts.name = ?', 'example').to_a
162
223
  #
163
224
  # Will throw an error, but this will work:
164
225
  #
165
- # User.includes(:posts).where('posts.name = ?', 'example').references(:posts)
226
+ # User.includes(:posts).where('posts.name = ?', 'example').references(:posts).to_a
227
+ # # SELECT "users"."id" AS t0_r0, ... FROM "users"
228
+ # # LEFT OUTER JOIN "posts" ON "posts"."user_id" = "users"."id"
229
+ # # WHERE "posts"."name" = ? [["name", "example"]]
230
+ #
231
+ # As the <tt>LEFT OUTER JOIN</tt> already contains the posts, the second query for
232
+ # the posts is no longer performed.
166
233
  #
167
234
  # Note that #includes works with association names while #references needs
168
235
  # the actual table name.
169
236
  #
170
- # If you pass the conditions via hash, you don't need to call #references
237
+ # If you pass the conditions via a Hash, you don't need to call #references
171
238
  # explicitly, as #where references the tables for you. For example, this
172
239
  # will work correctly:
173
240
  #
174
241
  # User.includes(:posts).where(posts: { name: 'example' })
242
+ #
243
+ # NOTE: Conditions affect both sides of an association. For example, the
244
+ # above code will return only users that have a post named "example",
245
+ # <em>and will only include posts named "example"</em>, even when a
246
+ # matching user has other additional posts.
175
247
  def includes(*args)
176
248
  check_if_method_has_arguments!(__callee__, args)
177
249
  spawn.includes!(*args)
@@ -182,12 +254,32 @@ module ActiveRecord
182
254
  self
183
255
  end
184
256
 
185
- # Forces eager loading by performing a LEFT OUTER JOIN on +args+:
257
+ # Specify associations +args+ to be eager loaded using a <tt>LEFT OUTER JOIN</tt>.
258
+ # Performs a single query joining all specified associations. For example:
259
+ #
260
+ # users = User.eager_load(:address).limit(5)
261
+ # users.each do |user|
262
+ # user.address.city
263
+ # end
264
+ #
265
+ # # SELECT "users"."id" AS t0_r0, "users"."name" AS t0_r1, ... FROM "users"
266
+ # # LEFT OUTER JOIN "addresses" ON "addresses"."id" = "users"."address_id"
267
+ # # LIMIT 5
268
+ #
269
+ # Instead of loading the 5 addresses with 5 separate queries, all addresses
270
+ # are loaded with a single joined query.
186
271
  #
187
- # User.eager_load(:posts)
188
- # # SELECT "users"."id" AS t0_r0, "users"."name" AS t0_r1, ...
189
- # # FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" =
190
- # # "users"."id"
272
+ # Loading multiple and nested associations is possible using Hashes and Arrays,
273
+ # similar to #includes:
274
+ #
275
+ # User.eager_load(:address, friends: [:address, :followers])
276
+ # # SELECT "users"."id" AS t0_r0, "users"."name" AS t0_r1, ... FROM "users"
277
+ # # LEFT OUTER JOIN "addresses" ON "addresses"."id" = "users"."address_id"
278
+ # # LEFT OUTER JOIN "friends" ON "friends"."user_id" = "users"."id"
279
+ # # ...
280
+ #
281
+ # NOTE: Loading the associations in a join can result in many rows that
282
+ # contain redundant data and it performs poorly at scale.
191
283
  def eager_load(*args)
192
284
  check_if_method_has_arguments!(__callee__, args)
193
285
  spawn.eager_load!(*args)
@@ -198,10 +290,28 @@ module ActiveRecord
198
290
  self
199
291
  end
200
292
 
201
- # Allows preloading of +args+, in the same way that #includes does:
293
+ # Specify associations +args+ to be eager loaded using separate queries.
294
+ # A separate query is performed for each association.
295
+ #
296
+ # users = User.preload(:address).limit(5)
297
+ # users.each do |user|
298
+ # user.address.city
299
+ # end
202
300
  #
203
- # User.preload(:posts)
204
- # # SELECT "posts".* FROM "posts" WHERE "posts"."user_id" IN (1, 2, 3)
301
+ # # SELECT "users".* FROM "users" LIMIT 5
302
+ # # SELECT "addresses".* FROM "addresses" WHERE "addresses"."id" IN (1,2,3,4,5)
303
+ #
304
+ # Instead of loading the 5 addresses with 5 separate queries, all addresses
305
+ # are loaded with a separate query.
306
+ #
307
+ # Loading multiple and nested associations is possible using Hashes and Arrays,
308
+ # similar to #includes:
309
+ #
310
+ # User.preload(:address, friends: [:address, :followers])
311
+ # # SELECT "users".* FROM "users"
312
+ # # SELECT "addresses".* FROM "addresses" WHERE "addresses"."id" IN (1,2,3,4,5)
313
+ # # SELECT "friends".* FROM "friends" WHERE "friends"."user_id" IN (1,2,3,4,5)
314
+ # # SELECT ...
205
315
  def preload(*args)
206
316
  check_if_method_has_arguments!(__callee__, args)
207
317
  spawn.preload!(*args)
@@ -226,7 +336,7 @@ module ActiveRecord
226
336
  end
227
337
 
228
338
  # Use to indicate that the given +table_names+ are referenced by an SQL string,
229
- # and should therefore be JOINed in any query rather than loaded separately.
339
+ # and should therefore be +JOIN+ed in any query rather than loaded separately.
230
340
  # This method only works in conjunction with #includes.
231
341
  # See #includes for more details.
232
342
  #
@@ -270,10 +380,18 @@ module ActiveRecord
270
380
  # Model.select(:field, :other_field, :and_one_more)
271
381
  # # => [#<Model id: nil, field: "value", other_field: "value", and_one_more: "value">]
272
382
  #
383
+ # The argument also can be a hash of fields and aliases.
384
+ #
385
+ # Model.select(models: { field: :alias, other_field: :other_alias })
386
+ # # => [#<Model id: nil, alias: "value", other_alias: "value">]
387
+ #
388
+ # Model.select(models: [:field, :other_field])
389
+ # # => [#<Model id: nil, field: "value", other_field: "value">]
390
+ #
273
391
  # You can also use one or more strings, which will be used unchanged as SELECT fields.
274
392
  #
275
393
  # Model.select('field AS field_one', 'other_field AS field_two')
276
- # # => [#<Model id: nil, field: "value", other_field: "value">]
394
+ # # => [#<Model id: nil, field_one: "value", field_two: "value">]
277
395
  #
278
396
  # If an alias was specified, it will be accessible from the resulting objects:
279
397
  #
@@ -284,7 +402,7 @@ module ActiveRecord
284
402
  # except +id+ will throw ActiveModel::MissingAttributeError:
285
403
  #
286
404
  # Model.select(:field).first.other_field
287
- # # => ActiveModel::MissingAttributeError: missing attribute: other_field
405
+ # # => ActiveModel::MissingAttributeError: missing attribute 'other_field' for Model
288
406
  def select(*fields)
289
407
  if block_given?
290
408
  if fields.any?
@@ -295,6 +413,8 @@ module ActiveRecord
295
413
  end
296
414
 
297
415
  check_if_method_has_arguments!(__callee__, fields, "Call `select' with at least one field.")
416
+
417
+ fields = process_select_args(fields)
298
418
  spawn._select!(*fields)
299
419
  end
300
420
 
@@ -303,6 +423,102 @@ module ActiveRecord
303
423
  self
304
424
  end
305
425
 
426
+ # Add a Common Table Expression (CTE) that you can then reference within another SELECT statement.
427
+ #
428
+ # Note: CTE's are only supported in MySQL for versions 8.0 and above. You will not be able to
429
+ # use CTE's with MySQL 5.7.
430
+ #
431
+ # Post.with(posts_with_tags: Post.where("tags_count > ?", 0))
432
+ # # => ActiveRecord::Relation
433
+ # # WITH posts_with_tags AS (
434
+ # # SELECT * FROM posts WHERE (tags_count > 0)
435
+ # # )
436
+ # # SELECT * FROM posts
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
+ #
449
+ # Once you define Common Table Expression you can use custom +FROM+ value or +JOIN+ to reference it.
450
+ #
451
+ # Post.with(posts_with_tags: Post.where("tags_count > ?", 0)).from("posts_with_tags AS posts")
452
+ # # => ActiveRecord::Relation
453
+ # # WITH posts_with_tags AS (
454
+ # # SELECT * FROM posts WHERE (tags_count > 0)
455
+ # # )
456
+ # # SELECT * FROM posts_with_tags AS posts
457
+ #
458
+ # Post.with(posts_with_tags: Post.where("tags_count > ?", 0)).joins("JOIN posts_with_tags ON posts_with_tags.id = posts.id")
459
+ # # => ActiveRecord::Relation
460
+ # # WITH posts_with_tags AS (
461
+ # # SELECT * FROM posts WHERE (tags_count > 0)
462
+ # # )
463
+ # # SELECT * FROM posts JOIN posts_with_tags ON posts_with_tags.id = posts.id
464
+ #
465
+ # It is recommended to pass a query as ActiveRecord::Relation. If that is not possible
466
+ # and you have verified it is safe for the database, you can pass it as SQL literal
467
+ # using +Arel+.
468
+ #
469
+ # Post.with(popular_posts: Arel.sql("... complex sql to calculate posts popularity ..."))
470
+ #
471
+ # Great caution should be taken to avoid SQL injection vulnerabilities. This method should not
472
+ # be used with unsafe values that include unsanitized input.
473
+ #
474
+ # To add multiple CTEs just pass multiple key-value pairs
475
+ #
476
+ # Post.with(
477
+ # posts_with_comments: Post.where("comments_count > ?", 0),
478
+ # posts_with_tags: Post.where("tags_count > ?", 0)
479
+ # )
480
+ #
481
+ # or chain multiple +.with+ calls
482
+ #
483
+ # Post
484
+ # .with(posts_with_comments: Post.where("comments_count > ?", 0))
485
+ # .with(posts_with_tags: Post.where("tags_count > ?", 0))
486
+ def with(*args)
487
+ raise ArgumentError, "ActiveRecord::Relation#with does not accept a block" if block_given?
488
+ check_if_method_has_arguments!(__callee__, args)
489
+ spawn.with!(*args)
490
+ end
491
+
492
+ # Like #with, but modifies relation in place.
493
+ def with!(*args) # :nodoc:
494
+ self.with_values += args
495
+ self
496
+ end
497
+
498
+ # Add a recursive Common Table Expression (CTE) that you can then reference within another SELECT statement.
499
+ #
500
+ # 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')])
501
+ # # => ActiveRecord::Relation
502
+ # # WITH post_and_replies AS (
503
+ # # (SELECT * FROM posts WHERE id = 42)
504
+ # # UNION ALL
505
+ # # (SELECT * FROM posts JOIN posts_and_replies ON posts.in_reply_to_id = posts_and_replies.id)
506
+ # # )
507
+ # # SELECT * FROM posts
508
+ #
509
+ # See `#with` for more information.
510
+ def with_recursive(*args)
511
+ check_if_method_has_arguments!(__callee__, args)
512
+ spawn.with_recursive!(*args)
513
+ end
514
+
515
+ # Like #with_recursive but modifies the relation in place.
516
+ def with_recursive!(*args) # :nodoc:
517
+ self.with_values += args
518
+ @with_is_recursive = true
519
+ self
520
+ end
521
+
306
522
  # Allows you to change a previously set select statement.
307
523
  #
308
524
  # Post.select(:title, :body)
@@ -315,6 +531,7 @@ module ActiveRecord
315
531
  # Note that we're unscoping the entire select statement.
316
532
  def reselect(*args)
317
533
  check_if_method_has_arguments!(__callee__, args)
534
+ args = process_select_args(args)
318
535
  spawn.reselect!(*args)
319
536
  end
320
537
 
@@ -354,6 +571,27 @@ module ActiveRecord
354
571
  self
355
572
  end
356
573
 
574
+ # Allows you to change a previously set group statement.
575
+ #
576
+ # Post.group(:title, :body)
577
+ # # SELECT `posts`.`*` FROM `posts` GROUP BY `posts`.`title`, `posts`.`body`
578
+ #
579
+ # Post.group(:title, :body).regroup(:title)
580
+ # # SELECT `posts`.`*` FROM `posts` GROUP BY `posts`.`title`
581
+ #
582
+ # This is short-hand for <tt>unscope(:group).group(fields)</tt>.
583
+ # Note that we're unscoping the entire group statement.
584
+ def regroup(*args)
585
+ check_if_method_has_arguments!(__callee__, args)
586
+ spawn.regroup!(*args)
587
+ end
588
+
589
+ # Same as #regroup but operates on relation in-place instead of copying.
590
+ def regroup!(*args) # :nodoc:
591
+ self.group_values = args
592
+ self
593
+ end
594
+
357
595
  # Applies an <code>ORDER BY</code> clause to a query.
358
596
  #
359
597
  # #order accepts arguments in one of several formats.
@@ -402,7 +640,7 @@ module ActiveRecord
402
640
  # User.order(Arel.sql('end_date - start_date'))
403
641
  # # SELECT "users".* FROM "users" ORDER BY end_date - start_date
404
642
  #
405
- # Custom query syntax, like JSON columns for Postgres, is supported in this way.
643
+ # Custom query syntax, like JSON columns for PostgreSQL, is supported in this way.
406
644
  #
407
645
  # User.order(Arel.sql("payload->>'kind'"))
408
646
  # # SELECT "users".* FROM "users" ORDER BY payload->>'kind'
@@ -420,22 +658,64 @@ module ActiveRecord
420
658
  self
421
659
  end
422
660
 
423
- # Allows to specify an order by a specific set of values. Depending on your
424
- # adapter this will either use a CASE statement or a built-in function.
661
+ # Applies an <tt>ORDER BY</tt> clause based on a given +column+,
662
+ # ordered and filtered by a specific set of +values+.
425
663
  #
426
664
  # User.in_order_of(:id, [1, 5, 3])
427
- # # SELECT "users".* FROM "users" ORDER BY FIELD("users"."id", 1, 5, 3)
665
+ # # SELECT "users".* FROM "users"
666
+ # # WHERE "users"."id" IN (1, 5, 3)
667
+ # # ORDER BY CASE
668
+ # # WHEN "users"."id" = 1 THEN 1
669
+ # # WHEN "users"."id" = 5 THEN 2
670
+ # # WHEN "users"."id" = 3 THEN 3
671
+ # # END ASC
672
+ #
673
+ # +column+ can point to an enum column; the actual query generated may be different depending
674
+ # on the database adapter and the column definition.
675
+ #
676
+ # class Conversation < ActiveRecord::Base
677
+ # enum :status, [ :active, :archived ]
678
+ # end
679
+ #
680
+ # Conversation.in_order_of(:status, [:archived, :active])
681
+ # # SELECT "conversations".* FROM "conversations"
682
+ # # WHERE "conversations"."status" IN (1, 0)
683
+ # # ORDER BY CASE
684
+ # # WHEN "conversations"."status" = 1 THEN 1
685
+ # # WHEN "conversations"."status" = 0 THEN 2
686
+ # # END ASC
687
+ #
688
+ # +values+ can also include +nil+.
689
+ #
690
+ # Conversation.in_order_of(:status, [nil, :archived, :active])
691
+ # # SELECT "conversations".* FROM "conversations"
692
+ # # WHERE ("conversations"."status" IN (1, 0) OR "conversations"."status" IS NULL)
693
+ # # ORDER BY CASE
694
+ # # WHEN "conversations"."status" IS NULL THEN 1
695
+ # # WHEN "conversations"."status" = 1 THEN 2
696
+ # # WHEN "conversations"."status" = 0 THEN 3
697
+ # # END ASC
428
698
  #
429
699
  def in_order_of(column, values)
430
- klass.disallow_raw_sql!([column], permit: connection.column_name_with_order_matcher)
700
+ klass.disallow_raw_sql!([column], permit: model.adapter_class.column_name_with_order_matcher)
701
+ return spawn.none! if values.empty?
431
702
 
432
703
  references = column_references([column])
433
704
  self.references_values |= references unless references.empty?
434
705
 
435
706
  values = values.map { |value| type_caster.type_cast_for_database(column, value) }
436
- column = order_column(column.to_s) if column.is_a?(Symbol)
707
+ arel_column = column.is_a?(Arel::Nodes::SqlLiteral) ? column : order_column(column.to_s)
708
+
709
+ where_clause =
710
+ if values.include?(nil)
711
+ arel_column.in(values.compact).or(arel_column.eq(nil))
712
+ else
713
+ arel_column.in(values)
714
+ end
437
715
 
438
- spawn.order!(connection.field_ordered_value(column, values))
716
+ spawn
717
+ .order!(build_case_for_value_position(arel_column, values))
718
+ .where!(where_clause)
439
719
  end
440
720
 
441
721
  # Replaces any existing order defined on the relation with the specified order.
@@ -446,7 +726,7 @@ module ActiveRecord
446
726
  #
447
727
  # User.order('email DESC').reorder('id ASC').order('name ASC')
448
728
  #
449
- # generates a query with 'ORDER BY id ASC, name ASC'.
729
+ # generates a query with <tt>ORDER BY id ASC, name ASC</tt>.
450
730
  def reorder(*args)
451
731
  check_if_method_has_arguments!(__callee__, args) do
452
732
  sanitize_order_arguments(args)
@@ -465,7 +745,8 @@ module ActiveRecord
465
745
 
466
746
  VALID_UNSCOPING_VALUES = Set.new([:where, :select, :group, :order, :lock,
467
747
  :limit, :offset, :joins, :left_outer_joins, :annotate,
468
- :includes, :from, :readonly, :having, :optimizer_hints])
748
+ :includes, :eager_load, :preload, :from, :readonly,
749
+ :having, :optimizer_hints, :with])
469
750
 
470
751
  # Removes an unwanted relation that is already defined on a chain of relations.
471
752
  # This is useful when passing around chains of relations and would like to
@@ -515,7 +796,7 @@ module ActiveRecord
515
796
  if !VALID_UNSCOPING_VALUES.include?(scope)
516
797
  raise ArgumentError, "Called unscope() with invalid unscoping argument ':#{scope}'. Valid arguments are :#{VALID_UNSCOPING_VALUES.to_a.join(", :")}."
517
798
  end
518
- assert_mutability!
799
+ assert_modifiable!
519
800
  @values.delete(scope)
520
801
  when Hash
521
802
  scope.each do |key, target_value|
@@ -575,7 +856,7 @@ module ActiveRecord
575
856
  # Performs LEFT OUTER JOINs on +args+:
576
857
  #
577
858
  # User.left_outer_joins(:posts)
578
- # => SELECT "users".* FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" = "users"."id"
859
+ # # SELECT "users".* FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" = "users"."id"
579
860
  #
580
861
  def left_outer_joins(*args)
581
862
  check_if_method_has_arguments!(__callee__, args)
@@ -595,7 +876,7 @@ module ActiveRecord
595
876
  # SQL is given as an illustration; the actual query generated may be different depending
596
877
  # on the database adapter.
597
878
  #
598
- # === string
879
+ # === \String
599
880
  #
600
881
  # A single string, without additional arguments, is passed to the query
601
882
  # constructor as an SQL fragment, and used in the where clause of the query.
@@ -607,7 +888,7 @@ module ActiveRecord
607
888
  # to injection attacks if not done properly. As an alternative, it is recommended
608
889
  # to use one of the following methods.
609
890
  #
610
- # === array
891
+ # === \Array
611
892
  #
612
893
  # If an array is passed, then the first element of the array is treated as a template, and
613
894
  # the remaining elements are inserted into the template to generate the condition.
@@ -647,7 +928,7 @@ module ActiveRecord
647
928
  # dependencies on the underlying database. If your code is intended for general consumption,
648
929
  # test with multiple database backends.
649
930
  #
650
- # === hash
931
+ # === \Hash
651
932
  #
652
933
  # #where will also accept a hash condition, in which the keys are fields and the values
653
934
  # are values to be searched for.
@@ -681,6 +962,12 @@ module ActiveRecord
681
962
  # PriceEstimate.where(estimate_of: treasure)
682
963
  # PriceEstimate.where(estimate_of_type: 'Treasure', estimate_of_id: treasure)
683
964
  #
965
+ # Hash conditions may also be specified in a tuple-like syntax. Hash keys may be
966
+ # an array of columns with an array of tuples as values.
967
+ #
968
+ # Article.where([:author_id, :id] => [[15, 1], [15, 2]])
969
+ # # SELECT * FROM articles WHERE author_id = 15 AND id = 1 OR author_id = 15 AND id = 2
970
+ #
684
971
  # === Joins
685
972
  #
686
973
  # If the relation is the result of a join, you may create a condition which uses any of the
@@ -693,17 +980,31 @@ module ActiveRecord
693
980
  # User.joins(:posts).where("posts.published" => true)
694
981
  # User.joins(:posts).where(posts: { published: true })
695
982
  #
696
- # === no argument
983
+ # === No Argument
697
984
  #
698
985
  # If no argument is passed, #where returns a new instance of WhereChain, that
699
- # can be chained with #not to return a new relation that negates the where clause.
986
+ # can be chained with WhereChain#not, WhereChain#missing, or WhereChain#associated.
987
+ #
988
+ # Chaining with WhereChain#not:
700
989
  #
701
990
  # User.where.not(name: "Jon")
702
991
  # # SELECT * FROM users WHERE name != 'Jon'
703
992
  #
704
- # See WhereChain for more details on #not.
993
+ # Chaining with WhereChain#associated:
994
+ #
995
+ # Post.where.associated(:author)
996
+ # # SELECT "posts".* FROM "posts"
997
+ # # INNER JOIN "authors" ON "authors"."id" = "posts"."author_id"
998
+ # # WHERE "authors"."id" IS NOT NULL
705
999
  #
706
- # === blank condition
1000
+ # Chaining with WhereChain#missing:
1001
+ #
1002
+ # Post.where.missing(:author)
1003
+ # # SELECT "posts".* FROM "posts"
1004
+ # # LEFT OUTER JOIN "authors" ON "authors"."id" = "posts"."author_id"
1005
+ # # WHERE "authors"."id" IS NULL
1006
+ #
1007
+ # === Blank Condition
707
1008
  #
708
1009
  # If the condition is any blank-ish object, then #where is a no-op and returns
709
1010
  # the current relation.
@@ -736,6 +1037,8 @@ module ActiveRecord
736
1037
  # This is short-hand for <tt>unscope(where: conditions.keys).where(conditions)</tt>.
737
1038
  # Note that unlike reorder, we're only unscoping the named conditions -- not the entire where statement.
738
1039
  def rewhere(conditions)
1040
+ return unscope(:where) if conditions.nil?
1041
+
739
1042
  scope = spawn
740
1043
  where_clause = scope.build_where_clause(conditions)
741
1044
 
@@ -841,7 +1144,11 @@ module ActiveRecord
841
1144
  #
842
1145
  def or(other)
843
1146
  if other.is_a?(Relation)
844
- spawn.or!(other)
1147
+ if @none
1148
+ other.spawn
1149
+ else
1150
+ spawn.or!(other)
1151
+ end
845
1152
  else
846
1153
  raise ArgumentError, "You have passed #{other.class.name} object to #or. Pass an ActiveRecord::Relation object instead."
847
1154
  end
@@ -854,7 +1161,7 @@ module ActiveRecord
854
1161
  raise ArgumentError, "Relation passed to #or must be structurally compatible. Incompatible values: #{incompatible_values}"
855
1162
  end
856
1163
 
857
- self.where_clause = self.where_clause.or(other.where_clause)
1164
+ self.where_clause = where_clause.or(other.where_clause)
858
1165
  self.having_clause = having_clause.or(other.having_clause)
859
1166
  self.references_values |= other.references_values
860
1167
 
@@ -954,15 +1261,29 @@ module ActiveRecord
954
1261
  end
955
1262
 
956
1263
  def none! # :nodoc:
957
- where!("1=0").extending!(NullRelation)
1264
+ unless @none
1265
+ where!("1=0")
1266
+ @none = true
1267
+ end
1268
+ self
958
1269
  end
959
1270
 
960
- # Sets readonly attributes for the returned relation. If value is
961
- # true (default), attempting to update a record will result in an error.
1271
+ def null_relation? # :nodoc:
1272
+ @none
1273
+ end
1274
+
1275
+ # Mark a relation as readonly. Attempting to update a record will result in
1276
+ # an error.
962
1277
  #
963
1278
  # users = User.readonly
964
1279
  # users.first.save
965
1280
  # => ActiveRecord::ReadOnlyRecord: User is marked as readonly
1281
+ #
1282
+ # To make a readonly relation writable, pass +false+.
1283
+ #
1284
+ # users.readonly(false)
1285
+ # users.first.save
1286
+ # => true
966
1287
  def readonly(value = true)
967
1288
  spawn.readonly!(value)
968
1289
  end
@@ -1079,7 +1400,7 @@ module ActiveRecord
1079
1400
  #
1080
1401
  # The object returned is a relation, which can be further extended.
1081
1402
  #
1082
- # === Using a module
1403
+ # === Using a \Module
1083
1404
  #
1084
1405
  # module Pagination
1085
1406
  # def page(number)
@@ -1094,7 +1415,7 @@ module ActiveRecord
1094
1415
  #
1095
1416
  # scope = Model.all.extending(Pagination, SomethingElse)
1096
1417
  #
1097
- # === Using a block
1418
+ # === Using a Block
1098
1419
  #
1099
1420
  # scope = Model.all.extending do
1100
1421
  # def page(number)
@@ -1181,6 +1502,8 @@ module ActiveRecord
1181
1502
  # # SELECT "users"."name" FROM "users" /* selecting */ /* user */ /* names */
1182
1503
  #
1183
1504
  # The SQL block comment delimiters, "/*" and "*/", will be added automatically.
1505
+ #
1506
+ # Some escaping is performed, however untrusted user input should not be used.
1184
1507
  def annotate(*args)
1185
1508
  check_if_method_has_arguments!(__callee__, args)
1186
1509
  spawn.annotate!(*args)
@@ -1209,6 +1532,9 @@ module ActiveRecord
1209
1532
  # Post.excluding(post_one, post_two)
1210
1533
  # # SELECT "posts".* FROM "posts" WHERE "posts"."id" NOT IN (1, 2)
1211
1534
  #
1535
+ # Post.excluding(Post.drafts)
1536
+ # # SELECT "posts".* FROM "posts" WHERE "posts"."id" NOT IN (3, 4, 5)
1537
+ #
1212
1538
  # This can also be called on associations. As with the above example, either
1213
1539
  # a single record of collection thereof may be specified:
1214
1540
  #
@@ -1224,14 +1550,15 @@ module ActiveRecord
1224
1550
  # is passed in) are not instances of the same model that the relation is
1225
1551
  # scoping.
1226
1552
  def excluding(*records)
1553
+ relations = records.extract! { |element| element.is_a?(Relation) }
1227
1554
  records.flatten!(1)
1228
1555
  records.compact!
1229
1556
 
1230
- unless records.all?(klass)
1557
+ unless records.all?(klass) && relations.all? { |relation| relation.klass == klass }
1231
1558
  raise ArgumentError, "You must only pass a single or collection of #{klass.name} objects to ##{__callee__}."
1232
1559
  end
1233
1560
 
1234
- spawn.excluding!(records)
1561
+ spawn.excluding!(records + relations.flat_map(&:ids))
1235
1562
  end
1236
1563
  alias :without :excluding
1237
1564
 
@@ -1243,7 +1570,7 @@ module ActiveRecord
1243
1570
 
1244
1571
  # Returns the Arel object associated with the relation.
1245
1572
  def arel(aliases = nil) # :nodoc:
1246
- @arel ||= build_arel(aliases)
1573
+ @arel ||= with_connection { |c| build_arel(c, aliases) }
1247
1574
  end
1248
1575
 
1249
1576
  def construct_join_dependency(associations, join_type) # :nodoc:
@@ -1264,13 +1591,29 @@ module ActiveRecord
1264
1591
  def build_where_clause(opts, rest = []) # :nodoc:
1265
1592
  opts = sanitize_forbidden_attributes(opts)
1266
1593
 
1594
+ if opts.is_a?(Array)
1595
+ opts, *rest = opts
1596
+ end
1597
+
1267
1598
  case opts
1268
- when String, Array
1269
- parts = [klass.sanitize_sql(rest.empty? ? opts : [opts, *rest])]
1599
+ when String
1600
+ if rest.empty?
1601
+ parts = [Arel.sql(opts)]
1602
+ elsif rest.first.is_a?(Hash) && /:\w+/.match?(opts)
1603
+ parts = [build_named_bound_sql_literal(opts, rest.first)]
1604
+ elsif opts.include?("?")
1605
+ parts = [build_bound_sql_literal(opts, rest)]
1606
+ else
1607
+ parts = [klass.sanitize_sql(rest.empty? ? opts : [opts, *rest])]
1608
+ end
1270
1609
  when Hash
1271
1610
  opts = opts.transform_keys do |key|
1272
- key = key.to_s
1273
- klass.attribute_aliases[key] || key
1611
+ if key.is_a?(Array)
1612
+ key.map { |k| klass.attribute_aliases[k.to_s] || k.to_s }
1613
+ else
1614
+ key = key.to_s
1615
+ klass.attribute_aliases[key] || key
1616
+ end
1274
1617
  end
1275
1618
  references = PredicateBuilder.references(opts)
1276
1619
  self.references_values |= references unless references.empty?
@@ -1288,7 +1631,56 @@ module ActiveRecord
1288
1631
  end
1289
1632
  alias :build_having_clause :build_where_clause
1290
1633
 
1634
+ def async!
1635
+ @async = true
1636
+ self
1637
+ end
1638
+
1291
1639
  private
1640
+ def async
1641
+ spawn.async!
1642
+ end
1643
+
1644
+ def build_named_bound_sql_literal(statement, values)
1645
+ bound_values = values.transform_values do |value|
1646
+ if ActiveRecord::Relation === value
1647
+ Arel.sql(value.to_sql)
1648
+ elsif value.respond_to?(:map) && !value.acts_like?(:string)
1649
+ values = value.map { |v| v.respond_to?(:id_for_database) ? v.id_for_database : v }
1650
+ values.empty? ? nil : values
1651
+ else
1652
+ value = value.id_for_database if value.respond_to?(:id_for_database)
1653
+ value
1654
+ end
1655
+ end
1656
+
1657
+ begin
1658
+ Arel::Nodes::BoundSqlLiteral.new("(#{statement})", nil, bound_values)
1659
+ rescue Arel::BindError => error
1660
+ raise ActiveRecord::PreparedStatementInvalid, error.message
1661
+ end
1662
+ end
1663
+
1664
+ def build_bound_sql_literal(statement, values)
1665
+ bound_values = values.map do |value|
1666
+ if ActiveRecord::Relation === value
1667
+ Arel.sql(value.to_sql)
1668
+ elsif value.respond_to?(:map) && !value.acts_like?(:string)
1669
+ values = value.map { |v| v.respond_to?(:id_for_database) ? v.id_for_database : v }
1670
+ values.empty? ? nil : values
1671
+ else
1672
+ value = value.id_for_database if value.respond_to?(:id_for_database)
1673
+ value
1674
+ end
1675
+ end
1676
+
1677
+ begin
1678
+ Arel::Nodes::BoundSqlLiteral.new("(#{statement})", bound_values, nil)
1679
+ rescue Arel::BindError => error
1680
+ raise ActiveRecord::PreparedStatementInvalid, error.message
1681
+ end
1682
+ end
1683
+
1292
1684
  def lookup_table_klass_from_join_dependencies(table_name)
1293
1685
  each_join_dependencies do |join|
1294
1686
  return join.base_klass if table_name == join.table_name
@@ -1303,22 +1695,21 @@ module ActiveRecord
1303
1695
  end
1304
1696
 
1305
1697
  def build_join_dependencies
1306
- associations = joins_values | left_outer_joins_values
1307
- associations |= eager_load_values unless eager_load_values.empty?
1308
- associations |= includes_values unless includes_values.empty?
1698
+ joins = joins_values | left_outer_joins_values
1699
+ joins |= eager_load_values unless eager_load_values.empty?
1700
+ joins |= includes_values unless includes_values.empty?
1309
1701
 
1310
1702
  join_dependencies = []
1311
1703
  join_dependencies.unshift construct_join_dependency(
1312
- select_association_list(associations, join_dependencies), nil
1704
+ select_named_joins(joins, join_dependencies), nil
1313
1705
  )
1314
1706
  end
1315
1707
 
1316
- def assert_mutability!
1317
- raise ImmutableRelation if @loaded
1318
- raise ImmutableRelation if defined?(@arel) && @arel
1708
+ def assert_modifiable!
1709
+ raise UnmodifiableRelation if @loaded || @arel
1319
1710
  end
1320
1711
 
1321
- def build_arel(aliases = nil)
1712
+ def build_arel(connection, aliases = nil)
1322
1713
  arel = Arel::SelectManager.new(table)
1323
1714
 
1324
1715
  build_joins(arel.join_sources, aliases)
@@ -1330,6 +1721,7 @@ module ActiveRecord
1330
1721
  arel.group(*arel_columns(group_values.uniq)) unless group_values.empty?
1331
1722
 
1332
1723
  build_order(arel)
1724
+ build_with(arel)
1333
1725
  build_select(arel)
1334
1726
 
1335
1727
  arel.optimizer_hints(*optimizer_hints_values) unless optimizer_hints_values.empty?
@@ -1365,6 +1757,18 @@ module ActiveRecord
1365
1757
  end
1366
1758
  end
1367
1759
 
1760
+ def select_named_joins(join_names, stashed_joins = nil, &block)
1761
+ cte_joins, associations = join_names.partition do |join_name|
1762
+ Symbol === join_name && with_values.any? { _1.key?(join_name) }
1763
+ end
1764
+
1765
+ cte_joins.each do |cte_name|
1766
+ block&.call(CTEJoin.new(cte_name))
1767
+ end
1768
+
1769
+ select_association_list(associations, stashed_joins, &block)
1770
+ end
1771
+
1368
1772
  def select_association_list(associations, stashed_joins = nil)
1369
1773
  result = []
1370
1774
  associations.each do |association|
@@ -1380,20 +1784,21 @@ module ActiveRecord
1380
1784
  result
1381
1785
  end
1382
1786
 
1383
- class ::Arel::Nodes::LeadingJoin < Arel::Nodes::InnerJoin # :nodoc:
1384
- end
1385
-
1386
1787
  def build_join_buckets
1387
1788
  buckets = Hash.new { |h, k| h[k] = [] }
1388
1789
 
1389
1790
  unless left_outer_joins_values.empty?
1390
1791
  stashed_left_joins = []
1391
- left_joins = select_association_list(left_outer_joins_values, stashed_left_joins) do
1392
- raise ArgumentError, "only Hash, Symbol and Array are allowed"
1792
+ left_joins = select_named_joins(left_outer_joins_values, stashed_left_joins) do |left_join|
1793
+ if left_join.is_a?(CTEJoin)
1794
+ buckets[:join_node] << build_with_join_node(left_join.name, Arel::Nodes::OuterJoin)
1795
+ else
1796
+ raise ArgumentError, "only Hash, Symbol and Array are allowed"
1797
+ end
1393
1798
  end
1394
1799
 
1395
1800
  if joins_values.empty?
1396
- buckets[:association_join] = left_joins
1801
+ buckets[:named_join] = left_joins
1397
1802
  buckets[:stashed_join] = stashed_left_joins
1398
1803
  return buckets, Arel::Nodes::OuterJoin
1399
1804
  else
@@ -1419,9 +1824,11 @@ module ActiveRecord
1419
1824
  end
1420
1825
  end
1421
1826
 
1422
- buckets[:association_join] = select_association_list(joins, buckets[:stashed_join]) do |join|
1827
+ buckets[:named_join] = select_named_joins(joins, buckets[:stashed_join]) do |join|
1423
1828
  if join.is_a?(Arel::Nodes::Join)
1424
1829
  buckets[:join_node] << join
1830
+ elsif join.is_a?(CTEJoin)
1831
+ buckets[:join_node] << build_with_join_node(join.name)
1425
1832
  else
1426
1833
  raise "unknown class: %s" % join.class.name
1427
1834
  end
@@ -1438,16 +1845,16 @@ module ActiveRecord
1438
1845
 
1439
1846
  buckets, join_type = build_join_buckets
1440
1847
 
1441
- association_joins = buckets[:association_join]
1442
- stashed_joins = buckets[:stashed_join]
1443
- leading_joins = buckets[:leading_join]
1444
- join_nodes = buckets[:join_node]
1848
+ named_joins = buckets[:named_join]
1849
+ stashed_joins = buckets[:stashed_join]
1850
+ leading_joins = buckets[:leading_join]
1851
+ join_nodes = buckets[:join_node]
1445
1852
 
1446
1853
  join_sources.concat(leading_joins) unless leading_joins.empty?
1447
1854
 
1448
- unless association_joins.empty? && stashed_joins.empty?
1855
+ unless named_joins.empty? && stashed_joins.empty?
1449
1856
  alias_tracker = alias_tracker(leading_joins + join_nodes, aliases)
1450
- join_dependency = construct_join_dependency(association_joins, join_type)
1857
+ join_dependency = construct_join_dependency(named_joins, join_type)
1451
1858
  join_sources.concat(join_dependency.join_constraints(stashed_joins, alias_tracker, references_values))
1452
1859
  end
1453
1860
 
@@ -1465,17 +1872,56 @@ module ActiveRecord
1465
1872
  end
1466
1873
  end
1467
1874
 
1875
+ def build_with(arel)
1876
+ return if with_values.empty?
1877
+
1878
+ with_statements = with_values.map do |with_value|
1879
+ raise ArgumentError, "Unsupported argument type: #{with_value} #{with_value.class}" unless with_value.is_a?(Hash)
1880
+
1881
+ build_with_value_from_hash(with_value)
1882
+ end
1883
+
1884
+ @with_is_recursive ? arel.with(:recursive, with_statements) : arel.with(with_statements)
1885
+ end
1886
+
1887
+ def build_with_value_from_hash(hash)
1888
+ hash.map do |name, value|
1889
+ Arel::Nodes::TableAlias.new(build_with_expression_from_value(value), name)
1890
+ end
1891
+ end
1892
+
1893
+ def build_with_expression_from_value(value)
1894
+ case value
1895
+ when Arel::Nodes::SqlLiteral then Arel::Nodes::Grouping.new(value)
1896
+ when ActiveRecord::Relation then value.arel
1897
+ when Arel::SelectManager then value
1898
+ when Array then value.map { |q| build_with_expression_from_value(q) }.reduce { |result, value| result.union(:all, value) }
1899
+ else
1900
+ raise ArgumentError, "Unsupported argument type: `#{value}` #{value.class}"
1901
+ end
1902
+ end
1903
+
1904
+ def build_with_join_node(name, kind = Arel::Nodes::InnerJoin)
1905
+ with_table = Arel::Table.new(name)
1906
+
1907
+ table.join(with_table, kind).on(
1908
+ with_table[klass.model_name.to_s.foreign_key].eq(table[klass.primary_key])
1909
+ ).join_sources.first
1910
+ end
1911
+
1468
1912
  def arel_columns(columns)
1469
1913
  columns.flat_map do |field|
1470
1914
  case field
1471
1915
  when Symbol
1472
1916
  arel_column(field.to_s) do |attr_name|
1473
- connection.quote_table_name(attr_name)
1917
+ adapter_class.quote_table_name(attr_name)
1474
1918
  end
1475
1919
  when String
1476
1920
  arel_column(field, &:itself)
1477
1921
  when Proc
1478
1922
  field.call
1923
+ when Hash
1924
+ arel_columns_from_hash(field)
1479
1925
  else
1480
1926
  field
1481
1927
  end
@@ -1500,7 +1946,7 @@ module ActiveRecord
1500
1946
 
1501
1947
  def table_name_matches?(from)
1502
1948
  table_name = Regexp.escape(table.name)
1503
- quoted_table_name = Regexp.escape(connection.quote_table_name(table.name))
1949
+ quoted_table_name = Regexp.escape(adapter_class.quote_table_name(table.name))
1504
1950
  /(?:\A|(?<!FROM)\s)(?:\b#{table_name}\b|#{quoted_table_name})(?!\.)/i.match?(from.to_s)
1505
1951
  end
1506
1952
 
@@ -1556,7 +2002,9 @@ module ActiveRecord
1556
2002
  args.each do |arg|
1557
2003
  next unless arg.is_a?(Hash)
1558
2004
  arg.each do |_key, value|
1559
- unless VALID_DIRECTIONS.include?(value)
2005
+ if value.is_a?(Hash)
2006
+ validate_order_args([value])
2007
+ elsif VALID_DIRECTIONS.exclude?(value)
1560
2008
  raise ArgumentError,
1561
2009
  "Direction \"#{value}\" is invalid. Valid directions are: #{VALID_DIRECTIONS.to_a.inspect}"
1562
2010
  end
@@ -1564,10 +2012,14 @@ module ActiveRecord
1564
2012
  end
1565
2013
  end
1566
2014
 
2015
+ def flattened_args(args)
2016
+ args.flat_map { |e| (e.is_a?(Hash) || e.is_a?(Array)) ? flattened_args(e.to_a) : e }
2017
+ end
2018
+
1567
2019
  def preprocess_order_args(order_args)
1568
2020
  @klass.disallow_raw_sql!(
1569
- order_args.flat_map { |a| a.is_a?(Hash) ? a.keys : a },
1570
- permit: connection.column_name_with_order_matcher
2021
+ flattened_args(order_args),
2022
+ permit: model.adapter_class.column_name_with_order_matcher
1571
2023
  )
1572
2024
 
1573
2025
  validate_order_args(order_args)
@@ -1581,14 +2033,20 @@ module ActiveRecord
1581
2033
  when Symbol
1582
2034
  order_column(arg.to_s).asc
1583
2035
  when Hash
1584
- arg.map { |field, dir|
1585
- case field
1586
- when Arel::Nodes::SqlLiteral
1587
- field.public_send(dir.downcase)
2036
+ arg.map do |key, value|
2037
+ if value.is_a?(Hash)
2038
+ value.map do |field, dir|
2039
+ order_column([key.to_s, field.to_s].join(".")).public_send(dir.downcase)
2040
+ end
1588
2041
  else
1589
- order_column(field.to_s).public_send(dir.downcase)
2042
+ case key
2043
+ when Arel::Nodes::SqlLiteral, Arel::Nodes::Node, Arel::Attribute
2044
+ key.public_send(value.downcase)
2045
+ else
2046
+ order_column(key.to_s).public_send(value.downcase)
2047
+ end
1590
2048
  end
1591
- }
2049
+ end
1592
2050
  else
1593
2051
  arg
1594
2052
  end
@@ -1602,16 +2060,32 @@ module ActiveRecord
1602
2060
  end
1603
2061
 
1604
2062
  def column_references(order_args)
1605
- references = order_args.flat_map do |arg|
2063
+ order_args.flat_map do |arg|
1606
2064
  case arg
1607
2065
  when String, Symbol
1608
- arg
2066
+ extract_table_name_from(arg)
1609
2067
  when Hash
1610
- arg.keys
2068
+ arg
2069
+ .map do |key, value|
2070
+ case value
2071
+ when Hash
2072
+ key.to_s
2073
+ else
2074
+ extract_table_name_from(key) if key.is_a?(String) || key.is_a?(Symbol)
2075
+ end
2076
+ end
2077
+ when Arel::Attribute
2078
+ arg.relation.name
2079
+ when Arel::Nodes::Ordering
2080
+ if arg.expr.is_a?(Arel::Attribute)
2081
+ arg.expr.relation.name
2082
+ end
1611
2083
  end
1612
- end
1613
- references.map! { |arg| arg =~ /^\W?(\w+)\W?\./ && $1 }.compact!
1614
- references
2084
+ end.compact
2085
+ end
2086
+
2087
+ def extract_table_name_from(string)
2088
+ string.match(/^\W?(\w+)\W?\./) && $1
1615
2089
  end
1616
2090
 
1617
2091
  def order_column(field)
@@ -1619,11 +2093,20 @@ module ActiveRecord
1619
2093
  if attr_name == "count" && !group_values.empty?
1620
2094
  table[attr_name]
1621
2095
  else
1622
- Arel.sql(connection.quote_table_name(attr_name))
2096
+ Arel.sql(adapter_class.quote_table_name(attr_name), retryable: true)
1623
2097
  end
1624
2098
  end
1625
2099
  end
1626
2100
 
2101
+ def build_case_for_value_position(column, values)
2102
+ node = Arel::Nodes::Case.new
2103
+ values.each.with_index(1) do |value, order|
2104
+ node.when(column.eq(value)).then(order)
2105
+ end
2106
+
2107
+ Arel::Nodes::Ascending.new(node)
2108
+ end
2109
+
1627
2110
  def resolve_arel_attributes(attrs)
1628
2111
  attrs.flat_map do |attr|
1629
2112
  case attr
@@ -1675,6 +2158,41 @@ module ActiveRecord
1675
2158
  end
1676
2159
  end
1677
2160
 
2161
+ def process_select_args(fields)
2162
+ fields.flat_map do |field|
2163
+ if field.is_a?(Hash)
2164
+ arel_columns_from_hash(field)
2165
+ else
2166
+ field
2167
+ end
2168
+ end
2169
+ end
2170
+
2171
+ def arel_columns_from_hash(fields)
2172
+ fields.flat_map do |key, columns_aliases|
2173
+ case columns_aliases
2174
+ when Hash
2175
+ columns_aliases.map do |column, column_alias|
2176
+ if values[:joins]&.include?(key)
2177
+ references = PredicateBuilder.references({ key.to_s => fields[key] })
2178
+ self.references_values |= references unless references.empty?
2179
+ end
2180
+ arel_column("#{key}.#{column}") do
2181
+ predicate_builder.resolve_arel_attribute(key.to_s, column)
2182
+ end.as(column_alias.to_s)
2183
+ end
2184
+ when Array
2185
+ columns_aliases.map do |column|
2186
+ arel_column("#{key}.#{column}", &:itself)
2187
+ end
2188
+ when String, Symbol
2189
+ arel_column(key.to_s) do
2190
+ predicate_builder.resolve_arel_attribute(klass.table_name, key.to_s)
2191
+ end.as(columns_aliases.to_s)
2192
+ end
2193
+ end
2194
+ end
2195
+
1678
2196
  STRUCTURAL_VALUE_METHODS = (
1679
2197
  Relation::VALUE_METHODS -
1680
2198
  [:extending, :where, :having, :unscope, :references, :annotate, :optimizer_hints]