activerecord 7.0.5 → 7.1.3.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (234) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1624 -1338
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +18 -18
  5. data/lib/active_record/aggregations.rb +16 -13
  6. data/lib/active_record/association_relation.rb +1 -1
  7. data/lib/active_record/associations/association.rb +20 -4
  8. data/lib/active_record/associations/association_scope.rb +16 -9
  9. data/lib/active_record/associations/belongs_to_association.rb +14 -6
  10. data/lib/active_record/associations/builder/association.rb +3 -3
  11. data/lib/active_record/associations/builder/belongs_to.rb +21 -8
  12. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +1 -5
  13. data/lib/active_record/associations/builder/singular_association.rb +4 -0
  14. data/lib/active_record/associations/collection_association.rb +16 -10
  15. data/lib/active_record/associations/collection_proxy.rb +20 -10
  16. data/lib/active_record/associations/foreign_association.rb +10 -3
  17. data/lib/active_record/associations/has_many_association.rb +20 -13
  18. data/lib/active_record/associations/has_many_through_association.rb +10 -6
  19. data/lib/active_record/associations/has_one_association.rb +10 -7
  20. data/lib/active_record/associations/join_dependency.rb +10 -8
  21. data/lib/active_record/associations/preloader/association.rb +31 -7
  22. data/lib/active_record/associations/preloader.rb +13 -10
  23. data/lib/active_record/associations/singular_association.rb +6 -8
  24. data/lib/active_record/associations/through_association.rb +22 -11
  25. data/lib/active_record/associations.rb +313 -217
  26. data/lib/active_record/attribute_assignment.rb +0 -2
  27. data/lib/active_record/attribute_methods/before_type_cast.rb +17 -0
  28. data/lib/active_record/attribute_methods/dirty.rb +52 -34
  29. data/lib/active_record/attribute_methods/primary_key.rb +76 -24
  30. data/lib/active_record/attribute_methods/query.rb +28 -16
  31. data/lib/active_record/attribute_methods/read.rb +18 -5
  32. data/lib/active_record/attribute_methods/serialization.rb +150 -31
  33. data/lib/active_record/attribute_methods/write.rb +3 -3
  34. data/lib/active_record/attribute_methods.rb +105 -21
  35. data/lib/active_record/attributes.rb +3 -3
  36. data/lib/active_record/autosave_association.rb +55 -9
  37. data/lib/active_record/base.rb +7 -2
  38. data/lib/active_record/callbacks.rb +10 -24
  39. data/lib/active_record/coders/column_serializer.rb +61 -0
  40. data/lib/active_record/coders/json.rb +1 -1
  41. data/lib/active_record/coders/yaml_column.rb +70 -42
  42. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +163 -88
  43. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +2 -0
  44. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +3 -1
  45. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +74 -51
  46. data/lib/active_record/connection_adapters/abstract/database_limits.rb +5 -0
  47. data/lib/active_record/connection_adapters/abstract/database_statements.rb +129 -31
  48. data/lib/active_record/connection_adapters/abstract/query_cache.rb +62 -23
  49. data/lib/active_record/connection_adapters/abstract/quoting.rb +41 -6
  50. data/lib/active_record/connection_adapters/abstract/savepoints.rb +4 -3
  51. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +18 -4
  52. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +137 -11
  53. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +290 -125
  54. data/lib/active_record/connection_adapters/abstract/transaction.rb +287 -58
  55. data/lib/active_record/connection_adapters/abstract_adapter.rb +505 -102
  56. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +214 -113
  57. data/lib/active_record/connection_adapters/column.rb +9 -0
  58. data/lib/active_record/connection_adapters/mysql/column.rb +1 -0
  59. data/lib/active_record/connection_adapters/mysql/database_statements.rb +23 -144
  60. data/lib/active_record/connection_adapters/mysql/quoting.rb +21 -14
  61. data/lib/active_record/connection_adapters/mysql/schema_creation.rb +9 -0
  62. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +6 -0
  63. data/lib/active_record/connection_adapters/mysql/schema_dumper.rb +1 -1
  64. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +18 -13
  65. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +151 -0
  66. data/lib/active_record/connection_adapters/mysql2_adapter.rb +98 -53
  67. data/lib/active_record/connection_adapters/pool_config.rb +14 -5
  68. data/lib/active_record/connection_adapters/pool_manager.rb +19 -9
  69. data/lib/active_record/connection_adapters/postgresql/column.rb +14 -3
  70. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +75 -41
  71. data/lib/active_record/connection_adapters/postgresql/oid/money.rb +3 -2
  72. data/lib/active_record/connection_adapters/postgresql/oid/range.rb +11 -2
  73. data/lib/active_record/connection_adapters/postgresql/oid/timestamp_with_time_zone.rb +2 -2
  74. data/lib/active_record/connection_adapters/postgresql/quoting.rb +15 -8
  75. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +3 -9
  76. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +76 -6
  77. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +131 -2
  78. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +53 -0
  79. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +361 -60
  80. data/lib/active_record/connection_adapters/postgresql_adapter.rb +353 -192
  81. data/lib/active_record/connection_adapters/schema_cache.rb +287 -59
  82. data/lib/active_record/connection_adapters/sqlite3/column.rb +49 -0
  83. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +52 -39
  84. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +9 -5
  85. data/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb +7 -0
  86. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +28 -9
  87. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +211 -83
  88. data/lib/active_record/connection_adapters/statement_pool.rb +7 -0
  89. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +99 -0
  90. data/lib/active_record/connection_adapters/trilogy_adapter.rb +262 -0
  91. data/lib/active_record/connection_adapters.rb +3 -1
  92. data/lib/active_record/connection_handling.rb +72 -95
  93. data/lib/active_record/core.rb +175 -153
  94. data/lib/active_record/counter_cache.rb +46 -25
  95. data/lib/active_record/database_configurations/database_config.rb +9 -3
  96. data/lib/active_record/database_configurations/hash_config.rb +22 -12
  97. data/lib/active_record/database_configurations/url_config.rb +17 -11
  98. data/lib/active_record/database_configurations.rb +86 -33
  99. data/lib/active_record/delegated_type.rb +9 -4
  100. data/lib/active_record/deprecator.rb +7 -0
  101. data/lib/active_record/destroy_association_async_job.rb +2 -0
  102. data/lib/active_record/encryption/auto_filtered_parameters.rb +66 -0
  103. data/lib/active_record/encryption/cipher/aes256_gcm.rb +4 -1
  104. data/lib/active_record/encryption/config.rb +25 -1
  105. data/lib/active_record/encryption/configurable.rb +12 -19
  106. data/lib/active_record/encryption/context.rb +10 -3
  107. data/lib/active_record/encryption/contexts.rb +5 -1
  108. data/lib/active_record/encryption/derived_secret_key_provider.rb +8 -2
  109. data/lib/active_record/encryption/encryptable_record.rb +42 -18
  110. data/lib/active_record/encryption/encrypted_attribute_type.rb +21 -6
  111. data/lib/active_record/encryption/extended_deterministic_queries.rb +66 -69
  112. data/lib/active_record/encryption/extended_deterministic_uniqueness_validator.rb +3 -3
  113. data/lib/active_record/encryption/key_generator.rb +12 -1
  114. data/lib/active_record/encryption/message_serializer.rb +2 -0
  115. data/lib/active_record/encryption/properties.rb +3 -3
  116. data/lib/active_record/encryption/scheme.rb +19 -22
  117. data/lib/active_record/encryption.rb +1 -0
  118. data/lib/active_record/enum.rb +112 -28
  119. data/lib/active_record/errors.rb +112 -18
  120. data/lib/active_record/explain.rb +23 -3
  121. data/lib/active_record/fixture_set/model_metadata.rb +14 -4
  122. data/lib/active_record/fixture_set/render_context.rb +2 -0
  123. data/lib/active_record/fixture_set/table_row.rb +29 -8
  124. data/lib/active_record/fixtures.rb +135 -71
  125. data/lib/active_record/future_result.rb +31 -5
  126. data/lib/active_record/gem_version.rb +4 -4
  127. data/lib/active_record/inheritance.rb +30 -16
  128. data/lib/active_record/insert_all.rb +57 -10
  129. data/lib/active_record/integration.rb +8 -8
  130. data/lib/active_record/internal_metadata.rb +120 -30
  131. data/lib/active_record/locking/optimistic.rb +32 -18
  132. data/lib/active_record/locking/pessimistic.rb +5 -2
  133. data/lib/active_record/log_subscriber.rb +29 -12
  134. data/lib/active_record/marshalling.rb +56 -0
  135. data/lib/active_record/message_pack.rb +124 -0
  136. data/lib/active_record/middleware/database_selector/resolver.rb +4 -0
  137. data/lib/active_record/middleware/database_selector.rb +6 -8
  138. data/lib/active_record/middleware/shard_selector.rb +3 -1
  139. data/lib/active_record/migration/command_recorder.rb +104 -5
  140. data/lib/active_record/migration/compatibility.rb +150 -58
  141. data/lib/active_record/migration/default_strategy.rb +23 -0
  142. data/lib/active_record/migration/execution_strategy.rb +19 -0
  143. data/lib/active_record/migration/pending_migration_connection.rb +21 -0
  144. data/lib/active_record/migration.rb +271 -114
  145. data/lib/active_record/model_schema.rb +64 -44
  146. data/lib/active_record/nested_attributes.rb +24 -6
  147. data/lib/active_record/normalization.rb +167 -0
  148. data/lib/active_record/persistence.rb +195 -42
  149. data/lib/active_record/promise.rb +84 -0
  150. data/lib/active_record/query_cache.rb +3 -21
  151. data/lib/active_record/query_logs.rb +77 -52
  152. data/lib/active_record/query_logs_formatter.rb +41 -0
  153. data/lib/active_record/querying.rb +15 -2
  154. data/lib/active_record/railtie.rb +109 -47
  155. data/lib/active_record/railties/controller_runtime.rb +14 -9
  156. data/lib/active_record/railties/databases.rake +142 -148
  157. data/lib/active_record/railties/job_runtime.rb +23 -0
  158. data/lib/active_record/readonly_attributes.rb +32 -5
  159. data/lib/active_record/reflection.rb +182 -44
  160. data/lib/active_record/relation/batches/batch_enumerator.rb +5 -3
  161. data/lib/active_record/relation/batches.rb +190 -61
  162. data/lib/active_record/relation/calculations.rb +187 -63
  163. data/lib/active_record/relation/delegation.rb +23 -9
  164. data/lib/active_record/relation/finder_methods.rb +77 -16
  165. data/lib/active_record/relation/merger.rb +2 -0
  166. data/lib/active_record/relation/predicate_builder/association_query_value.rb +31 -3
  167. data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +4 -6
  168. data/lib/active_record/relation/predicate_builder/relation_handler.rb +5 -1
  169. data/lib/active_record/relation/predicate_builder.rb +27 -16
  170. data/lib/active_record/relation/query_attribute.rb +25 -1
  171. data/lib/active_record/relation/query_methods.rb +386 -70
  172. data/lib/active_record/relation/spawn_methods.rb +18 -1
  173. data/lib/active_record/relation.rb +91 -35
  174. data/lib/active_record/result.rb +25 -9
  175. data/lib/active_record/runtime_registry.rb +24 -1
  176. data/lib/active_record/sanitization.rb +51 -11
  177. data/lib/active_record/schema.rb +2 -3
  178. data/lib/active_record/schema_dumper.rb +46 -7
  179. data/lib/active_record/schema_migration.rb +68 -33
  180. data/lib/active_record/scoping/default.rb +15 -5
  181. data/lib/active_record/scoping/named.rb +2 -2
  182. data/lib/active_record/scoping.rb +2 -1
  183. data/lib/active_record/secure_password.rb +60 -0
  184. data/lib/active_record/secure_token.rb +21 -3
  185. data/lib/active_record/signed_id.rb +7 -5
  186. data/lib/active_record/store.rb +8 -8
  187. data/lib/active_record/suppressor.rb +3 -1
  188. data/lib/active_record/table_metadata.rb +16 -3
  189. data/lib/active_record/tasks/database_tasks.rb +127 -105
  190. data/lib/active_record/tasks/mysql_database_tasks.rb +15 -6
  191. data/lib/active_record/tasks/postgresql_database_tasks.rb +16 -13
  192. data/lib/active_record/tasks/sqlite_database_tasks.rb +15 -7
  193. data/lib/active_record/test_fixtures.rb +113 -96
  194. data/lib/active_record/timestamp.rb +27 -15
  195. data/lib/active_record/token_for.rb +113 -0
  196. data/lib/active_record/touch_later.rb +11 -6
  197. data/lib/active_record/transactions.rb +39 -13
  198. data/lib/active_record/type/adapter_specific_registry.rb +1 -8
  199. data/lib/active_record/type/internal/timezone.rb +7 -2
  200. data/lib/active_record/type/serialized.rb +4 -0
  201. data/lib/active_record/type/time.rb +4 -0
  202. data/lib/active_record/validations/absence.rb +1 -1
  203. data/lib/active_record/validations/numericality.rb +5 -4
  204. data/lib/active_record/validations/presence.rb +5 -28
  205. data/lib/active_record/validations/uniqueness.rb +47 -2
  206. data/lib/active_record/validations.rb +8 -4
  207. data/lib/active_record/version.rb +1 -1
  208. data/lib/active_record.rb +121 -16
  209. data/lib/arel/errors.rb +10 -0
  210. data/lib/arel/factory_methods.rb +4 -0
  211. data/lib/arel/nodes/and.rb +4 -0
  212. data/lib/arel/nodes/binary.rb +6 -1
  213. data/lib/arel/nodes/bound_sql_literal.rb +61 -0
  214. data/lib/arel/nodes/cte.rb +36 -0
  215. data/lib/arel/nodes/fragments.rb +35 -0
  216. data/lib/arel/nodes/homogeneous_in.rb +1 -9
  217. data/lib/arel/nodes/leading_join.rb +8 -0
  218. data/lib/arel/nodes/node.rb +111 -2
  219. data/lib/arel/nodes/sql_literal.rb +6 -0
  220. data/lib/arel/nodes/table_alias.rb +4 -0
  221. data/lib/arel/nodes.rb +4 -0
  222. data/lib/arel/predications.rb +2 -0
  223. data/lib/arel/table.rb +9 -5
  224. data/lib/arel/visitors/mysql.rb +8 -1
  225. data/lib/arel/visitors/to_sql.rb +81 -17
  226. data/lib/arel/visitors/visitor.rb +2 -2
  227. data/lib/arel.rb +16 -2
  228. data/lib/rails/generators/active_record/application_record/USAGE +8 -0
  229. data/lib/rails/generators/active_record/migration.rb +3 -1
  230. data/lib/rails/generators/active_record/model/USAGE +113 -0
  231. data/lib/rails/generators/active_record/model/model_generator.rb +15 -6
  232. metadata +51 -15
  233. data/lib/active_record/connection_adapters/legacy_pool_manager.rb +0 -35
  234. data/lib/active_record/null_relation.rb +0 -63
@@ -3,14 +3,13 @@
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.
12
+ # +WhereChain+ objects act as placeholder for queries in which +where+ does not have any parameter.
14
13
  # In this case, +where+ can be chained to return a new relation.
15
14
  class WhereChain
16
15
  def initialize(scope) # :nodoc:
@@ -39,7 +38,7 @@ 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')
43
42
  #
44
43
  # If there is a non-nil condition on a nullable column in the hash condition, the records that have
45
44
  # nil values on the nullable column won't be returned.
@@ -77,7 +76,11 @@ module ActiveRecord
77
76
  associations.each do |association|
78
77
  reflection = scope_association_reflection(association)
79
78
  @scope.joins!(association)
80
- self.not(association => { reflection.association_primary_key => nil })
79
+ if reflection.options[:class_name]
80
+ self.not(association => { reflection.association_primary_key => nil })
81
+ else
82
+ self.not(reflection.table_name => { reflection.association_primary_key => nil })
83
+ end
81
84
  end
82
85
 
83
86
  @scope
@@ -105,7 +108,11 @@ module ActiveRecord
105
108
  associations.each do |association|
106
109
  reflection = scope_association_reflection(association)
107
110
  @scope.left_outer_joins!(association)
108
- @scope.where!(association => { reflection.association_primary_key => nil })
111
+ if reflection.options[:class_name]
112
+ @scope.where!(association => { reflection.association_primary_key => nil })
113
+ else
114
+ @scope.where!(reflection.table_name => { reflection.association_primary_key => nil })
115
+ end
109
116
  end
110
117
 
111
118
  @scope
@@ -121,6 +128,15 @@ module ActiveRecord
121
128
  end
122
129
  end
123
130
 
131
+ # A wrapper to distinguish CTE joins from other nodes.
132
+ class CTEJoin # :nodoc:
133
+ attr_reader :name
134
+
135
+ def initialize(name)
136
+ @name = name
137
+ end
138
+ end
139
+
124
140
  FROZEN_EMPTY_ARRAY = [].freeze
125
141
  FROZEN_EMPTY_HASH = {}.freeze
126
142
 
@@ -149,45 +165,69 @@ module ActiveRecord
149
165
 
150
166
  alias extensions extending_values
151
167
 
152
- # Specify relationships to be included in the result set. For
153
- # example:
168
+ # Specify associations +args+ to be eager loaded to prevent N + 1 queries.
169
+ # A separate query is performed for each association, unless a join is
170
+ # required by conditions.
154
171
  #
155
- # users = User.includes(:address)
172
+ # For example:
173
+ #
174
+ # users = User.includes(:address).limit(5)
156
175
  # users.each do |user|
157
176
  # user.address.city
158
177
  # end
159
178
  #
160
- # allows you to access the +address+ attribute of the +User+ model without
161
- # firing an additional query. This will often result in a
162
- # performance improvement over a simple join.
179
+ # # SELECT "users".* FROM "users" LIMIT 5
180
+ # # SELECT "addresses".* FROM "addresses" WHERE "addresses"."id" IN (1,2,3,4,5)
181
+ #
182
+ # Instead of loading the 5 addresses with 5 separate queries, all addresses
183
+ # are loaded with a single query.
184
+ #
185
+ # Loading the associations in a separate query will often result in a
186
+ # performance improvement over a simple join, as a join can result in many
187
+ # rows that contain redundant data and it performs poorly at scale.
163
188
  #
164
- # You can also specify multiple relationships, like this:
189
+ # You can also specify multiple associations. Each association will result
190
+ # in an additional query:
165
191
  #
166
- # users = User.includes(:address, :friends)
192
+ # User.includes(:address, :friends).to_a
193
+ # # SELECT "users".* FROM "users"
194
+ # # SELECT "addresses".* FROM "addresses" WHERE "addresses"."id" IN (1,2,3,4,5)
195
+ # # SELECT "friends".* FROM "friends" WHERE "friends"."user_id" IN (1,2,3,4,5)
167
196
  #
168
- # Loading nested relationships is possible using a Hash:
197
+ # Loading nested associations is possible using a Hash:
169
198
  #
170
- # users = User.includes(:address, friends: [:address, :followers])
199
+ # User.includes(:address, friends: [:address, :followers])
171
200
  #
172
201
  # === Conditions
173
202
  #
174
203
  # If you want to add string conditions to your included models, you'll have
175
204
  # to explicitly reference them. For example:
176
205
  #
177
- # User.includes(:posts).where('posts.name = ?', 'example')
206
+ # User.includes(:posts).where('posts.name = ?', 'example').to_a
178
207
  #
179
208
  # Will throw an error, but this will work:
180
209
  #
181
- # User.includes(:posts).where('posts.name = ?', 'example').references(:posts)
210
+ # User.includes(:posts).where('posts.name = ?', 'example').references(:posts).to_a
211
+ # # SELECT "users"."id" AS t0_r0, ... FROM "users"
212
+ # # LEFT OUTER JOIN "posts" ON "posts"."user_id" = "users"."id"
213
+ # # WHERE "posts"."name" = ? [["name", "example"]]
214
+ #
215
+ # As the <tt>LEFT OUTER JOIN</tt> already contains the posts, the second query for
216
+ # the posts is no longer performed.
182
217
  #
183
218
  # Note that #includes works with association names while #references needs
184
219
  # the actual table name.
185
220
  #
186
- # If you pass the conditions via hash, you don't need to call #references
221
+ # If you pass the conditions via a Hash, you don't need to call #references
187
222
  # explicitly, as #where references the tables for you. For example, this
188
223
  # will work correctly:
189
224
  #
190
225
  # User.includes(:posts).where(posts: { name: 'example' })
226
+ #
227
+ # NOTE: Conditions affect both sides of an association. For example, the
228
+ # above code will return only users that have a post named "example",
229
+ # <em>and will only include posts named "example"</em>, even when a
230
+ # matching user has other additional posts.
191
231
  def includes(*args)
192
232
  check_if_method_has_arguments!(__callee__, args)
193
233
  spawn.includes!(*args)
@@ -198,12 +238,32 @@ module ActiveRecord
198
238
  self
199
239
  end
200
240
 
201
- # Forces eager loading by performing a LEFT OUTER JOIN on +args+:
241
+ # Specify associations +args+ to be eager loaded using a <tt>LEFT OUTER JOIN</tt>.
242
+ # Performs a single query joining all specified associations. For example:
243
+ #
244
+ # users = User.eager_load(:address).limit(5)
245
+ # users.each do |user|
246
+ # user.address.city
247
+ # end
248
+ #
249
+ # # SELECT "users"."id" AS t0_r0, "users"."name" AS t0_r1, ... FROM "users"
250
+ # # LEFT OUTER JOIN "addresses" ON "addresses"."id" = "users"."address_id"
251
+ # # LIMIT 5
202
252
  #
203
- # User.eager_load(:posts)
204
- # # SELECT "users"."id" AS t0_r0, "users"."name" AS t0_r1, ...
205
- # # FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" =
206
- # # "users"."id"
253
+ # Instead of loading the 5 addresses with 5 separate queries, all addresses
254
+ # are loaded with a single joined query.
255
+ #
256
+ # Loading multiple and nested associations is possible using Hashes and Arrays,
257
+ # similar to #includes:
258
+ #
259
+ # User.eager_load(:address, friends: [:address, :followers])
260
+ # # SELECT "users"."id" AS t0_r0, "users"."name" AS t0_r1, ... FROM "users"
261
+ # # LEFT OUTER JOIN "addresses" ON "addresses"."id" = "users"."address_id"
262
+ # # LEFT OUTER JOIN "friends" ON "friends"."user_id" = "users"."id"
263
+ # # ...
264
+ #
265
+ # NOTE: Loading the associations in a join can result in many rows that
266
+ # contain redundant data and it performs poorly at scale.
207
267
  def eager_load(*args)
208
268
  check_if_method_has_arguments!(__callee__, args)
209
269
  spawn.eager_load!(*args)
@@ -214,10 +274,28 @@ module ActiveRecord
214
274
  self
215
275
  end
216
276
 
217
- # Allows preloading of +args+, in the same way that #includes does:
277
+ # Specify associations +args+ to be eager loaded using separate queries.
278
+ # A separate query is performed for each association.
279
+ #
280
+ # users = User.preload(:address).limit(5)
281
+ # users.each do |user|
282
+ # user.address.city
283
+ # end
284
+ #
285
+ # # SELECT "users".* FROM "users" LIMIT 5
286
+ # # SELECT "addresses".* FROM "addresses" WHERE "addresses"."id" IN (1,2,3,4,5)
218
287
  #
219
- # User.preload(:posts)
220
- # # SELECT "posts".* FROM "posts" WHERE "posts"."user_id" IN (1, 2, 3)
288
+ # Instead of loading the 5 addresses with 5 separate queries, all addresses
289
+ # are loaded with a separate query.
290
+ #
291
+ # Loading multiple and nested associations is possible using Hashes and Arrays,
292
+ # similar to #includes:
293
+ #
294
+ # User.preload(:address, friends: [:address, :followers])
295
+ # # SELECT "users".* FROM "users"
296
+ # # SELECT "addresses".* FROM "addresses" WHERE "addresses"."id" IN (1,2,3,4,5)
297
+ # # SELECT "friends".* FROM "friends" WHERE "friends"."user_id" IN (1,2,3,4,5)
298
+ # # SELECT ...
221
299
  def preload(*args)
222
300
  check_if_method_has_arguments!(__callee__, args)
223
301
  spawn.preload!(*args)
@@ -242,7 +320,7 @@ module ActiveRecord
242
320
  end
243
321
 
244
322
  # Use to indicate that the given +table_names+ are referenced by an SQL string,
245
- # and should therefore be JOINed in any query rather than loaded separately.
323
+ # and should therefore be +JOIN+ed in any query rather than loaded separately.
246
324
  # This method only works in conjunction with #includes.
247
325
  # See #includes for more details.
248
326
  #
@@ -286,6 +364,14 @@ module ActiveRecord
286
364
  # Model.select(:field, :other_field, :and_one_more)
287
365
  # # => [#<Model id: nil, field: "value", other_field: "value", and_one_more: "value">]
288
366
  #
367
+ # The argument also can be a hash of fields and aliases.
368
+ #
369
+ # Model.select(models: { field: :alias, other_field: :other_alias })
370
+ # # => [#<Model id: nil, alias: "value", other_alias: "value">]
371
+ #
372
+ # Model.select(models: [:field, :other_field])
373
+ # # => [#<Model id: nil, field: "value", other_field: "value">]
374
+ #
289
375
  # You can also use one or more strings, which will be used unchanged as SELECT fields.
290
376
  #
291
377
  # Model.select('field AS field_one', 'other_field AS field_two')
@@ -300,7 +386,7 @@ module ActiveRecord
300
386
  # except +id+ will throw ActiveModel::MissingAttributeError:
301
387
  #
302
388
  # Model.select(:field).first.other_field
303
- # # => ActiveModel::MissingAttributeError: missing attribute: other_field
389
+ # # => ActiveModel::MissingAttributeError: missing attribute 'other_field' for Model
304
390
  def select(*fields)
305
391
  if block_given?
306
392
  if fields.any?
@@ -311,6 +397,8 @@ module ActiveRecord
311
397
  end
312
398
 
313
399
  check_if_method_has_arguments!(__callee__, fields, "Call `select' with at least one field.")
400
+
401
+ fields = process_select_args(fields)
314
402
  spawn._select!(*fields)
315
403
  end
316
404
 
@@ -319,6 +407,66 @@ module ActiveRecord
319
407
  self
320
408
  end
321
409
 
410
+ # Add a Common Table Expression (CTE) that you can then reference within another SELECT statement.
411
+ #
412
+ # Note: CTE's are only supported in MySQL for versions 8.0 and above. You will not be able to
413
+ # use CTE's with MySQL 5.7.
414
+ #
415
+ # Post.with(posts_with_tags: Post.where("tags_count > ?", 0))
416
+ # # => ActiveRecord::Relation
417
+ # # WITH posts_with_tags AS (
418
+ # # SELECT * FROM posts WHERE (tags_count > 0)
419
+ # # )
420
+ # # SELECT * FROM posts
421
+ #
422
+ # Once you define Common Table Expression you can use custom +FROM+ value or +JOIN+ to reference it.
423
+ #
424
+ # Post.with(posts_with_tags: Post.where("tags_count > ?", 0)).from("posts_with_tags AS posts")
425
+ # # => ActiveRecord::Relation
426
+ # # WITH posts_with_tags AS (
427
+ # # SELECT * FROM posts WHERE (tags_count > 0)
428
+ # # )
429
+ # # SELECT * FROM posts_with_tags AS posts
430
+ #
431
+ # Post.with(posts_with_tags: Post.where("tags_count > ?", 0)).joins("JOIN posts_with_tags ON posts_with_tags.id = posts.id")
432
+ # # => ActiveRecord::Relation
433
+ # # WITH posts_with_tags AS (
434
+ # # SELECT * FROM posts WHERE (tags_count > 0)
435
+ # # )
436
+ # # SELECT * FROM posts JOIN posts_with_tags ON posts_with_tags.id = posts.id
437
+ #
438
+ # It is recommended to pass a query as ActiveRecord::Relation. If that is not possible
439
+ # and you have verified it is safe for the database, you can pass it as SQL literal
440
+ # using +Arel+.
441
+ #
442
+ # Post.with(popular_posts: Arel.sql("... complex sql to calculate posts popularity ..."))
443
+ #
444
+ # Great caution should be taken to avoid SQL injection vulnerabilities. This method should not
445
+ # be used with unsafe values that include unsanitized input.
446
+ #
447
+ # To add multiple CTEs just pass multiple key-value pairs
448
+ #
449
+ # Post.with(
450
+ # posts_with_comments: Post.where("comments_count > ?", 0),
451
+ # posts_with_tags: Post.where("tags_count > ?", 0)
452
+ # )
453
+ #
454
+ # or chain multiple +.with+ calls
455
+ #
456
+ # Post
457
+ # .with(posts_with_comments: Post.where("comments_count > ?", 0))
458
+ # .with(posts_with_tags: Post.where("tags_count > ?", 0))
459
+ def with(*args)
460
+ check_if_method_has_arguments!(__callee__, args)
461
+ spawn.with!(*args)
462
+ end
463
+
464
+ # Like #with, but modifies relation in place.
465
+ def with!(*args) # :nodoc:
466
+ self.with_values += args
467
+ self
468
+ end
469
+
322
470
  # Allows you to change a previously set select statement.
323
471
  #
324
472
  # Post.select(:title, :body)
@@ -331,6 +479,7 @@ module ActiveRecord
331
479
  # Note that we're unscoping the entire select statement.
332
480
  def reselect(*args)
333
481
  check_if_method_has_arguments!(__callee__, args)
482
+ args = process_select_args(args)
334
483
  spawn.reselect!(*args)
335
484
  end
336
485
 
@@ -370,6 +519,27 @@ module ActiveRecord
370
519
  self
371
520
  end
372
521
 
522
+ # Allows you to change a previously set group statement.
523
+ #
524
+ # Post.group(:title, :body)
525
+ # # SELECT `posts`.`*` FROM `posts` GROUP BY `posts`.`title`, `posts`.`body`
526
+ #
527
+ # Post.group(:title, :body).regroup(:title)
528
+ # # SELECT `posts`.`*` FROM `posts` GROUP BY `posts`.`title`
529
+ #
530
+ # This is short-hand for <tt>unscope(:group).group(fields)</tt>.
531
+ # Note that we're unscoping the entire group statement.
532
+ def regroup(*args)
533
+ check_if_method_has_arguments!(__callee__, args)
534
+ spawn.regroup!(*args)
535
+ end
536
+
537
+ # Same as #regroup but operates on relation in-place instead of copying.
538
+ def regroup!(*args) # :nodoc:
539
+ self.group_values = args
540
+ self
541
+ end
542
+
373
543
  # Applies an <code>ORDER BY</code> clause to a query.
374
544
  #
375
545
  # #order accepts arguments in one of several formats.
@@ -418,7 +588,7 @@ module ActiveRecord
418
588
  # User.order(Arel.sql('end_date - start_date'))
419
589
  # # SELECT "users".* FROM "users" ORDER BY end_date - start_date
420
590
  #
421
- # Custom query syntax, like JSON columns for Postgres, is supported in this way.
591
+ # Custom query syntax, like JSON columns for PostgreSQL, is supported in this way.
422
592
  #
423
593
  # User.order(Arel.sql("payload->>'kind'"))
424
594
  # # SELECT "users".* FROM "users" ORDER BY payload->>'kind'
@@ -436,13 +606,16 @@ module ActiveRecord
436
606
  self
437
607
  end
438
608
 
439
- # Allows to specify an order by a specific set of values. Depending on your
440
- # adapter this will either use a CASE statement or a built-in function.
609
+ # Allows to specify an order by a specific set of values.
441
610
  #
442
611
  # User.in_order_of(:id, [1, 5, 3])
443
612
  # # SELECT "users".* FROM "users"
444
- # # ORDER BY FIELD("users"."id", 1, 5, 3)
445
613
  # # WHERE "users"."id" IN (1, 5, 3)
614
+ # # ORDER BY CASE
615
+ # # WHEN "users"."id" = 1 THEN 1
616
+ # # WHEN "users"."id" = 5 THEN 2
617
+ # # WHEN "users"."id" = 3 THEN 3
618
+ # # END ASC
446
619
  #
447
620
  def in_order_of(column, values)
448
621
  klass.disallow_raw_sql!([column], permit: connection.column_name_with_order_matcher)
@@ -452,11 +625,18 @@ module ActiveRecord
452
625
  self.references_values |= references unless references.empty?
453
626
 
454
627
  values = values.map { |value| type_caster.type_cast_for_database(column, value) }
455
- arel_column = column.is_a?(Symbol) ? order_column(column.to_s) : column
628
+ arel_column = column.is_a?(Arel::Nodes::SqlLiteral) ? column : order_column(column.to_s)
629
+
630
+ where_clause =
631
+ if values.include?(nil)
632
+ arel_column.in(values.compact).or(arel_column.eq(nil))
633
+ else
634
+ arel_column.in(values)
635
+ end
456
636
 
457
637
  spawn
458
- .order!(connection.field_ordered_value(arel_column, values))
459
- .where!(arel_column.in(values))
638
+ .order!(build_case_for_value_position(arel_column, values))
639
+ .where!(where_clause)
460
640
  end
461
641
 
462
642
  # Replaces any existing order defined on the relation with the specified order.
@@ -467,7 +647,7 @@ module ActiveRecord
467
647
  #
468
648
  # User.order('email DESC').reorder('id ASC').order('name ASC')
469
649
  #
470
- # generates a query with 'ORDER BY id ASC, name ASC'.
650
+ # generates a query with <tt>ORDER BY id ASC, name ASC</tt>.
471
651
  def reorder(*args)
472
652
  check_if_method_has_arguments!(__callee__, args) do
473
653
  sanitize_order_arguments(args)
@@ -486,7 +666,8 @@ module ActiveRecord
486
666
 
487
667
  VALID_UNSCOPING_VALUES = Set.new([:where, :select, :group, :order, :lock,
488
668
  :limit, :offset, :joins, :left_outer_joins, :annotate,
489
- :includes, :from, :readonly, :having, :optimizer_hints])
669
+ :includes, :eager_load, :preload, :from, :readonly,
670
+ :having, :optimizer_hints])
490
671
 
491
672
  # Removes an unwanted relation that is already defined on a chain of relations.
492
673
  # This is useful when passing around chains of relations and would like to
@@ -596,7 +777,7 @@ module ActiveRecord
596
777
  # Performs LEFT OUTER JOINs on +args+:
597
778
  #
598
779
  # User.left_outer_joins(:posts)
599
- # => SELECT "users".* FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" = "users"."id"
780
+ # # SELECT "users".* FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" = "users"."id"
600
781
  #
601
782
  def left_outer_joins(*args)
602
783
  check_if_method_has_arguments!(__callee__, args)
@@ -616,7 +797,7 @@ module ActiveRecord
616
797
  # SQL is given as an illustration; the actual query generated may be different depending
617
798
  # on the database adapter.
618
799
  #
619
- # === string
800
+ # === \String
620
801
  #
621
802
  # A single string, without additional arguments, is passed to the query
622
803
  # constructor as an SQL fragment, and used in the where clause of the query.
@@ -628,7 +809,7 @@ module ActiveRecord
628
809
  # to injection attacks if not done properly. As an alternative, it is recommended
629
810
  # to use one of the following methods.
630
811
  #
631
- # === array
812
+ # === \Array
632
813
  #
633
814
  # If an array is passed, then the first element of the array is treated as a template, and
634
815
  # the remaining elements are inserted into the template to generate the condition.
@@ -668,7 +849,7 @@ module ActiveRecord
668
849
  # dependencies on the underlying database. If your code is intended for general consumption,
669
850
  # test with multiple database backends.
670
851
  #
671
- # === hash
852
+ # === \Hash
672
853
  #
673
854
  # #where will also accept a hash condition, in which the keys are fields and the values
674
855
  # are values to be searched for.
@@ -702,6 +883,12 @@ module ActiveRecord
702
883
  # PriceEstimate.where(estimate_of: treasure)
703
884
  # PriceEstimate.where(estimate_of_type: 'Treasure', estimate_of_id: treasure)
704
885
  #
886
+ # Hash conditions may also be specified in a tuple-like syntax. Hash keys may be
887
+ # an array of columns with an array of tuples as values.
888
+ #
889
+ # Article.where([:author_id, :id] => [[15, 1], [15, 2]])
890
+ # # SELECT * FROM articles WHERE author_id = 15 AND id = 1 OR author_id = 15 AND id = 2
891
+ #
705
892
  # === Joins
706
893
  #
707
894
  # If the relation is the result of a join, you may create a condition which uses any of the
@@ -714,7 +901,7 @@ module ActiveRecord
714
901
  # User.joins(:posts).where("posts.published" => true)
715
902
  # User.joins(:posts).where(posts: { published: true })
716
903
  #
717
- # === no argument
904
+ # === No Argument
718
905
  #
719
906
  # If no argument is passed, #where returns a new instance of WhereChain, that
720
907
  # can be chained with WhereChain#not, WhereChain#missing, or WhereChain#associated.
@@ -738,7 +925,7 @@ module ActiveRecord
738
925
  # # LEFT OUTER JOIN "authors" ON "authors"."id" = "posts"."author_id"
739
926
  # # WHERE "authors"."id" IS NULL
740
927
  #
741
- # === blank condition
928
+ # === Blank Condition
742
929
  #
743
930
  # If the condition is any blank-ish object, then #where is a no-op and returns
744
931
  # the current relation.
@@ -771,6 +958,8 @@ module ActiveRecord
771
958
  # This is short-hand for <tt>unscope(where: conditions.keys).where(conditions)</tt>.
772
959
  # Note that unlike reorder, we're only unscoping the named conditions -- not the entire where statement.
773
960
  def rewhere(conditions)
961
+ return unscope(:where) if conditions.nil?
962
+
774
963
  scope = spawn
775
964
  where_clause = scope.build_where_clause(conditions)
776
965
 
@@ -876,7 +1065,11 @@ module ActiveRecord
876
1065
  #
877
1066
  def or(other)
878
1067
  if other.is_a?(Relation)
879
- spawn.or!(other)
1068
+ if @none
1069
+ other.spawn
1070
+ else
1071
+ spawn.or!(other)
1072
+ end
880
1073
  else
881
1074
  raise ArgumentError, "You have passed #{other.class.name} object to #or. Pass an ActiveRecord::Relation object instead."
882
1075
  end
@@ -989,15 +1182,29 @@ module ActiveRecord
989
1182
  end
990
1183
 
991
1184
  def none! # :nodoc:
992
- where!("1=0").extending!(NullRelation)
1185
+ unless @none
1186
+ where!("1=0")
1187
+ @none = true
1188
+ end
1189
+ self
993
1190
  end
994
1191
 
995
- # Sets readonly attributes for the returned relation. If value is
996
- # true (default), attempting to update a record will result in an error.
1192
+ def null_relation? # :nodoc:
1193
+ @none
1194
+ end
1195
+
1196
+ # Mark a relation as readonly. Attempting to update a record will result in
1197
+ # an error.
997
1198
  #
998
1199
  # users = User.readonly
999
1200
  # users.first.save
1000
1201
  # => ActiveRecord::ReadOnlyRecord: User is marked as readonly
1202
+ #
1203
+ # To make a readonly relation writable, pass +false+.
1204
+ #
1205
+ # users.readonly(false)
1206
+ # users.first.save
1207
+ # => true
1001
1208
  def readonly(value = true)
1002
1209
  spawn.readonly!(value)
1003
1210
  end
@@ -1114,7 +1321,7 @@ module ActiveRecord
1114
1321
  #
1115
1322
  # The object returned is a relation, which can be further extended.
1116
1323
  #
1117
- # === Using a module
1324
+ # === Using a \Module
1118
1325
  #
1119
1326
  # module Pagination
1120
1327
  # def page(number)
@@ -1129,7 +1336,7 @@ module ActiveRecord
1129
1336
  #
1130
1337
  # scope = Model.all.extending(Pagination, SomethingElse)
1131
1338
  #
1132
- # === Using a block
1339
+ # === Using a Block
1133
1340
  #
1134
1341
  # scope = Model.all.extending do
1135
1342
  # def page(number)
@@ -1306,8 +1513,12 @@ module ActiveRecord
1306
1513
  parts = [klass.sanitize_sql(rest.empty? ? opts : [opts, *rest])]
1307
1514
  when Hash
1308
1515
  opts = opts.transform_keys do |key|
1309
- key = key.to_s
1310
- klass.attribute_aliases[key] || key
1516
+ if key.is_a?(Array)
1517
+ key.map { |k| klass.attribute_aliases[k.to_s] || k.to_s }
1518
+ else
1519
+ key = key.to_s
1520
+ klass.attribute_aliases[key] || key
1521
+ end
1311
1522
  end
1312
1523
  references = PredicateBuilder.references(opts)
1313
1524
  self.references_values |= references unless references.empty?
@@ -1325,7 +1536,16 @@ module ActiveRecord
1325
1536
  end
1326
1537
  alias :build_having_clause :build_where_clause
1327
1538
 
1539
+ def async!
1540
+ @async = true
1541
+ self
1542
+ end
1543
+
1328
1544
  private
1545
+ def async
1546
+ spawn.async!
1547
+ end
1548
+
1329
1549
  def lookup_table_klass_from_join_dependencies(table_name)
1330
1550
  each_join_dependencies do |join|
1331
1551
  return join.base_klass if table_name == join.table_name
@@ -1340,13 +1560,13 @@ module ActiveRecord
1340
1560
  end
1341
1561
 
1342
1562
  def build_join_dependencies
1343
- associations = joins_values | left_outer_joins_values
1344
- associations |= eager_load_values unless eager_load_values.empty?
1345
- associations |= includes_values unless includes_values.empty?
1563
+ joins = joins_values | left_outer_joins_values
1564
+ joins |= eager_load_values unless eager_load_values.empty?
1565
+ joins |= includes_values unless includes_values.empty?
1346
1566
 
1347
1567
  join_dependencies = []
1348
1568
  join_dependencies.unshift construct_join_dependency(
1349
- select_association_list(associations, join_dependencies), nil
1569
+ select_named_joins(joins, join_dependencies), nil
1350
1570
  )
1351
1571
  end
1352
1572
 
@@ -1367,6 +1587,7 @@ module ActiveRecord
1367
1587
  arel.group(*arel_columns(group_values.uniq)) unless group_values.empty?
1368
1588
 
1369
1589
  build_order(arel)
1590
+ build_with(arel)
1370
1591
  build_select(arel)
1371
1592
 
1372
1593
  arel.optimizer_hints(*optimizer_hints_values) unless optimizer_hints_values.empty?
@@ -1402,6 +1623,18 @@ module ActiveRecord
1402
1623
  end
1403
1624
  end
1404
1625
 
1626
+ def select_named_joins(join_names, stashed_joins = nil, &block)
1627
+ cte_joins, associations = join_names.partition do |join_name|
1628
+ Symbol === join_name && with_values.any? { _1.key?(join_name) }
1629
+ end
1630
+
1631
+ cte_joins.each do |cte_name|
1632
+ block&.call(CTEJoin.new(cte_name))
1633
+ end
1634
+
1635
+ select_association_list(associations, stashed_joins, &block)
1636
+ end
1637
+
1405
1638
  def select_association_list(associations, stashed_joins = nil)
1406
1639
  result = []
1407
1640
  associations.each do |association|
@@ -1417,20 +1650,21 @@ module ActiveRecord
1417
1650
  result
1418
1651
  end
1419
1652
 
1420
- class ::Arel::Nodes::LeadingJoin < Arel::Nodes::InnerJoin # :nodoc:
1421
- end
1422
-
1423
1653
  def build_join_buckets
1424
1654
  buckets = Hash.new { |h, k| h[k] = [] }
1425
1655
 
1426
1656
  unless left_outer_joins_values.empty?
1427
1657
  stashed_left_joins = []
1428
- left_joins = select_association_list(left_outer_joins_values, stashed_left_joins) do
1429
- raise ArgumentError, "only Hash, Symbol and Array are allowed"
1658
+ left_joins = select_named_joins(left_outer_joins_values, stashed_left_joins) do |left_join|
1659
+ if left_join.is_a?(CTEJoin)
1660
+ buckets[:join_node] << build_with_join_node(left_join.name, Arel::Nodes::OuterJoin)
1661
+ else
1662
+ raise ArgumentError, "only Hash, Symbol and Array are allowed"
1663
+ end
1430
1664
  end
1431
1665
 
1432
1666
  if joins_values.empty?
1433
- buckets[:association_join] = left_joins
1667
+ buckets[:named_join] = left_joins
1434
1668
  buckets[:stashed_join] = stashed_left_joins
1435
1669
  return buckets, Arel::Nodes::OuterJoin
1436
1670
  else
@@ -1456,9 +1690,11 @@ module ActiveRecord
1456
1690
  end
1457
1691
  end
1458
1692
 
1459
- buckets[:association_join] = select_association_list(joins, buckets[:stashed_join]) do |join|
1693
+ buckets[:named_join] = select_named_joins(joins, buckets[:stashed_join]) do |join|
1460
1694
  if join.is_a?(Arel::Nodes::Join)
1461
1695
  buckets[:join_node] << join
1696
+ elsif join.is_a?(CTEJoin)
1697
+ buckets[:join_node] << build_with_join_node(join.name)
1462
1698
  else
1463
1699
  raise "unknown class: %s" % join.class.name
1464
1700
  end
@@ -1475,16 +1711,16 @@ module ActiveRecord
1475
1711
 
1476
1712
  buckets, join_type = build_join_buckets
1477
1713
 
1478
- association_joins = buckets[:association_join]
1479
- stashed_joins = buckets[:stashed_join]
1480
- leading_joins = buckets[:leading_join]
1481
- join_nodes = buckets[:join_node]
1714
+ named_joins = buckets[:named_join]
1715
+ stashed_joins = buckets[:stashed_join]
1716
+ leading_joins = buckets[:leading_join]
1717
+ join_nodes = buckets[:join_node]
1482
1718
 
1483
1719
  join_sources.concat(leading_joins) unless leading_joins.empty?
1484
1720
 
1485
- unless association_joins.empty? && stashed_joins.empty?
1721
+ unless named_joins.empty? && stashed_joins.empty?
1486
1722
  alias_tracker = alias_tracker(leading_joins + join_nodes, aliases)
1487
- join_dependency = construct_join_dependency(association_joins, join_type)
1723
+ join_dependency = construct_join_dependency(named_joins, join_type)
1488
1724
  join_sources.concat(join_dependency.join_constraints(stashed_joins, alias_tracker, references_values))
1489
1725
  end
1490
1726
 
@@ -1502,6 +1738,40 @@ module ActiveRecord
1502
1738
  end
1503
1739
  end
1504
1740
 
1741
+ def build_with(arel)
1742
+ return if with_values.empty?
1743
+
1744
+ 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
+ build_with_value_from_hash(with_value)
1748
+ end
1749
+
1750
+ arel.with(with_statements)
1751
+ end
1752
+
1753
+ def build_with_value_from_hash(hash)
1754
+ 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)
1764
+ end
1765
+ end
1766
+
1767
+ def build_with_join_node(name, kind = Arel::Nodes::InnerJoin)
1768
+ with_table = Arel::Table.new(name)
1769
+
1770
+ table.join(with_table, kind).on(
1771
+ with_table[klass.model_name.to_s.foreign_key].eq(table[klass.primary_key])
1772
+ ).join_sources.first
1773
+ end
1774
+
1505
1775
  def arel_columns(columns)
1506
1776
  columns.flat_map do |field|
1507
1777
  case field
@@ -1620,7 +1890,7 @@ module ActiveRecord
1620
1890
  when Hash
1621
1891
  arg.map { |field, dir|
1622
1892
  case field
1623
- when Arel::Nodes::SqlLiteral
1893
+ when Arel::Nodes::SqlLiteral, Arel::Nodes::Node, Arel::Attribute
1624
1894
  field.public_send(dir.downcase)
1625
1895
  else
1626
1896
  order_column(field.to_s).public_send(dir.downcase)
@@ -1644,7 +1914,9 @@ module ActiveRecord
1644
1914
  when String, Symbol
1645
1915
  arg
1646
1916
  when Hash
1647
- arg.keys
1917
+ arg.keys.map do |key|
1918
+ key if key.is_a?(String) || key.is_a?(Symbol)
1919
+ end
1648
1920
  end
1649
1921
  end
1650
1922
  references.map! { |arg| arg =~ /^\W?(\w+)\W?\./ && $1 }.compact!
@@ -1661,6 +1933,15 @@ module ActiveRecord
1661
1933
  end
1662
1934
  end
1663
1935
 
1936
+ def build_case_for_value_position(column, values)
1937
+ node = Arel::Nodes::Case.new
1938
+ values.each.with_index(1) do |value, order|
1939
+ node.when(column.eq(value)).then(order)
1940
+ end
1941
+
1942
+ Arel::Nodes::Ascending.new(node)
1943
+ end
1944
+
1664
1945
  def resolve_arel_attributes(attrs)
1665
1946
  attrs.flat_map do |attr|
1666
1947
  case attr
@@ -1712,6 +1993,41 @@ module ActiveRecord
1712
1993
  end
1713
1994
  end
1714
1995
 
1996
+ def process_select_args(fields)
1997
+ fields.flat_map do |field|
1998
+ if field.is_a?(Hash)
1999
+ transform_select_hash_values(field)
2000
+ else
2001
+ field
2002
+ end
2003
+ end
2004
+ end
2005
+
2006
+ def transform_select_hash_values(fields)
2007
+ fields.flat_map do |key, columns_aliases|
2008
+ case columns_aliases
2009
+ when Hash
2010
+ columns_aliases.map do |column, column_alias|
2011
+ if values[:joins]&.include?(key)
2012
+ references = PredicateBuilder.references({ key.to_s => fields[key] })
2013
+ self.references_values |= references unless references.empty?
2014
+ end
2015
+ arel_column("#{key}.#{column}") do
2016
+ predicate_builder.resolve_arel_attribute(key.to_s, column)
2017
+ end.as(column_alias.to_s)
2018
+ end
2019
+ when Array
2020
+ columns_aliases.map do |column|
2021
+ arel_column("#{key}.#{column}", &:itself)
2022
+ end
2023
+ when String, Symbol
2024
+ arel_column(key.to_s) do
2025
+ predicate_builder.resolve_arel_attribute(klass.table_name, key.to_s)
2026
+ end.as(columns_aliases.to_s)
2027
+ end
2028
+ end
2029
+ end
2030
+
1715
2031
  STRUCTURAL_VALUE_METHODS = (
1716
2032
  Relation::VALUE_METHODS -
1717
2033
  [:extending, :where, :having, :unscope, :references, :annotate, :optimizer_hints]