activerecord-refined 0.9.0 → 0.10.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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +17 -0
  3. data/README.md +61 -1064
  4. data/activerecord-refined.gemspec +8 -7
  5. data/docs/conditions.md +206 -0
  6. data/docs/ctes.md +65 -0
  7. data/docs/expressions.md +125 -0
  8. data/docs/functions.md +219 -0
  9. data/docs/grouping.md +55 -0
  10. data/docs/index.md +70 -0
  11. data/docs/joins.md +73 -0
  12. data/docs/json.md +230 -0
  13. data/docs/ordering.md +55 -0
  14. data/docs/time_zones.md +30 -0
  15. data/docs/windows.md +41 -0
  16. data/docs/writing.md +33 -0
  17. data/examples/aggregations.rb +18 -0
  18. data/examples/expressions.rb +35 -5
  19. data/lib/active_record/refined/ast.rb +461 -246
  20. data/lib/active_record/refined/dialect/mariadb.rb +25 -0
  21. data/lib/active_record/refined/dialect/mysql.rb +18 -0
  22. data/lib/active_record/refined/dialect/mysql_compat.rb +67 -0
  23. data/lib/active_record/refined/dialect/oracle.rb +110 -0
  24. data/lib/active_record/refined/dialect/postgresql.rb +120 -0
  25. data/lib/active_record/refined/dialect/sql_server.rb +115 -0
  26. data/lib/active_record/refined/dialect/sqlite.rb +57 -0
  27. data/lib/active_record/refined/dialect.rb +340 -0
  28. data/lib/active_record/refined.rb +682 -192
  29. data/lib/activerecord-refined/version.rb +1 -1
  30. data/lib/activerecord-refined.rb +1 -0
  31. metadata +43 -16
  32. data/.github/workflows/push_gem.yml +0 -45
  33. data/.github/workflows/sandbox.yml +0 -295
  34. data/.github/workflows/test.yml +0 -104
  35. data/.gitignore +0 -19
  36. data/.rubocop.yml +0 -393
  37. data/Gemfile +0 -14
  38. data/Rakefile +0 -53
  39. data/benchmark/query_building.rb +0 -129
  40. data/test/test_block_syntax.rb +0 -2999
  41. data/test/test_helper.rb +0 -238
@@ -2,7 +2,72 @@
2
2
 
3
3
  module ActiveRecord
4
4
  module Refined
5
+ # What a symbol answers to inside a block. A symbol names a column
6
+ # there -- `:age` is `"users"."age"` -- and the methods below build a
7
+ # condition, an expression or an ordering from it. Every one of them is
8
+ # a refinement, so it exists inside a `where`, `select`, `having`,
9
+ # `order`, `group`, `joins`, `update_all` or `upsert_all` block and
10
+ # nowhere else.
11
+ #
12
+ # The comparisons and the rest of the conditions are listed under
13
+ # {AST::Predications}, the arithmetic under {AST::Arithmetics}; a number
14
+ # or a string in a block takes `as` too, for a literal in a select list
15
+ # -- `0.as(:depth)` -- and a number on the left of an operator builds the
16
+ # same expression a column on the left would.
17
+ #
18
+ # @example A column compared, aliased and ordered
19
+ # Author.where { :age >= 18 }
20
+ # Author.select { :name.as(:author) }
21
+ # Author.order { :age.desc.nulls_last }
22
+ # @example A column of another table, and a collation
23
+ # Author.joins(:posts) { :posts[:author_id] == :authors[:id] }
24
+ # Author.where { :name.collate(:nocase) == "alice" }
5
25
  module BlockSyntax
26
+ # @!parse include AST::Predications
27
+ # @!parse include AST::Arithmetics
28
+
29
+ # @!method as(alias_name, quote: true)
30
+ # The column under an alias: `AS "name"`. The alias is quoted, so
31
+ # the name asked for is the name that comes back on every adapter;
32
+ # `quote: false` writes it bare, for a schema that wants the folding,
33
+ # and then it has to be a plain name.
34
+ # @param alias_name [Symbol, String]
35
+ # @param quote [Boolean]
36
+ # @return [AST::As]
37
+ # @example
38
+ # Author.select { :name.as(:author) } # "authors"."name" AS "author"
39
+
40
+ # @!method asc
41
+ # An ascending ordering, which takes `nulls_first` and `nulls_last`.
42
+ # @return [AST::Ordering]
43
+ # @example
44
+ # Author.order { :country.asc.nulls_last }
45
+
46
+ # @!method desc
47
+ # A descending ordering, which takes `nulls_first` and `nulls_last`.
48
+ # @return [AST::Ordering]
49
+ # @example
50
+ # Post.order { :likes.desc }
51
+
52
+ # @!method collate(name)
53
+ # The column under a collation, for a comparison or an ordering:
54
+ # `"name" COLLATE nocase`. The name is the database's own and not
55
+ # portable; PostgreSQL quotes it, the others take it bare and refuse
56
+ # one that is not a plain identifier.
57
+ # @param name [Symbol, String] the collation's name
58
+ # @return [AST::Collate]
59
+ # @example
60
+ # Author.where { :name.collate(:nocase) == "alice" }
61
+ # Author.order { :name.collate(:"en-US-x-icu").asc } # PostgreSQL
62
+
63
+ # @!method [](column_name)
64
+ # A column of another table: `:posts[:author_id]` is
65
+ # `"posts"."author_id"`, for a join condition or a query over a join.
66
+ # @param column_name [Symbol]
67
+ # @return [AST::Column]
68
+ # @example
69
+ # Author.joins(:posts) { :posts[:author_id] == :authors[:id] }
70
+
6
71
  refine Symbol do
7
72
  import_methods AST::Predications
8
73
  import_methods AST::Arithmetics
@@ -19,6 +84,10 @@ module ActiveRecord
19
84
  AST::Ordering.new(self, :desc)
20
85
  end
21
86
 
87
+ def collate(name)
88
+ AST::Collate.new(self, name)
89
+ end
90
+
22
91
  def [](column_name)
23
92
  AST::Column.new(self, column_name)
24
93
  end
@@ -46,93 +115,308 @@ module ActiveRecord
46
115
  end
47
116
  end
48
117
 
118
+ # What a block can call: the aggregates, the functions, CASE, and the
119
+ # escape hatches. A block is evaluated with one of these as `self`, so
120
+ # its methods are called bare -- `count(:*)`, `upper(:name)` -- and each
121
+ # gives back an expression that compares, aliases and orders like a
122
+ # column does (see {BlockSyntax}).
123
+ #
124
+ # Where a function is spelled differently from one database to the next,
125
+ # the method names the one meaning and the adapter gets its own
126
+ # spelling; where a database has no equivalent, the method raises
127
+ # `NotImplementedError` as the block is read, rather than leaving the
128
+ # database to reject the SQL.
129
+ #
130
+ # @example
131
+ # Author.select { [upper(:name).as(:author), count(:*).as(:posts)] }
132
+ # Author.having { count(:*) > 1 }
49
133
  class BlockContext
50
134
  # The model is only consulted to learn which adapter the query is being
51
135
  # built for, which is what decides how a scalar function is spelled.
136
+ # @api private
52
137
  def initialize(model)
53
138
  @model = model
54
139
  end
55
140
 
141
+ # @!group Aggregates
142
+
143
+ # @!method sum(column, distinct: false)
144
+ # `SUM(column)`, or `SUM(DISTINCT column)`.
145
+ # @return [AST::Aggregate]
146
+ # @!method avg(column, distinct: false)
147
+ # `AVG(column)`, or `AVG(DISTINCT column)`.
148
+ # @return [AST::Aggregate]
149
+ # @!method min(column)
150
+ # `MIN(column)`.
151
+ # @return [AST::Aggregate]
152
+ # @!method max(column)
153
+ # `MAX(column)`.
154
+ # @return [AST::Aggregate]
155
+ # @private
56
156
  AGGREGATE_FUNCTIONS = {
57
157
  sum: :sum, avg: :average, min: :minimum, max: :maximum,
58
158
  }.freeze
59
159
 
160
+ # count, sum and avg take distinct: true, for the aggregate over each
161
+ # value once; min and max would answer the same with or without it,
162
+ # so they take no such thing.
60
163
  AGGREGATE_FUNCTIONS.each do |name, arel_func|
61
- define_method(name) { |column| AST::Aggregate.new(column, arel_func) }
164
+ if AST::Aggregate::DISTINCT_FUNCTIONS.include?(arel_func)
165
+ define_method(name) do |column, distinct: false|
166
+ AST::Aggregate.new(column, arel_func, distinct: distinct)
167
+ end
168
+ else
169
+ define_method(name) { |column| AST::Aggregate.new(column, arel_func) }
170
+ end
62
171
  end
63
172
 
173
+ # `COUNT(column)`; `:*` for `COUNT(*)`, `distinct: true` for
174
+ # `COUNT(DISTINCT column)`. Every aggregate takes {AST::Aggregate#filter}
175
+ # for the rows it is taken over, and {AST::Windowing#over} for a window.
176
+ # @param column [Symbol, AST::Node, :*]
177
+ # @return [AST::Aggregate]
178
+ # @example
179
+ # Author.group { :country }.having { count(:*) > 1 }
180
+ # Post.select { count(:author_id, distinct: true) }
181
+ # Author.select { count(:*).filter { :age < 50 }.as(:young) }
64
182
  def count(column, distinct: false)
65
183
  AST::Aggregate.new(column, :count, distinct: distinct)
66
184
  end
67
185
 
68
- # Rows gathered into one JSON document: json_arrayagg collects a value
69
- # from each row into an array, json_objectagg a key and a value into an
70
- # object.
186
+ # The rows of a group gathered into one JSON array, a value from each:
187
+ # `jsonb_agg` on PostgreSQL, `json_group_array` on SQLite,
188
+ # `JSON_ARRAYAGG` elsewhere. What it gives is JSON, which compares
189
+ # as a dug value does.
190
+ # @return [AST::JsonAggregate]
191
+ # @example
192
+ # Post.group { :author_id }.select { json_arrayagg(:title).as(:titles) }
71
193
  def json_arrayagg(value)
72
194
  AST::JsonAggregate.new(:arrayagg, [value])
73
195
  end
74
196
 
197
+ # The rows of a group gathered into one JSON object, a key and a value
198
+ # from each.
199
+ # @return [AST::JsonAggregate]
200
+ # @example
201
+ # Post.select { json_objectagg(:title, :meta.dig(:stars)).as(:stars) }
75
202
  def json_objectagg(key, value)
76
203
  AST::JsonAggregate.new(:objectagg, [key, value])
77
204
  end
78
205
 
79
- # A JSON document built in the row: json_array from the values given,
80
- # json_object from a Ruby hash whose values are expressions.
206
+ # The strings of a group joined into one, a separator between:
207
+ # `STRING_AGG` on PostgreSQL and SQL Server, `group_concat` on SQLite,
208
+ # `GROUP_CONCAT` on MySQL, `LISTAGG` on Oracle. Takes
209
+ # {AST::StringAggregate#order} for the order they are joined in.
210
+ # @param separator [String] the comma GROUP_CONCAT defaults to, unless given
211
+ # @return [AST::StringAggregate]
212
+ # @example
213
+ # Post.group { :author_id }.
214
+ # select { string_agg(:title, ", ").order(:title).as(:titles) }
215
+ def string_agg(value, separator = ",")
216
+ AST::StringAggregate.new(value, separator)
217
+ end
218
+
219
+ # @!endgroup
220
+ # @!group JSON
221
+
222
+ # A JSON array built in the row from the values given.
223
+ # @return [AST::JsonBuild]
224
+ # @example
225
+ # Post.select { json_array(:title, :likes).as(:pair) }
81
226
  def json_array(*values)
82
227
  AST::JsonBuild.new(:array, values)
83
228
  end
84
229
 
230
+ # A JSON object built in the row from a hash whose values are
231
+ # expressions.
232
+ # @param pairs [Hash{Symbol, String => Object}]
233
+ # @return [AST::JsonBuild]
234
+ # @example
235
+ # Post.select { json_object(title: :title, stars: :meta.dig(:stars)).as(:doc) }
85
236
  def json_object(pairs = {})
86
237
  AST::JsonBuild.new(:object, pairs)
87
238
  end
88
239
 
240
+ # @!endgroup
241
+ # @!group Scalar functions
242
+
243
+ # @!method abs(x)
244
+ # `ABS(x)`.
245
+ # @return [AST::Function]
246
+ # @!method acos(x)
247
+ # `ACOS(x)`.
248
+ # @return [AST::Function]
249
+ # @!method asin(x)
250
+ # `ASIN(x)`.
251
+ # @return [AST::Function]
252
+ # @!method atan(x)
253
+ # `ATAN(x)`.
254
+ # @return [AST::Function]
255
+ # @!method atan2(y, x)
256
+ # `ATAN2(y, x)`.
257
+ # @return [AST::Function]
258
+ # @!method ceil(x)
259
+ # `CEIL(x)`.
260
+ # @return [AST::Function]
261
+ # @!method coalesce(*values)
262
+ # `COALESCE(a, b, ...)`: the first that is not NULL.
263
+ # @return [AST::Function]
264
+ # @!method concat(*strings)
265
+ # `CONCAT(a, b, ...)`.
266
+ # @return [AST::Function]
267
+ # @!method cos(x)
268
+ # `COS(x)`.
269
+ # @return [AST::Function]
270
+ # @!method exp(x)
271
+ # `EXP(x)`.
272
+ # @return [AST::Function]
273
+ # @!method floor(x)
274
+ # `FLOOR(x)`.
275
+ # @return [AST::Function]
276
+ # @!method length(string)
277
+ # `LENGTH(string)`.
278
+ # @return [AST::Function]
279
+ # @!method ln(x)
280
+ # `LN(x)`.
281
+ # @return [AST::Function]
282
+ # @!method log(base, x)
283
+ # `LOG(base, x)`.
284
+ # @return [AST::Function]
285
+ # @!method lower(string)
286
+ # `LOWER(string)`.
287
+ # @return [AST::Function]
288
+ # @!method ltrim(string)
289
+ # `LTRIM(string)`.
290
+ # @return [AST::Function]
291
+ # @!method mod(x, y)
292
+ # `MOD(x, y)`.
293
+ # @return [AST::Function]
294
+ # @!method nullif(x, y)
295
+ # `NULLIF(x, y)`: NULL where the two are equal, x otherwise.
296
+ # @return [AST::Function]
297
+ # @!method power(x, y)
298
+ # `POWER(x, y)`.
299
+ # @return [AST::Function]
300
+ # @!method replace(string, from, to)
301
+ # `REPLACE(string, from, to)`.
302
+ # @return [AST::Function]
303
+ # @!method round(x, places = 0)
304
+ # `ROUND(x, places)`.
305
+ # @return [AST::Function]
306
+ # @!method rtrim(string)
307
+ # `RTRIM(string)`.
308
+ # @return [AST::Function]
309
+ # @!method sign(x)
310
+ # `SIGN(x)`.
311
+ # @return [AST::Function]
312
+ # @!method sin(x)
313
+ # `SIN(x)`.
314
+ # @return [AST::Function]
315
+ # @!method sqrt(x)
316
+ # `SQRT(x)`.
317
+ # @return [AST::Function]
318
+ # @!method substr(string, from, length = nil)
319
+ # `SUBSTR(string, from, length)`.
320
+ # @return [AST::Function]
321
+ # @!method tan(x)
322
+ # `TAN(x)`.
323
+ # @return [AST::Function]
324
+ # @!method trim(string)
325
+ # `TRIM(string)`.
326
+ # @return [AST::Function]
327
+ # @!method upper(string)
328
+ # `UPPER(string)`.
329
+ # @return [AST::Function]
330
+ # @!method degrees(x)
331
+ # `DEGREES(x)`. Oracle has none.
332
+ # @return [AST::Function]
333
+ # @!method radians(x)
334
+ # `RADIANS(x)`. Oracle has none.
335
+ # @return [AST::Function]
336
+ # @!method pi
337
+ # `PI()`. Oracle has none.
338
+ # @return [AST::Function]
339
+ # @!method char_length(string)
340
+ # `CHAR_LENGTH(string)`: `LENGTH` on SQLite and Oracle, `LEN` on SQL Server.
341
+ # @return [AST::Function]
342
+ # @!method greatest(*values)
343
+ # `GREATEST(a, b, ...)`: `MAX` on SQLite.
344
+ # @return [AST::Function]
345
+ # @!method least(*values)
346
+ # `LEAST(a, b, ...)`: `MIN` on SQLite.
347
+ # @return [AST::Function]
348
+ # @!method log2(x)
349
+ # `LOG2(x)`. PostgreSQL and Oracle have none; `log(2, x)` is their spelling.
350
+ # @return [AST::Function]
351
+ # @!method log10(x)
352
+ # `LOG10(x)`. Oracle has none.
353
+ # @return [AST::Function]
354
+ # @!method trunc(x, places = 0)
355
+ # `TRUNC(x, places)`: `TRUNCATE` on MySQL, which insists on the places.
356
+ # @return [AST::Function]
357
+ # @!method now
358
+ # `NOW()`. SQLite and Oracle have none; {#current_timestamp} reaches both.
359
+ # @return [AST::Function]
360
+ # @!method bit_and(column)
361
+ # `BIT_AND(column)`, an aggregate. PostgreSQL and MySQL have it.
362
+ # @return [AST::Function]
363
+ # @!method bit_or(column)
364
+ # `BIT_OR(column)`, an aggregate. PostgreSQL and MySQL have it.
365
+ # @return [AST::Function]
366
+ # @!method bit_xor(column)
367
+ # `BIT_XOR(column)`, an aggregate. PostgreSQL and MySQL have it.
368
+ # @return [AST::Function]
369
+ # @!method date_trunc(field, timestamp)
370
+ # `date_trunc('day', timestamp)`. PostgreSQL has it; the others do not.
371
+ # @return [AST::Function]
372
+ # @!method rand
373
+ # `RAND()`, a random number per row: `RANDOM()` on PostgreSQL and SQLite. Oracle and SQL Server have none.
374
+ # @return [AST::Function]
375
+ # @!method format(template, *values)
376
+ # printf-style `FORMAT(template, ...)`. PostgreSQL and SQLite have it; MySQL's FORMAT is a different function, reached through {#fn}.
377
+ # @return [AST::Function]
378
+ #
89
379
  # Scalar functions, defined as real methods so that a typo is a
90
380
  # NoMethodError and a name Kernel also answers to (format, hash, test)
91
- # cannot quietly mean something else.
92
- #
93
- # The value lists the adapters that differ: a string is what the
94
- # function is called there, nil says the adapter has no equivalent. An
95
- # adapter that is not listed spells it like the method. The families
96
- # are what the entries key on, so trilogy reads the mysql column.
97
- #
98
- # Availability was checked by calling each one; the SQLite figures
99
- # assume the math functions its build usually enables.
100
- SCALAR_FUNCTIONS = {
101
- abs: {}, acos: {}, asin: {}, atan: {}, atan2: {}, ceil: {},
102
- coalesce: {}, concat: {}, cos: {}, degrees: {}, exp: {}, floor: {},
103
- length: {}, ln: {}, log: {}, log10: {}, lower: {}, ltrim: {},
104
- mod: {}, nullif: {}, pi: {}, power: {}, radians: {}, replace: {},
105
- round: {}, rtrim: {}, sign: {}, sin: {}, sqrt: {}, substr: {},
106
- tan: {}, trim: {}, upper: {},
107
- char_length: { sqlite: "LENGTH" },
108
- greatest: { sqlite: "MAX" },
109
- least: { sqlite: "MIN" },
110
- # PostgreSQL spells log2(x) as log(2, x), which no renaming carries.
111
- log2: { postgresql: nil },
112
- # MySQL's TRUNCATE insists on the second argument, where the others
113
- # default it to zero; SQLite's trunc takes only the one.
114
- trunc: { mysql: "TRUNCATE" },
115
- now: { sqlite: nil },
116
- # The bit aggregates, which PostgreSQL and MySQL spell alike and
117
- # SQLite has none of. PostgreSQL gained bit_xor in 14.
118
- bit_and: { sqlite: nil }, bit_or: { sqlite: nil }, bit_xor: { sqlite: nil },
119
- date_trunc: { sqlite: nil, mysql: nil },
120
- # Named for Kernel#rand, which it also takes back: a block calling
121
- # rand would otherwise get Ruby's and never reach the database.
122
- rand: { sqlite: "RANDOM", postgresql: "RANDOM" },
123
- # Two different functions share this name: printf formatting here, and
124
- # on MySQL the one that puts separators in a number, which reads a
125
- # printf template as the number zero rather than complaining. The
126
- # name keeps the one meaning; fn(:format, ...) reaches MySQL's.
127
- format: { mysql: nil },
128
- }.freeze
129
-
130
- SCALAR_FUNCTIONS.each_key do |name|
381
+ # cannot quietly mean something else. Where one is spelled other than as
382
+ # its plain upper-cased name, and where a family has no equivalent, is
383
+ # the dialect's to say; here is only the list of them.
384
+ # @private
385
+ SCALAR_FUNCTIONS = %i[
386
+ abs acos asin atan atan2 ceil coalesce concat cos exp floor length ln
387
+ log lower ltrim mod nullif power replace round rtrim sign sin sqrt
388
+ substr tan trim upper degrees radians pi char_length greatest least
389
+ log2 log10 trunc now bit_and bit_or bit_xor date_trunc rand format
390
+ ].freeze
391
+
392
+ SCALAR_FUNCTIONS.each do |name|
131
393
  define_method(name) do |*args|
132
- AST::Function.new(function_name(name, SCALAR_FUNCTIONS), args)
394
+ AST::Function.new(dialect.function_name(name, @model), args)
133
395
  end
134
396
  end
135
397
 
398
+ # @!endgroup
399
+ # @!group Datetime value functions
400
+
401
+ # @!method current_timestamp(precision = nil)
402
+ # `CURRENT_TIMESTAMP`, the server's clock in the session's zone; the
403
+ # portable spelling of what {#now} means. A precision --
404
+ # `current_timestamp(3)` -- goes into parentheses, which SQLite and
405
+ # SQL Server refuse.
406
+ # @return [AST::DatetimeValueFunction]
407
+ # @example
408
+ # Post.where { :published_at <= current_timestamp }
409
+ # Post.where { :created_at > current_timestamp - 7.days }
410
+ # @!method current_time(precision = nil)
411
+ # `CURRENT_TIME`. SQL Server has none.
412
+ # @return [AST::DatetimeValueFunction]
413
+ # @!method localtime(precision = nil)
414
+ # `LOCALTIME`. SQLite and SQL Server have none.
415
+ # @return [AST::DatetimeValueFunction]
416
+ # @!method localtimestamp(precision = nil)
417
+ # `LOCALTIMESTAMP`. SQLite and SQL Server have none.
418
+ # @return [AST::DatetimeValueFunction]
419
+ #
136
420
  # The datetime value functions, as the SQL grammar calls them. These
137
421
  # the grammar has bare -- PostgreSQL and SQLite reject them written with
138
422
  # parentheses -- and the one thing that does go into parentheses is an
@@ -140,27 +424,28 @@ module ActiveRecord
140
424
  # takes and SQLite never accepts. The table reads like
141
425
  # SCALAR_FUNCTIONS; current_timestamp is the portable spelling of what
142
426
  # now means, reaching SQLite where now does not.
143
- DATETIME_VALUE_FUNCTIONS = {
144
- current_date: {},
145
- current_time: {},
146
- current_timestamp: {},
147
- localtime: { sqlite: nil },
148
- localtimestamp: { sqlite: nil },
149
- }.freeze
150
-
427
+ # @private
428
+ DATETIME_VALUE_FUNCTIONS = %i[
429
+ current_date current_time current_timestamp localtime localtimestamp
430
+ ].freeze
431
+
432
+ # `CURRENT_DATE`, today in the session's zone -- UTC where Active
433
+ # Record has set it so. Takes no precision. SQL Server has none.
434
+ # @return [AST::DatetimeValueFunction]
435
+ # @example
436
+ # Task.where { :due_on < current_date }
151
437
  def current_date
152
- AST::DatetimeValueFunction.new(
153
- function_name(:current_date, DATETIME_VALUE_FUNCTIONS))
438
+ AST::DatetimeValueFunction.new(dialect.function_name(:current_date, @model))
154
439
  end
155
440
 
156
- (DATETIME_VALUE_FUNCTIONS.keys - [:current_date]).each do |name|
441
+ (DATETIME_VALUE_FUNCTIONS - [:current_date]).each do |name|
157
442
  define_method(name) do |precision = nil|
158
443
  # Built first so that a precision of the wrong type is an
159
444
  # ArgumentError on every adapter, before SQLite gets to say it takes
160
445
  # none at all.
161
446
  node = AST::DatetimeValueFunction.new(
162
- function_name(name, DATETIME_VALUE_FUNCTIONS), precision)
163
- if precision && adapter_family == :sqlite
447
+ dialect.function_name(name, @model), precision)
448
+ if precision && !dialect.datetime_precision_supported?
164
449
  raise NotImplementedError,
165
450
  "#{name} takes no precision on #{@model.connection_db_config.adapter}"
166
451
  end
@@ -168,48 +453,109 @@ module ActiveRecord
168
453
  end
169
454
  end
170
455
 
171
- # EXTRACT(field FROM expr). The field is a keyword, not a value, so it
172
- # has to be a plain name; the node checks it. SQLite spells all of
173
- # this as strftime formats, which no renaming carries, so it raises
174
- # there -- after the node is built, so that a bad field is an
175
- # ArgumentError on every adapter.
456
+ # `EXTRACT(field FROM expr)`: a year, a month, a day of a date. The
457
+ # field is a keyword and has to be a plain name. SQLite and SQL Server
458
+ # have none.
459
+ # @param field [Symbol, String] `:year`, `:month`, `:day`, `:hour`, ...
460
+ # @return [AST::Extract]
461
+ # @example
462
+ # Post.where { extract(:year, :created_at) == 2026 }
463
+ #
464
+ # The field is a keyword, not a value, so it has to be a plain name;
465
+ # the node checks it. SQLite spells all of this as strftime formats,
466
+ # which no renaming carries, so it raises there -- after the node is
467
+ # built, so that a bad field is an ArgumentError on every adapter.
176
468
  def extract(field, expr)
177
469
  node = AST::Extract.new(field, expr)
178
- if adapter_family == :sqlite
470
+ unless dialect.extract_supported?
179
471
  raise NotImplementedError,
180
472
  "extract has no equivalent on #{@model.connection_db_config.adapter}"
181
473
  end
182
474
  node
183
475
  end
184
476
 
185
- # GROUP BY GROUPING SETS / ROLLUP / CUBE. PostgreSQL has all three;
186
- # the MySQL family has WITH ROLLUP, which says rollup and only rollup,
187
- # trailing the group list -- the node spells it there. Arel has the
188
- # nodes and writes them for PostgreSQL alone, so what it would raise
189
- # elsewhere says nothing; this says it here, as extract does, while
190
- # the block is being read.
191
- #
477
+ # @!endgroup
478
+ # @!group Grouping
479
+
480
+ # `GROUP BY GROUPING SETS ((a), (b), ())`: several groupings in one
481
+ # query, an empty set for the grand total. PostgreSQL has it; the
482
+ # others do not.
483
+ # @param sets [Array<Array<Symbol, AST::Node>>]
484
+ # @return [AST::GroupingSets]
485
+ # @example
192
486
  # Sale.group { grouping_sets([:region], [:product], []) }
193
- # Sale.group { rollup(:region, :product) }
487
+ #
488
+ # Arel has the nodes and writes them for PostgreSQL alone, so what it
489
+ # would raise elsewhere says nothing; this says it here, as extract
490
+ # does, while the block is being read.
194
491
  def grouping_sets(*sets)
195
492
  grouping(:grouping_sets, sets)
196
493
  end
197
494
 
495
+ # `GROUP BY ROLLUP (a, b)`: subtotals up the list and a grand total.
496
+ # PostgreSQL has it, and the MySQL family as `WITH ROLLUP` trailing
497
+ # the group list, which the node spells there.
498
+ # @return [AST::GroupingSets]
499
+ # @example
500
+ # Sale.group { rollup(:region, :product) }
198
501
  def rollup(*columns)
199
502
  grouping(:rollup, columns)
200
503
  end
201
504
 
505
+ # `GROUP BY CUBE (a, b)`: every subtotal there is. PostgreSQL has it;
506
+ # the others do not.
507
+ # @return [AST::GroupingSets]
508
+ # @example
509
+ # Sale.group { cube(:region, :product) }
202
510
  def cube(*columns)
203
511
  grouping(:cube, columns)
204
512
  end
205
513
 
206
- # CAST(expr AS type). The type is the adapter's own name for it,
207
- # checked for shape by the node; whether it exists is the database's to
208
- # say.
514
+ # @!endgroup
515
+ # @!group Conversions
516
+
517
+ # `CAST(expr AS type)`. The type is the adapter's own name for it --
518
+ # `decimal(10,2)`, `double precision` -- and has to look like one;
519
+ # whether it exists is the database's to say.
520
+ # @param type [Symbol, String]
521
+ # @return [AST::Cast]
522
+ # @example
523
+ # Post.select { cast(:price, "decimal(10,2)").as(:price) }
209
524
  def cast(expr, type)
210
525
  AST::Cast.new(expr, type)
211
526
  end
212
527
 
528
+ # @!endgroup
529
+ # @!group Window functions
530
+
531
+ # @!method row_number
532
+ # `ROW_NUMBER()`. Means nothing without {AST::Windowing#over}, and
533
+ # says so.
534
+ # @return [AST::WindowFunction]
535
+ # @example
536
+ # Author.select { row_number.over.partition(:country).order(:age.desc).as(:rank) }
537
+ # @!method rank
538
+ # `RANK()`; needs `over`.
539
+ # @return [AST::WindowFunction]
540
+ # @!method dense_rank
541
+ # `DENSE_RANK()`; needs `over`.
542
+ # @return [AST::WindowFunction]
543
+ # @!method percent_rank
544
+ # `PERCENT_RANK()`; needs `over`.
545
+ # @return [AST::WindowFunction]
546
+ # @!method cume_dist
547
+ # `CUME_DIST()`; needs `over`.
548
+ # @return [AST::WindowFunction]
549
+ # @!method ntile(buckets)
550
+ # `NTILE(buckets)`; needs `over`.
551
+ # @return [AST::WindowFunction]
552
+ # @!method first_value(expr)
553
+ # `FIRST_VALUE(expr)`; needs `over`.
554
+ # @return [AST::WindowFunction]
555
+ # @!method last_value(expr)
556
+ # `LAST_VALUE(expr)`; needs `over`.
557
+ # @return [AST::WindowFunction]
558
+ #
213
559
  # The functions that only mean anything with a window. Every adapter
214
560
  # that has window functions at all spells these the same -- PostgreSQL,
215
561
  # MySQL 8, SQLite 3.25 -- so unlike the scalar functions there is nothing
@@ -222,122 +568,193 @@ module ActiveRecord
222
568
  define_method(name) { |arg| AST::WindowFunction.new(name.to_s.upcase, [arg]) }
223
569
  end
224
570
 
571
+ # `NTH_VALUE(expr, nth)`; needs `over`.
572
+ # @return [AST::WindowFunction]
225
573
  def nth_value(expr, nth)
226
574
  AST::WindowFunction.new("NTH_VALUE", [expr, nth])
227
575
  end
228
576
 
577
+ # `LAG(expr, offset, default)`: the value `offset` rows before this
578
+ # one; needs `over`.
579
+ # @return [AST::WindowFunction]
580
+ # @example
581
+ # Post.select { (:likes - lag(:likes).over.order(:created_at)).as(:gain) }
582
+ #
229
583
  # The offset is written out rather than left to default, so that a
230
584
  # default value cannot end up where the offset belongs.
231
585
  def lag(expr, offset = 1, default = nil)
232
586
  AST::WindowFunction.new("LAG", default.nil? ? [expr, offset] : [expr, offset, default])
233
587
  end
234
588
 
589
+ # `LEAD(expr, offset, default)`: the value `offset` rows after this
590
+ # one; needs `over`.
591
+ # @return [AST::WindowFunction]
235
592
  def lead(expr, offset = 1, default = nil)
236
593
  AST::WindowFunction.new("LEAD", default.nil? ? [expr, offset] : [expr, offset, default])
237
594
  end
238
595
 
239
- # Escape hatch for functions without a method of their own. The name is
240
- # emitted as written, so a case-sensitive one can be spelled exactly,
241
- # and for that reason it has to be a plain name, optionally qualified by
242
- # a schema; anything else is refused rather than written into the SQL.
596
+ # @!endgroup
597
+ # @!group Escape hatches
598
+
599
+ # Any function by name: `fn(:date_part, "year", :created_at)`. The name
600
+ # is written as given -- so a case-sensitive one can be spelled exactly
601
+ # -- and has to be a plain name, optionally qualified by a schema;
602
+ # the arguments are quoted as values unless they are columns or
603
+ # expressions.
604
+ # @param name [Symbol, String]
605
+ # @return [AST::Function]
606
+ # @example
607
+ # Post.select { fn(:date_part, "year", :created_at).as(:year) }
608
+ #
609
+ # The name is emitted as written, so a case-sensitive one can be
610
+ # spelled exactly, and for that reason it has to be a plain name,
611
+ # optionally qualified by a schema; anything else is refused rather
612
+ # than written into the SQL.
243
613
  def fn(name, *args)
244
614
  AST::Function.new(
245
615
  AST.check_name(name, AST::FUNCTION_NAME, "function name").to_s, args)
246
616
  end
247
617
 
248
- # The same escape hatch for operators: op("&&", :tags, "{ruby,sql}").
618
+ # Any binary operator by its spelling: `op("&&", :tags, "{ruby,sql}")`.
619
+ # The operator has to be made of operator characters; the operands are
620
+ # quoted as values unless they are columns or expressions, and
621
+ # parenthesized, since the operator's precedence is not known.
622
+ # @param operator [String]
623
+ # @return [AST::Operation]
624
+ # @example
625
+ # Post.where { op("&&", :tags, "{ruby,sql}") } # PostgreSQL arrays
249
626
  def op(operator, left, right)
250
627
  AST::Operation.new(operator, left, right)
251
628
  end
252
629
 
253
- # BIT_COUNT. MySQL counts the bits of a number; PostgreSQL counts those
254
- # of a bit string, so the argument is cast, and to bit(64) because that
255
- # is what makes a negative come back as MySQL has it -- 64 bits of two's
630
+ # @!endgroup
631
+ # @!group Bits
632
+
633
+ # `BIT_COUNT(expr)`, the bits set in a number. MySQL and PostgreSQL
634
+ # have it; SQLite, Oracle and SQL Server do not.
635
+ # @return [AST::Function]
636
+ # @example
637
+ # Post.select { bit_count(:flags).as(:set) }
638
+ #
639
+ # MySQL counts the bits of a number; PostgreSQL counts those of a bit
640
+ # string, so the argument is cast, and to bit(64) because that is what
641
+ # makes a negative come back as MySQL has it -- 64 bits of two's
256
642
  # complement rather than as many as the column happens to be wide.
257
643
  def bit_count(expr)
258
- case adapter_family
259
- when :mysql then AST::Function.new("BIT_COUNT", [expr])
260
- when :postgresql
261
- AST::Function.new("BIT_COUNT", [AST::Cast.new(expr, "bit(64)")])
262
- else
263
- raise NotImplementedError,
264
- "bit_count has no equivalent on #{@model.connection_db_config.adapter}"
265
- end
644
+ dialect.bit_count(expr, @model)
266
645
  end
267
646
 
647
+ # @!endgroup
648
+ # @!group Subqueries
649
+
650
+ # `EXISTS (subquery)`. The subquery is a relation, which may refer to
651
+ # the outer row through a qualified column.
652
+ # @param relation [ActiveRecord::Relation]
653
+ # @return [AST::Exists]
654
+ # @example
655
+ # Author.where { exists?(Post.where { :posts[:author_id] == :authors[:id] }) }
268
656
  def exists?(relation)
269
657
  AST::Exists.new(relation)
270
658
  end
271
659
 
272
- # ANY and ALL quantify a comparison over a subquery, which is what a
273
- # scalar subquery cannot do: it has to return the one row.
274
- #
660
+ # `ANY (subquery)`, on the right of a comparison: true of the rows the
661
+ # comparison holds for any row of the subquery. SQLite has none.
662
+ # @param relation [ActiveRecord::Relation]
663
+ # @return [AST::Quantified]
664
+ # @example
275
665
  # Post.where { :likes > any(Post.published.select(:likes)) }
276
- # Post.where { :likes >= all(Post.select(:likes)) }
277
666
  #
278
- # `== any` is IN and `!= all` is NOT IN, so what these add is the four
667
+ # ANY and ALL quantify a comparison over a subquery, which is what a
668
+ # scalar subquery cannot do: it has to return the one row. `== any`
669
+ # is IN and `!= all` is NOT IN, so what these add is the four
279
670
  # comparisons IN has no spelling for.
280
671
  def any(relation)
281
672
  quantified("ANY", relation)
282
673
  end
283
674
 
675
+ # `ALL (subquery)`, on the right of a comparison: true of the rows the
676
+ # comparison holds for every row of the subquery. SQLite has none.
677
+ # @param relation [ActiveRecord::Relation]
678
+ # @return [AST::Quantified]
679
+ # @example
680
+ # Post.where { :likes >= all(Post.select(:likes)) }
284
681
  def all(relation)
285
682
  quantified("ALL", relation)
286
683
  end
287
684
 
288
- # SQL as written, asked for by name:
289
- #
290
- # where { sql("length(name) > ?", 10) }
291
- #
292
- # The one way a string means SQL inside a block. ? and :name
293
- # placeholders take quoted values, through sanitize_sql_array.
685
+ # @!endgroup
686
+ # @!group Escape hatches
687
+
688
+ # SQL as written, the one way a string means SQL inside a block. `?`
689
+ # and `:name` placeholders take quoted values, as `where` takes them.
690
+ # @param statement [String]
691
+ # @return [AST::Sql]
692
+ # @example
693
+ # Post.where { sql("length(title) > ?", 10) }
694
+ # Post.select { sql("count(*) FILTER (WHERE score > 0) AS positive") }
294
695
  def sql(statement, *binds)
295
696
  AST::Sql.new(statement, binds)
296
697
  end
297
698
 
298
- # A literal where an expression is expected, quoted like any other value:
299
- #
300
- # select { [:id, value(0).as(:depth)] }
301
- #
302
- # Numbers and strings have a shorthand -- `0.as(:depth)` -- so this is
303
- # the spelling for the rest: true, nil, a date.
699
+ # A literal where an expression is expected, quoted like any other
700
+ # value. A number or a string takes `as` for itself -- `0.as(:depth)`
701
+ # -- so this is the spelling for the rest: `true`, `nil`, a date.
702
+ # @return [AST::Value]
703
+ # @example
704
+ # Node.select { [:id, value(0).as(:depth)] }
705
+ # Post.select { [:title, value(nil).as(:score)] }
304
706
  def value(literal)
305
707
  AST::Value.new(literal)
306
708
  end
307
709
 
308
- # The row an upsert could not insert, for the block upsert_all takes.
309
- # PostgreSQL and SQLite give it a name; MySQL spells the same thing
310
- # VALUES(column), which takes the column bare.
710
+ # The row an upsert could not insert, in the block `upsert_all` takes:
711
+ # `"excluded"."column"` on PostgreSQL and SQLite, `VALUES(column)` on
712
+ # MySQL.
713
+ # @param column [Symbol]
714
+ # @return [AST::Node]
715
+ # @example
716
+ # Tally.upsert_all(rows, unique_by: :page) { { hits: :hits + excluded(:hits) } }
311
717
  def excluded(column)
312
- return AST::Column.new(:excluded, column) unless adapter_family == :mysql
313
-
314
- quoted = @model.with_connection { |c| c.quote_column_name(column) }
315
- AST::Function.new("VALUES", [Arel::Nodes::SqlLiteral.new(quoted)])
718
+ dialect.excluded(column, @model)
316
719
  end
317
720
 
318
- # CASE. `case` is a keyword, so Ruby only reaches this one through the
319
- # receiver -- `self.case` -- which is why the two shapes have shorthands
320
- # that do not need it: `:age.when(...)` for the form with an operand, and
321
- # `case_when` for the form where each when carries its own condition.
322
- #
721
+ # @!endgroup
722
+ # @!group CASE
723
+
724
+ # `CASE`, in either shape: with an operand each `when` is compared
725
+ # against, or without one, each `when` carrying its own condition.
726
+ # `case` is a keyword, so this one is reached as `self.case`; the
727
+ # shorthands `:age.when(...)` and {#case_when} need no receiver.
728
+ # @return [AST::Case]
729
+ # @example
323
730
  # self.case(:age).when(10).then(1).else(0)
324
731
  # self.case.when { :age >= 60 }.then { :age - 60 }
325
732
  def case(operand = nil)
326
733
  AST::Case.new(operand)
327
734
  end
328
735
 
329
- # The searched CASE, started at its first when:
330
- #
331
- # case_when { :age >= 60 }.then { :age - 60 }.else(0)
736
+ # The searched `CASE`, started at its first `when`: each `when` is a
737
+ # condition, as a value or a block, and `then` and `else` give the
738
+ # values.
739
+ # @return [AST::Case::When]
740
+ # @example
741
+ # Author.select { case_when { :age >= 60 }.then("senior").else("adult").as(:band) }
742
+ # Author.select { sum(case_when { :age >= 60 }.then(1).else(0)).as(:seniors) }
332
743
  def case_when(value = nil, &block)
333
744
  AST::Case.new.when(value, &block)
334
745
  end
335
746
 
336
747
  private
748
+ # @!endgroup
749
+ #
750
+ # The group closes here rather than above `private`: a comment on
751
+ # that line belongs to the `private` call, which reads no
752
+ # directives, and the group would run on into the next module.
753
+ #
337
754
  # SQLite is the one adapter with no quantifier at all, and what it says
338
755
  # when it meets one is a syntax error at the SELECT.
339
756
  def quantified(kind, relation)
340
- if adapter_family == :sqlite
757
+ unless dialect.quantifiers_supported?
341
758
  raise NotImplementedError,
342
759
  "#{kind} has no equivalent on #{@model.connection_db_config.adapter}"
343
760
  end
@@ -346,27 +763,39 @@ module ActiveRecord
346
763
 
347
764
  def grouping(kind, sets)
348
765
  node = AST::GroupingSets.new(kind, sets)
349
- return node if adapter_family == :postgresql
350
- return node if kind == :rollup && adapter_family == :mysql
766
+ return node if dialect.grouping_supported?(kind)
351
767
 
352
768
  raise NotImplementedError,
353
769
  "#{kind} has no equivalent on #{@model.connection_db_config.adapter}"
354
770
  end
355
771
 
356
- def function_name(name, functions)
357
- spellings = functions.fetch(name)
358
- return name.to_s.upcase unless spellings.key?(adapter_family)
359
- spellings.fetch(adapter_family) ||
360
- raise(NotImplementedError,
361
- "#{name} has no equivalent on #{@model.connection_db_config.adapter}")
362
- end
363
-
364
- def adapter_family
365
- @adapter_family ||= AST.adapter_family(@model)
772
+ def dialect
773
+ @dialect ||= Dialect.for(@model)
366
774
  end
367
775
  end
368
776
 
777
+ # The relation methods a block reaches, prepended to Active Record's
778
+ # own: `where`, `select`, `having`, `order` and `group` take a block
779
+ # beside what they take already, the joins take one for the ON, and
780
+ # `from`, `from_cte`, `distinct_on` and `lateral` are here for what
781
+ # Active Record has no spelling for. Without a block each is Active
782
+ # Record's own.
783
+ #
784
+ # @example
785
+ # Author.
786
+ # joins(:posts) { :posts[:author_id] == :authors[:id] }.
787
+ # where { :posts[:published] == true }.
788
+ # group { :authors[:id] }.
789
+ # having { count(:posts[:id]) > 1 }.
790
+ # order { count(:posts[:id]).desc }.
791
+ # select { [:name, count(:posts[:id]).as(:post_count)] }
369
792
  module QueryMethods
793
+ # `WHERE`, from a block: a condition built with the comparisons of
794
+ # {BlockSyntax}, combined with `&`, `|` and `!`.
795
+ # @yieldreturn [AST::Predicate]
796
+ # @example
797
+ # Author.where { :age >= 18 & :country.in?(%w[JP US]) }
798
+ # Author.where { !:name.like?("A%") }
370
799
  def where(opts = nil, *rest, &block)
371
800
  if block
372
801
  super(to_arel_condition(evaluate_block(&block)))
@@ -375,6 +804,11 @@ module ActiveRecord
375
804
  end
376
805
  end
377
806
 
807
+ # `SELECT`, from a block: an expression, or an array of them, each
808
+ # aliased with `as` or left to its own name.
809
+ # @yieldreturn [Symbol, AST::Node, Array<Symbol, AST::Node>]
810
+ # @example
811
+ # Author.select { [:name, upper(:name).as(:shouted), count(:*).as(:n)] }
378
812
  def select(*fields, &block)
379
813
  if block
380
814
  super(*to_arel_fields(evaluate_block(&block)), &nil)
@@ -383,6 +817,10 @@ module ActiveRecord
383
817
  end
384
818
  end
385
819
 
820
+ # `HAVING`, from a block: a condition over the aggregates of a group.
821
+ # @yieldreturn [AST::Predicate]
822
+ # @example
823
+ # Author.group { :country }.having { count(:*) > 1 }
386
824
  def having(opts = nil, *rest, &block)
387
825
  if block
388
826
  super(to_arel_condition(evaluate_block(&block)))
@@ -391,6 +829,11 @@ module ActiveRecord
391
829
  end
392
830
  end
393
831
 
832
+ # `ORDER BY`, from a block: an ordering, or an array of them --
833
+ # `:age.desc`, `count(:*).desc.nulls_last`, or a bare column.
834
+ # @yieldreturn [Symbol, AST::Node, Array<Symbol, AST::Node>]
835
+ # @example
836
+ # Author.order { [:country.asc.nulls_last, :age.desc] }
394
837
  def order(*args, &block)
395
838
  if block
396
839
  super(*to_arel_fields(evaluate_block(&block)), &nil)
@@ -399,6 +842,12 @@ module ActiveRecord
399
842
  end
400
843
  end
401
844
 
845
+ # `GROUP BY`, from a block: a column or an expression, an array of
846
+ # them, or one of {BlockContext#grouping_sets}, {BlockContext#rollup}
847
+ # and {BlockContext#cube}.
848
+ # @yieldreturn [Symbol, AST::Node, Array<Symbol, AST::Node>]
849
+ # @example
850
+ # Post.group { date_trunc("day", :created_at) }.select { [date_trunc("day", :created_at).as(:day), count(:*)] }
402
851
  def group(*args, &block)
403
852
  if block
404
853
  result = evaluate_block(&block)
@@ -409,9 +858,17 @@ module ActiveRecord
409
858
  end
410
859
  end
411
860
 
412
- # A symbol names a table, which Active Record's own from only takes as a
413
- # string. With `as` it is selected under another name; when that name
414
- # is the model's own, from_cte says the same thing without repeating it.
861
+ # `FROM`, with a table named as a symbol and, with `as:`, selected
862
+ # under another name; anything else is Active Record's own `from`.
863
+ # @param value [Symbol, String, ActiveRecord::Relation]
864
+ # @param as [Symbol, nil] the name the table is selected under
865
+ # @example
866
+ # Post.from(:archived_posts, as: :posts)
867
+ #
868
+ # A symbol names a table, which Active Record's own from only takes as
869
+ # a string. With `as` it is selected under another name; when that
870
+ # name is the model's own, from_cte says the same thing without
871
+ # repeating it.
415
872
  def from(value, subquery_name = nil, as: nil)
416
873
  unless value.is_a?(Symbol)
417
874
  if as
@@ -424,12 +881,16 @@ module ActiveRecord
424
881
  super(arel_table, subquery_name)
425
882
  end
426
883
 
427
- # Selects a CTE in place of the model's own table. The alias is not a
428
- # choice -- Active Record keeps qualifying columns with the table name,
429
- # so the model's is the only name that works -- which is why it is
430
- # taken from the model rather than asked for:
431
- # with_recursive(tree: [...]).from_cte(:tree)
884
+ # Selects a CTE in place of the model's own table, under the model's
885
+ # own name, so that the columns Active Record qualifies still resolve.
886
+ # The name has to be one `with` or `with_recursive` declares.
887
+ # @param name [Symbol] the CTE's name
888
+ # @example
889
+ # Node.with_recursive(tree: [Node.where { :id == 1 }, Node.joins(...)]).from_cte(:tree)
432
890
  #
891
+ # The alias is not a choice -- Active Record keeps qualifying columns
892
+ # with the table name, so the model's is the only name that works --
893
+ # which is why it is taken from the model rather than asked for.
433
894
  # The name is checked against what `with` declares, so that a typo is
434
895
  # not a query against a table nobody has. Checked when the SQL is
435
896
  # built, since the CTE may be declared after this in the chain, or by a
@@ -443,28 +904,31 @@ module ActiveRecord
443
904
  relation
444
905
  end
445
906
 
907
+ # @private
446
908
  def from_cte_value
447
909
  @values[:from_cte]
448
910
  end
449
911
 
912
+ # @private
450
913
  def from_cte_value=(name)
451
914
  assert_modifiable!
452
915
  @values[:from_cte] = name
453
916
  end
454
917
 
455
- # DISTINCT ON (...), which keeps the first row of each group the order
456
- # brings up. PostgreSQL has it and the others do not; Arel carries the
457
- # node and refuses to write it elsewhere, the way it does a regexp, so
458
- # there is nothing for this to check:
459
- #
460
- # Post.distinct_on { :author }.order { [:author, :likes.desc] }
918
+ # `SELECT DISTINCT ON (columns)`: the first row of each group the
919
+ # order brings up. PostgreSQL has it; the portable shape is a
920
+ # `row_number` window in a subquery.
921
+ # @param columns [Array<Symbol>] the columns, unless a block gives them
922
+ # @example
923
+ # Post.distinct_on { :author_id }.order { [:author_id, :likes.desc] }
461
924
  #
462
- # The portable shape is a row_number window in a subquery, which the
463
- # README shows.
925
+ # Arel carries the node and refuses to write it elsewhere, the way it
926
+ # does a regexp, so there is nothing for this to check.
464
927
  def distinct_on(*columns, &block)
465
928
  spawn.distinct_on!(*columns, &block)
466
929
  end
467
930
 
931
+ # {#distinct_on} on the relation itself.
468
932
  def distinct_on!(*columns, &block)
469
933
  columns = Array(evaluate_block(&block)) if block
470
934
  if columns.empty?
@@ -476,39 +940,54 @@ module ActiveRecord
476
940
 
477
941
  # Active Record generates these for the values it knows about; this one
478
942
  # is ours, and lives in the same place so that it survives a spawn.
943
+ # @private
479
944
  def distinct_on_values
480
945
  @values.fetch(:distinct_on, ActiveRecord::QueryMethods::FROZEN_EMPTY_ARRAY)
481
946
  end
482
947
 
948
+ # @private
483
949
  def distinct_on_values=(columns)
484
950
  assert_modifiable!
485
951
  @values[:distinct_on] = columns
486
952
  end
487
953
 
488
- # Marks the relation for a lateral join, which lets it see the row being
489
- # joined to -- the top few rows of each group, and the like. In SQL the
490
- # keyword modifies the subquery, not the join, so it is said on the
491
- # relation: left_outer_joins(top_post.lateral, as: :top).
954
+ # Marks the relation for a `LATERAL` join, which lets the subquery see
955
+ # the row it is joined to -- the top few rows of each group, and the
956
+ # like. Said on the relation, since in SQL the keyword modifies the
957
+ # subquery rather than the join. SQLite and MariaDB have none.
958
+ # @example
959
+ # top = Post.where { :posts[:author_id] == :authors[:id] }.order { :likes.desc }.limit(1)
960
+ # Author.left_outer_joins(top.lateral, as: :top) { true }.select { [:name, :top[:title]] }
492
961
  def lateral
493
962
  spawn.lateral!
494
963
  end
495
964
 
965
+ # {#lateral} on the relation itself.
496
966
  def lateral!
497
967
  self.lateral_value = true
498
968
  self
499
969
  end
500
970
 
971
+ # @private
501
972
  def lateral_value
502
973
  @values[:lateral]
503
974
  end
504
975
 
976
+ # @private
505
977
  def lateral_value=(value)
506
978
  assert_modifiable!
507
979
  @values[:lateral] = value
508
980
  end
509
981
 
510
- # `as` names the table within the query, which is what makes a self
511
- # join expressible: joins(:employees, as: :managers) { ... }.
982
+ # `INNER JOIN`, with the `ON` from a block: `joins(:posts) { ... }`
983
+ # joins the table named, `joins(relation) { ... }` a subquery -- a
984
+ # lateral one when the relation is marked {#lateral}. `as:` names the
985
+ # table within the query, which is what makes a self join expressible.
986
+ # Without a block it is Active Record's own `joins`.
987
+ # @param as [Symbol, nil]
988
+ # @example
989
+ # Author.joins(:posts) { :posts[:author_id] == :authors[:id] }
990
+ # Employee.joins(:employees, as: :managers) { :managers[:id] == :employees[:manager_id] }
512
991
  def joins(*args, as: nil, &block)
513
992
  if args.first.is_a?(ActiveRecord::Relation)
514
993
  super(build_lateral_join(args.first, Arel::Nodes::InnerJoin, as, &block))
@@ -520,6 +999,10 @@ module ActiveRecord
520
999
  end
521
1000
  end
522
1001
 
1002
+ # `LEFT OUTER JOIN`, as {#joins} takes it.
1003
+ # @param as [Symbol, nil]
1004
+ # @example
1005
+ # Author.left_outer_joins(:posts) { :posts[:author_id] == :authors[:id] }
523
1006
  def left_outer_joins(*args, as: nil, &block)
524
1007
  if args.first.is_a?(ActiveRecord::Relation)
525
1008
  joins(build_lateral_join(args.first, Arel::Nodes::OuterJoin, as, &block))
@@ -531,6 +1014,12 @@ module ActiveRecord
531
1014
  end
532
1015
  end
533
1016
 
1017
+ # `RIGHT OUTER JOIN`, as {#joins} takes it, of a table or a relation;
1018
+ # an association name is not among what it takes.
1019
+ # @param as [Symbol, nil]
1020
+ # @example
1021
+ # Post.right_outer_joins(:authors) { :posts[:author_id] == :authors[:id] }
1022
+ #
534
1023
  # The other two outer joins, which Active Record has no method for and
535
1024
  # Arel has the nodes for. The rules are joins': the block is the ON,
536
1025
  # `as` names the table within the query, a relation marked `lateral`
@@ -541,16 +1030,19 @@ module ActiveRecord
541
1030
  args, as, &block)
542
1031
  end
543
1032
 
1033
+ # `FULL OUTER JOIN`, as {#right_outer_joins} takes it. The MySQL
1034
+ # family has none.
1035
+ # @param as [Symbol, nil]
544
1036
  def full_outer_joins(*args, as: nil, &block)
545
1037
  check_full_outer_support
546
1038
  outer_joins(:full_outer_joins, Arel::Nodes::FullOuterJoin,
547
1039
  args, as, &block)
548
1040
  end
549
1041
 
550
- # CROSS JOIN: every row of one table against every row of the other, so
551
- # unlike the joins above there is no condition to give and no block to
552
- # write it in.
553
- #
1042
+ # `CROSS JOIN`: every row of one table against every row of the
1043
+ # other, so there is no condition to give and no block to write it in.
1044
+ # @param as [Symbol, nil]
1045
+ # @example
554
1046
  # Post.cross_joins(:authors)
555
1047
  # Post.cross_joins(:posts, as: :others)
556
1048
  def cross_joins(*args, as: nil, &block)
@@ -599,7 +1091,7 @@ module ActiveRecord
599
1091
  entries = Array(result)
600
1092
  return if entries.size == 1
601
1093
  return unless entries.any? { |node| node.is_a?(AST::GroupingSets) }
602
- return unless AST.adapter_family(klass) == :mysql
1094
+ return unless Dialect.for(klass).grouping_by_with_rollup?
603
1095
 
604
1096
  raise ArgumentError,
605
1097
  "WITH ROLLUP takes the whole group list; group by the rollup alone"
@@ -621,7 +1113,12 @@ module ActiveRecord
621
1113
  # whose meaning turns on where it stands is how an interpolation
622
1114
  # becomes an injection.
623
1115
  def to_arel_fields(result)
624
- Array(result).map do |node|
1116
+ fields =
1117
+ if result.nil? then []
1118
+ elsif result.is_a?(::Array) then result
1119
+ else [result]
1120
+ end
1121
+ fields.map do |node|
625
1122
  if node.is_a?(::String) && !node.is_a?(Arel::Nodes::SqlLiteral)
626
1123
  raise ArgumentError,
627
1124
  "#{node.inspect} could mean SQL or a string; " \
@@ -666,26 +1163,12 @@ module ActiveRecord
666
1163
  join_class.new(aliased, Arel::Nodes::On.new(on))
667
1164
  end
668
1165
 
669
- # PostgreSQL has LATERAL and so does MySQL, from 8.0.14. SQLite has
670
- # none, and neither has MariaDB, which answers to the same adapter as
671
- # MySQL. An adapter nobody has classified is left to say for itself.
672
1166
  def check_lateral_support
673
- case AST.adapter_family(klass)
674
- when :sqlite
675
- refuse_lateral("sqlite3")
676
- when :mysql
677
- refuse_lateral("MariaDB") if klass.with_connection { |c| c.mariadb? }
678
- end
679
- end
680
-
681
- def refuse_lateral(database)
682
- raise NotImplementedError, "a lateral join has no equivalent on #{database}"
1167
+ Dialect.for(klass).check_lateral(klass)
683
1168
  end
684
1169
 
685
- # MySQL has no FULL OUTER JOIN, and neither has MariaDB; SQLite has had
686
- # one since 3.39 and PostgreSQL always.
687
1170
  def check_full_outer_support
688
- return unless AST.adapter_family(klass) == :mysql
1171
+ return if Dialect.for(klass).full_outer_join_supported?
689
1172
  raise NotImplementedError, "a full outer join has no equivalent on MySQL"
690
1173
  end
691
1174
 
@@ -725,11 +1208,14 @@ module ActiveRecord
725
1208
  # QueryMethods. What a block adds here is the one thing their arguments
726
1209
  # cannot carry: a value worked out from the row rather than given.
727
1210
  module Writes
728
- # `update_all(likes: :likes)` sets the column to the symbol; the block
729
- # reads a symbol as the column it names, as every other block here does,
730
- # which is what lets the new value be built from the old:
731
- #
732
- # Post.where { ... }.update_all { { likes: :likes + 1 } }
1211
+ # `UPDATE`, from a block that gives a hash of column to value, where a
1212
+ # value may be an expression built from the row: `{ likes: :likes + 1 }`.
1213
+ # Without a block it is Active Record's own, where `likes: :likes`
1214
+ # sets the column to the symbol.
1215
+ # @yieldreturn [Hash{Symbol => Object}]
1216
+ # @example
1217
+ # Post.where { :published == true }.update_all { { likes: :likes + 1 } }
1218
+ # Post.update_all { { title: upper(:title) } }
733
1219
  def update_all(updates = nil, &block)
734
1220
  return super(updates) unless block
735
1221
  if updates
@@ -742,13 +1228,17 @@ module ActiveRecord
742
1228
  super(result.transform_values { |value| to_arel_field(value) })
743
1229
  end
744
1230
 
1231
+ # `INSERT ... ON CONFLICT DO UPDATE`, with a block for what happens to
1232
+ # a row that is already there: a hash of column to value, where
1233
+ # {BlockContext#excluded} is the row that could not be inserted. Takes
1234
+ # the block or `on_duplicate:`, not both.
1235
+ # @yieldreturn [Hash{Symbol => Object}]
1236
+ # @example
1237
+ # Tally.upsert_all(rows, unique_by: :page) { { hits: :hits + excluded(:hits) } }
1238
+ #
745
1239
  # upsert_all's on_duplicate takes SQL text and nothing else, so this is
746
1240
  # the one place the DSL writes the SQL out itself rather than handing
747
- # Arel a tree. `excluded` is the row that could not be inserted:
748
- #
749
- # Post.upsert_all(rows, unique_by: :title) {
750
- # { likes: :likes + excluded(:likes) }
751
- # }
1241
+ # Arel a tree.
752
1242
  def upsert_all(attributes, **options, &block)
753
1243
  return super(attributes, **options) unless block
754
1244
  if options.key?(:on_duplicate)