activerecord-refined 0.8.1 → 0.10.0

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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +17 -0
  3. data/README.md +111 -815
  4. data/activerecord-refined.gemspec +36 -17
  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/joins.md +73 -0
  11. data/docs/json.md +230 -0
  12. data/docs/ordering.md +55 -0
  13. data/docs/time_zones.md +30 -0
  14. data/docs/windows.md +41 -0
  15. data/docs/writing.md +33 -0
  16. data/examples/aggregations.rb +31 -11
  17. data/examples/complex_joins.rb +12 -10
  18. data/examples/ctes.rb +22 -20
  19. data/examples/expressions.rb +110 -45
  20. data/examples/json.rb +77 -38
  21. data/examples/postgresql.rb +64 -53
  22. data/examples/predicates.rb +35 -33
  23. data/examples/subqueries.rb +20 -18
  24. data/examples/windows.rb +23 -21
  25. data/examples/writes.rb +26 -24
  26. data/lib/active_record/refined/ast.rb +1117 -373
  27. data/lib/active_record/refined/dialect/mariadb.rb +25 -0
  28. data/lib/active_record/refined/dialect/mysql.rb +18 -0
  29. data/lib/active_record/refined/dialect/mysql_compat.rb +67 -0
  30. data/lib/active_record/refined/dialect/oracle.rb +110 -0
  31. data/lib/active_record/refined/dialect/postgresql.rb +120 -0
  32. data/lib/active_record/refined/dialect/sql_server.rb +115 -0
  33. data/lib/active_record/refined/dialect/sqlite.rb +57 -0
  34. data/lib/active_record/refined/dialect.rb +340 -0
  35. data/lib/active_record/refined.rb +877 -307
  36. data/lib/activerecord-refined/version.rb +3 -1
  37. data/lib/activerecord-refined.rb +9 -5
  38. metadata +186 -15
  39. data/.github/workflows/push_gem.yml +0 -45
  40. data/.github/workflows/sandbox.yml +0 -295
  41. data/.github/workflows/test.yml +0 -90
  42. data/.gitignore +0 -19
  43. data/Gemfile +0 -12
  44. data/Rakefile +0 -25
  45. data/benchmark/query_building.rb +0 -129
  46. data/test/test_block_syntax.rb +0 -2493
  47. data/test/test_helper.rb +0 -221
@@ -1,6 +1,73 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveRecord
2
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" }
3
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
+
4
71
  refine Symbol do
5
72
  import_methods AST::Predications
6
73
  import_methods AST::Arithmetics
@@ -17,90 +84,339 @@ module ActiveRecord
17
84
  AST::Ordering.new(self, :desc)
18
85
  end
19
86
 
87
+ def collate(name)
88
+ AST::Collate.new(self, name)
89
+ end
90
+
20
91
  def [](column_name)
21
92
  AST::Column.new(self, column_name)
22
93
  end
23
94
  end
24
95
 
25
- # Shorthand for `value(0).as(:depth)` and the like. Numbers only: a
26
- # string in a select list already means SQL rather than a string, so
27
- # giving String this would make the same literal mean two things
28
- # depending on whether it had been sent a message.
29
- [Integer, Float].each do |klass|
96
+ # Shorthand for `value(0).as(:depth)` and the like, and arithmetic with
97
+ # the number on the left: 20 - :quantity. BigDecimal is a number here
98
+ # because that is what a decimal column's values are.
99
+ [Integer, Float, BigDecimal].each do |klass|
30
100
  refine klass do
101
+ import_methods AST::NumericArithmetics
102
+
31
103
  def as(alias_name, quote: true)
32
104
  AST::As.new(AST::Value.new(self), alias_name, quote: quote)
33
105
  end
34
106
  end
35
107
  end
108
+
109
+ # A string is a value here as it is in every other position of a block;
110
+ # SQL is asked for by name, with sql().
111
+ refine String do
112
+ def as(alias_name, quote: true)
113
+ AST::As.new(AST::Value.new(self), alias_name, quote: quote)
114
+ end
115
+ end
36
116
  end
37
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 }
38
133
  class BlockContext
39
134
  # The model is only consulted to learn which adapter the query is being
40
135
  # built for, which is what decides how a scalar function is spelled.
136
+ # @api private
41
137
  def initialize(model)
42
138
  @model = model
43
139
  end
44
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
45
156
  AGGREGATE_FUNCTIONS = {
46
157
  sum: :sum, avg: :average, min: :minimum, max: :maximum,
47
158
  }.freeze
48
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.
49
163
  AGGREGATE_FUNCTIONS.each do |name, arel_func|
50
- 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
51
171
  end
52
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) }
53
182
  def count(column, distinct: false)
54
183
  AST::Aggregate.new(column, :count, distinct: distinct)
55
184
  end
56
185
 
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) }
193
+ def json_arrayagg(value)
194
+ AST::JsonAggregate.new(:arrayagg, [value])
195
+ end
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) }
202
+ def json_objectagg(key, value)
203
+ AST::JsonAggregate.new(:objectagg, [key, value])
204
+ end
205
+
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) }
226
+ def json_array(*values)
227
+ AST::JsonBuild.new(:array, values)
228
+ end
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) }
236
+ def json_object(pairs = {})
237
+ AST::JsonBuild.new(:object, pairs)
238
+ end
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
+ #
57
379
  # Scalar functions, defined as real methods so that a typo is a
58
380
  # NoMethodError and a name Kernel also answers to (format, hash, test)
59
- # cannot quietly mean something else.
60
- #
61
- # The value lists the adapters that differ: a string is what the
62
- # function is called there, nil says the adapter has no equivalent. An
63
- # adapter that is not listed spells it like the method. The families
64
- # are what the entries key on, so trilogy reads the mysql column.
65
- #
66
- # Availability was checked by calling each one; the SQLite figures
67
- # assume the math functions its build usually enables.
68
- SCALAR_FUNCTIONS = {
69
- abs: {}, acos: {}, asin: {}, atan: {}, atan2: {}, ceil: {},
70
- coalesce: {}, concat: {}, cos: {}, degrees: {}, exp: {}, floor: {},
71
- length: {}, ln: {}, log: {}, log10: {}, lower: {}, ltrim: {},
72
- mod: {}, nullif: {}, pi: {}, power: {}, radians: {}, replace: {},
73
- round: {}, rtrim: {}, sign: {}, sin: {}, sqrt: {}, substr: {},
74
- tan: {}, trim: {}, upper: {},
75
- char_length: {sqlite: 'LENGTH'},
76
- greatest: {sqlite: 'MAX'},
77
- least: {sqlite: 'MIN'},
78
- # PostgreSQL spells log2(x) as log(2, x), which no renaming carries.
79
- log2: {postgresql: nil},
80
- # MySQL's TRUNCATE insists on the second argument, where the others
81
- # default it to zero; SQLite's trunc takes only the one.
82
- trunc: {mysql: 'TRUNCATE'},
83
- now: {sqlite: nil},
84
- # The bit aggregates, which PostgreSQL and MySQL spell alike and
85
- # SQLite has none of. PostgreSQL gained bit_xor in 14.
86
- bit_and: {sqlite: nil}, bit_or: {sqlite: nil}, bit_xor: {sqlite: nil},
87
- date_trunc: {sqlite: nil, mysql: nil},
88
- # Named for Kernel#rand, which it also takes back: a block calling
89
- # rand would otherwise get Ruby's and never reach the database.
90
- rand: {sqlite: 'RANDOM', postgresql: 'RANDOM'},
91
- # Two different functions share this name: printf formatting here, and
92
- # on MySQL the one that puts separators in a number, which reads a
93
- # printf template as the number zero rather than complaining. The
94
- # name keeps the one meaning; fn(:format, ...) reaches MySQL's.
95
- format: {mysql: nil},
96
- }.freeze
97
-
98
- 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|
99
393
  define_method(name) do |*args|
100
- AST::Function.new(function_name(name, SCALAR_FUNCTIONS), args)
394
+ AST::Function.new(dialect.function_name(name, @model), args)
101
395
  end
102
396
  end
103
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
+ #
104
420
  # The datetime value functions, as the SQL grammar calls them. These
105
421
  # the grammar has bare -- PostgreSQL and SQLite reject them written with
106
422
  # parentheses -- and the one thing that does go into parentheses is an
@@ -108,27 +424,28 @@ module ActiveRecord
108
424
  # takes and SQLite never accepts. The table reads like
109
425
  # SCALAR_FUNCTIONS; current_timestamp is the portable spelling of what
110
426
  # now means, reaching SQLite where now does not.
111
- DATETIME_VALUE_FUNCTIONS = {
112
- current_date: {},
113
- current_time: {},
114
- current_timestamp: {},
115
- localtime: {sqlite: nil},
116
- localtimestamp: {sqlite: nil},
117
- }.freeze
118
-
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 }
119
437
  def current_date
120
- AST::DatetimeValueFunction.new(
121
- function_name(:current_date, DATETIME_VALUE_FUNCTIONS))
438
+ AST::DatetimeValueFunction.new(dialect.function_name(:current_date, @model))
122
439
  end
123
440
 
124
- (DATETIME_VALUE_FUNCTIONS.keys - [:current_date]).each do |name|
441
+ (DATETIME_VALUE_FUNCTIONS - [:current_date]).each do |name|
125
442
  define_method(name) do |precision = nil|
126
443
  # Built first so that a precision of the wrong type is an
127
444
  # ArgumentError on every adapter, before SQLite gets to say it takes
128
445
  # none at all.
129
446
  node = AST::DatetimeValueFunction.new(
130
- function_name(name, DATETIME_VALUE_FUNCTIONS), precision)
131
- if precision && adapter_family == :sqlite
447
+ dialect.function_name(name, @model), precision)
448
+ if precision && !dialect.datetime_precision_supported?
132
449
  raise NotImplementedError,
133
450
  "#{name} takes no precision on #{@model.connection_db_config.adapter}"
134
451
  end
@@ -136,47 +453,109 @@ module ActiveRecord
136
453
  end
137
454
  end
138
455
 
139
- # EXTRACT(field FROM expr). The field is a keyword, not a value, so it
140
- # has to be a plain name; the node checks it. SQLite spells all of
141
- # this as strftime formats, which no renaming carries, so it raises
142
- # there -- after the node is built, so that a bad field is an
143
- # 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.
144
468
  def extract(field, expr)
145
469
  node = AST::Extract.new(field, expr)
146
- if adapter_family == :sqlite
470
+ unless dialect.extract_supported?
147
471
  raise NotImplementedError,
148
472
  "extract has no equivalent on #{@model.connection_db_config.adapter}"
149
473
  end
150
474
  node
151
475
  end
152
476
 
153
- # GROUP BY GROUPING SETS / ROLLUP / CUBE, which PostgreSQL has and the
154
- # others do not -- MySQL's WITH ROLLUP says one of the three and says it
155
- # somewhere else in the clause. Arel has the nodes and writes them for
156
- # PostgreSQL alone, so what it would raise elsewhere says nothing; this
157
- # says it here, as extract does, while the block is being read.
158
- #
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
159
486
  # Sale.group { grouping_sets([:region], [:product], []) }
160
- # 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.
161
491
  def grouping_sets(*sets)
162
492
  grouping(:grouping_sets, sets)
163
493
  end
164
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) }
165
501
  def rollup(*columns)
166
502
  grouping(:rollup, columns)
167
503
  end
168
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) }
169
510
  def cube(*columns)
170
511
  grouping(:cube, columns)
171
512
  end
172
513
 
173
- # CAST(expr AS type). The type is the adapter's own name for it,
174
- # checked for shape by the node; whether it exists is the database's to
175
- # 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) }
176
524
  def cast(expr, type)
177
525
  AST::Cast.new(expr, type)
178
526
  end
179
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
+ #
180
559
  # The functions that only mean anything with a window. Every adapter
181
560
  # that has window functions at all spells these the same -- PostgreSQL,
182
561
  # MySQL 8, SQLite 3.25 -- so unlike the scalar functions there is nothing
@@ -186,190 +565,310 @@ module ActiveRecord
186
565
  end
187
566
 
188
567
  %i[ntile first_value last_value].each do |name|
189
- define_method(name) {|arg| AST::WindowFunction.new(name.to_s.upcase, [arg]) }
568
+ define_method(name) { |arg| AST::WindowFunction.new(name.to_s.upcase, [arg]) }
190
569
  end
191
570
 
571
+ # `NTH_VALUE(expr, nth)`; needs `over`.
572
+ # @return [AST::WindowFunction]
192
573
  def nth_value(expr, nth)
193
- AST::WindowFunction.new('NTH_VALUE', [expr, nth])
574
+ AST::WindowFunction.new("NTH_VALUE", [expr, nth])
194
575
  end
195
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
+ #
196
583
  # The offset is written out rather than left to default, so that a
197
584
  # default value cannot end up where the offset belongs.
198
585
  def lag(expr, offset = 1, default = nil)
199
- AST::WindowFunction.new('LAG', default.nil? ? [expr, offset] : [expr, offset, default])
586
+ AST::WindowFunction.new("LAG", default.nil? ? [expr, offset] : [expr, offset, default])
200
587
  end
201
588
 
589
+ # `LEAD(expr, offset, default)`: the value `offset` rows after this
590
+ # one; needs `over`.
591
+ # @return [AST::WindowFunction]
202
592
  def lead(expr, offset = 1, default = nil)
203
- AST::WindowFunction.new('LEAD', default.nil? ? [expr, offset] : [expr, offset, default])
593
+ AST::WindowFunction.new("LEAD", default.nil? ? [expr, offset] : [expr, offset, default])
204
594
  end
205
595
 
206
- # Escape hatch for functions without a method of their own. The name is
207
- # emitted as written, so a case-sensitive one can be spelled exactly,
208
- # and for that reason it has to be a plain name, optionally qualified by
209
- # 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.
210
613
  def fn(name, *args)
211
614
  AST::Function.new(
212
615
  AST.check_name(name, AST::FUNCTION_NAME, "function name").to_s, args)
213
616
  end
214
617
 
215
- # BIT_COUNT. MySQL counts the bits of a number; PostgreSQL counts those
216
- # of a bit string, so the argument is cast, and to bit(64) because that
217
- # is what makes a negative come back as MySQL has it -- 64 bits of two's
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
626
+ def op(operator, left, right)
627
+ AST::Operation.new(operator, left, right)
628
+ end
629
+
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
218
642
  # complement rather than as many as the column happens to be wide.
219
643
  def bit_count(expr)
220
- case adapter_family
221
- when :mysql then AST::Function.new('BIT_COUNT', [expr])
222
- when :postgresql
223
- AST::Function.new('BIT_COUNT', [AST::Cast.new(expr, 'bit(64)')])
224
- else
225
- raise NotImplementedError,
226
- "bit_count has no equivalent on #{@model.connection_db_config.adapter}"
227
- end
644
+ dialect.bit_count(expr, @model)
228
645
  end
229
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] }) }
230
656
  def exists?(relation)
231
657
  AST::Exists.new(relation)
232
658
  end
233
659
 
234
- # ANY and ALL quantify a comparison over a subquery, which is what a
235
- # scalar subquery cannot do: it has to return the one row.
236
- #
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
237
665
  # Post.where { :likes > any(Post.published.select(:likes)) }
238
- # Post.where { :likes >= all(Post.select(:likes)) }
239
666
  #
240
- # `== 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
241
670
  # comparisons IN has no spelling for.
242
671
  def any(relation)
243
- quantified('ANY', relation)
672
+ quantified("ANY", relation)
244
673
  end
245
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)) }
246
681
  def all(relation)
247
- quantified('ALL', relation)
248
- end
249
-
250
- # A literal where an expression is expected, quoted like any other value:
251
- #
252
- # select { [:id, value(0).as(:depth)] }
253
- #
254
- # Needed because the top of a select list is Active Record's, and a bare
255
- # string there is SQL rather than a string. Numbers have a shorthand --
256
- # `0.as(:depth)` -- since nothing else could be meant by one.
682
+ quantified("ALL", relation)
683
+ end
684
+
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") }
695
+ def sql(statement, *binds)
696
+ AST::Sql.new(statement, binds)
697
+ end
698
+
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)] }
257
706
  def value(literal)
258
707
  AST::Value.new(literal)
259
708
  end
260
709
 
261
- # The row an upsert could not insert, for the block upsert_all takes.
262
- # PostgreSQL and SQLite give it a name; MySQL spells the same thing
263
- # 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) } }
264
717
  def excluded(column)
265
- return AST::Column.new(:excluded, column) unless adapter_family == :mysql
266
-
267
- quoted = @model.with_connection {|c| c.quote_column_name(column) }
268
- AST::Function.new('VALUES', [Arel::Nodes::SqlLiteral.new(quoted)])
718
+ dialect.excluded(column, @model)
269
719
  end
270
720
 
271
- # CASE. `case` is a keyword, so Ruby only reaches this one through the
272
- # receiver -- `self.case` -- which is why the two shapes have shorthands
273
- # that do not need it: `:age.when(...)` for the form with an operand, and
274
- # `case_when` for the form where each when carries its own condition.
275
- #
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
276
730
  # self.case(:age).when(10).then(1).else(0)
277
731
  # self.case.when { :age >= 60 }.then { :age - 60 }
278
732
  def case(operand = nil)
279
733
  AST::Case.new(operand)
280
734
  end
281
735
 
282
- # The searched CASE, started at its first when:
283
- #
284
- # 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) }
285
743
  def case_when(value = nil, &block)
286
744
  AST::Case.new.when(value, &block)
287
745
  end
288
746
 
289
747
  private
290
-
291
- # SQLite is the one adapter with no quantifier at all, and what it says
292
- # when it meets one is a syntax error at the SELECT.
293
- def quantified(kind, relation)
294
- if adapter_family == :sqlite
295
- raise NotImplementedError,
296
- "#{kind} has no equivalent on #{@model.connection_db_config.adapter}"
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
+ #
754
+ # SQLite is the one adapter with no quantifier at all, and what it says
755
+ # when it meets one is a syntax error at the SELECT.
756
+ def quantified(kind, relation)
757
+ unless dialect.quantifiers_supported?
758
+ raise NotImplementedError,
759
+ "#{kind} has no equivalent on #{@model.connection_db_config.adapter}"
760
+ end
761
+ AST::Quantified.new(kind, relation)
297
762
  end
298
- AST::Quantified.new(kind, relation)
299
- end
300
763
 
301
- def grouping(kind, sets)
302
- node = AST::GroupingSets.new(kind, sets)
303
- unless adapter_family == :postgresql
764
+ def grouping(kind, sets)
765
+ node = AST::GroupingSets.new(kind, sets)
766
+ return node if dialect.grouping_supported?(kind)
767
+
304
768
  raise NotImplementedError,
305
769
  "#{kind} has no equivalent on #{@model.connection_db_config.adapter}"
306
770
  end
307
- node
308
- end
309
771
 
310
- def function_name(name, functions)
311
- spellings = functions.fetch(name)
312
- return name.to_s.upcase unless spellings.key?(adapter_family)
313
- spellings.fetch(adapter_family) ||
314
- raise(NotImplementedError,
315
- "#{name} has no equivalent on #{@model.connection_db_config.adapter}")
316
- end
317
-
318
- def adapter_family
319
- @adapter_family ||= AST.adapter_family(@model)
320
- end
772
+ def dialect
773
+ @dialect ||= Dialect.for(@model)
774
+ end
321
775
  end
322
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)] }
323
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%") }
324
799
  def where(opts = nil, *rest, &block)
325
800
  if block
326
- super(evaluate_block(&block).to_arel(table, klass))
801
+ super(to_arel_condition(evaluate_block(&block)))
327
802
  else
328
803
  super
329
804
  end
330
805
  end
331
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)] }
332
812
  def select(*fields, &block)
333
813
  if block
334
- result = evaluate_block(&block)
335
- arel = Array(result).map {|node| to_arel_field(node) }
336
- super(*arel, &nil)
814
+ super(*to_arel_fields(evaluate_block(&block)), &nil)
337
815
  else
338
816
  super
339
817
  end
340
818
  end
341
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 }
342
824
  def having(opts = nil, *rest, &block)
343
825
  if block
344
- super(evaluate_block(&block).to_arel(table, klass))
826
+ super(to_arel_condition(evaluate_block(&block)))
345
827
  else
346
828
  super
347
829
  end
348
830
  end
349
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] }
350
837
  def order(*args, &block)
351
838
  if block
352
- result = evaluate_block(&block)
353
- arel = Array(result).map {|node| to_arel_field(node) }
354
- super(*arel, &nil)
839
+ super(*to_arel_fields(evaluate_block(&block)), &nil)
355
840
  else
356
841
  super
357
842
  end
358
843
  end
359
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(:*)] }
360
851
  def group(*args, &block)
361
852
  if block
362
853
  result = evaluate_block(&block)
363
- arel = Array(result).map {|node| to_arel_field(node) }
364
- super(*arel, &nil)
854
+ check_rollup_stands_alone(result)
855
+ super(*to_arel_fields(result), &nil)
365
856
  else
366
857
  super
367
858
  end
368
859
  end
369
860
 
370
- # A symbol names a table, which Active Record's own from only takes as a
371
- # string. With `as` it is selected under another name; when that name
372
- # 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.
373
872
  def from(value, subquery_name = nil, as: nil)
374
873
  unless value.is_a?(Symbol)
375
874
  if as
@@ -382,12 +881,16 @@ module ActiveRecord
382
881
  super(arel_table, subquery_name)
383
882
  end
384
883
 
385
- # Selects a CTE in place of the model's own table. The alias is not a
386
- # choice -- Active Record keeps qualifying columns with the table name,
387
- # so the model's is the only name that works -- which is why it is
388
- # taken from the model rather than asked for:
389
- # 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)
390
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.
391
894
  # The name is checked against what `with` declares, so that a typo is
392
895
  # not a query against a table nobody has. Checked when the SQL is
393
896
  # built, since the CTE may be declared after this in the chain, or by a
@@ -401,28 +904,31 @@ module ActiveRecord
401
904
  relation
402
905
  end
403
906
 
907
+ # @private
404
908
  def from_cte_value
405
909
  @values[:from_cte]
406
910
  end
407
911
 
912
+ # @private
408
913
  def from_cte_value=(name)
409
914
  assert_modifiable!
410
915
  @values[:from_cte] = name
411
916
  end
412
917
 
413
- # DISTINCT ON (...), which keeps the first row of each group the order
414
- # brings up. PostgreSQL has it and the others do not; Arel carries the
415
- # node and refuses to write it elsewhere, the way it does a regexp, so
416
- # there is nothing for this to check:
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] }
417
924
  #
418
- # Post.distinct_on { :author }.order { [:author, :likes.desc] }
419
- #
420
- # The portable shape is a row_number window in a subquery, which the
421
- # 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.
422
927
  def distinct_on(*columns, &block)
423
928
  spawn.distinct_on!(*columns, &block)
424
929
  end
425
930
 
931
+ # {#distinct_on} on the relation itself.
426
932
  def distinct_on!(*columns, &block)
427
933
  columns = Array(evaluate_block(&block)) if block
428
934
  if columns.empty?
@@ -434,39 +940,54 @@ module ActiveRecord
434
940
 
435
941
  # Active Record generates these for the values it knows about; this one
436
942
  # is ours, and lives in the same place so that it survives a spawn.
943
+ # @private
437
944
  def distinct_on_values
438
945
  @values.fetch(:distinct_on, ActiveRecord::QueryMethods::FROZEN_EMPTY_ARRAY)
439
946
  end
440
947
 
948
+ # @private
441
949
  def distinct_on_values=(columns)
442
950
  assert_modifiable!
443
951
  @values[:distinct_on] = columns
444
952
  end
445
953
 
446
- # Marks the relation for a lateral join, which lets it see the row being
447
- # joined to -- the top few rows of each group, and the like. In SQL the
448
- # keyword modifies the subquery, not the join, so it is said on the
449
- # 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]] }
450
961
  def lateral
451
962
  spawn.lateral!
452
963
  end
453
964
 
965
+ # {#lateral} on the relation itself.
454
966
  def lateral!
455
967
  self.lateral_value = true
456
968
  self
457
969
  end
458
970
 
971
+ # @private
459
972
  def lateral_value
460
973
  @values[:lateral]
461
974
  end
462
975
 
976
+ # @private
463
977
  def lateral_value=(value)
464
978
  assert_modifiable!
465
979
  @values[:lateral] = value
466
980
  end
467
981
 
468
- # `as` names the table within the query, which is what makes a self
469
- # 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] }
470
991
  def joins(*args, as: nil, &block)
471
992
  if args.first.is_a?(ActiveRecord::Relation)
472
993
  super(build_lateral_join(args.first, Arel::Nodes::InnerJoin, as, &block))
@@ -478,6 +999,10 @@ module ActiveRecord
478
999
  end
479
1000
  end
480
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] }
481
1006
  def left_outer_joins(*args, as: nil, &block)
482
1007
  if args.first.is_a?(ActiveRecord::Relation)
483
1008
  joins(build_lateral_join(args.first, Arel::Nodes::OuterJoin, as, &block))
@@ -489,6 +1014,12 @@ module ActiveRecord
489
1014
  end
490
1015
  end
491
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
+ #
492
1023
  # The other two outer joins, which Active Record has no method for and
493
1024
  # Arel has the nodes for. The rules are joins': the block is the ON,
494
1025
  # `as` names the table within the query, a relation marked `lateral`
@@ -499,156 +1030,192 @@ module ActiveRecord
499
1030
  args, as, &block)
500
1031
  end
501
1032
 
1033
+ # `FULL OUTER JOIN`, as {#right_outer_joins} takes it. The MySQL
1034
+ # family has none.
1035
+ # @param as [Symbol, nil]
502
1036
  def full_outer_joins(*args, as: nil, &block)
503
1037
  check_full_outer_support
504
1038
  outer_joins(:full_outer_joins, Arel::Nodes::FullOuterJoin,
505
1039
  args, as, &block)
506
1040
  end
507
1041
 
508
- # CROSS JOIN: every row of one table against every row of the other, so
509
- # unlike the joins above there is no condition to give and no block to
510
- # write it in.
511
- #
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
512
1046
  # Post.cross_joins(:authors)
513
1047
  # Post.cross_joins(:posts, as: :others)
514
1048
  def cross_joins(*args, as: nil, &block)
515
1049
  if block
516
1050
  raise ArgumentError,
517
- 'a cross join has no condition; joins is the one that takes a block'
1051
+ "a cross join has no condition; joins is the one that takes a block"
518
1052
  end
519
1053
  joins(build_cross_join(args.first, as))
520
1054
  end
521
1055
 
522
1056
  private
523
-
524
- def build_arel(...)
525
- check_from_cte
526
- arel = super
527
- unless distinct_on_values.empty?
528
- arel.distinct_on(distinct_on_values.map {|column| to_arel_field(column) })
1057
+ def build_arel(...)
1058
+ check_from_cte
1059
+ arel = super
1060
+ unless distinct_on_values.empty?
1061
+ arel.distinct_on(distinct_on_values.map { |column| to_arel_field(column) })
1062
+ end
1063
+ arel
529
1064
  end
530
- arel
531
- end
532
1065
 
533
- # Only when every `with` is one this can read the names out of; anything
534
- # else and there is nothing to be sure about, so nothing is said.
535
- def check_from_cte
536
- name = from_cte_value
537
- return unless name
538
- return unless with_values.all? {|value| value.is_a?(::Hash) }
1066
+ # Only when every `with` is one this can read the names out of; anything
1067
+ # else and there is nothing to be sure about, so nothing is said.
1068
+ def check_from_cte
1069
+ name = from_cte_value
1070
+ return unless name
1071
+ return unless with_values.all? { |value| value.is_a?(::Hash) }
539
1072
 
540
- declared = with_values.flat_map {|value| value.keys.map(&:to_sym) }
541
- return if declared.include?(name)
1073
+ declared = with_values.flat_map { |value| value.keys.map(&:to_sym) }
1074
+ return if declared.include?(name)
542
1075
 
543
- raise ArgumentError,
544
- "from_cte(#{name.inspect}) names no CTE; " +
545
- (declared.empty? ? "this query declares none" :
546
- "this query declares #{declared.map(&:inspect).join(', ')}")
547
- end
548
-
549
- def evaluate_block(&block)
550
- refined_block = block.refined(ActiveRecord::Refined::BlockSyntax)
551
- BlockContext.new(klass).instance_exec(&refined_block)
552
- end
1076
+ raise ArgumentError,
1077
+ "from_cte(#{name.inspect}) names no CTE; " +
1078
+ (declared.empty? ? "this query declares none" :
1079
+ "this query declares #{declared.map(&:inspect).join(', ')}")
1080
+ end
553
1081
 
554
- def to_arel_field(node)
555
- case node
556
- when AST::Node then node.to_arel(table, klass)
557
- when Symbol then table[node]
558
- else node
1082
+ def evaluate_block(&block)
1083
+ refined_block = block.refined(ActiveRecord::Refined::BlockSyntax)
1084
+ BlockContext.new(klass).instance_exec(&refined_block)
559
1085
  end
560
- end
561
1086
 
562
- def reject_join_alias(alias_name)
563
- return unless alias_name
564
- raise ArgumentError, "as: needs a block to write the ON clause with"
565
- end
1087
+ # WITH ROLLUP trails the whole group list, so on the MySQL family a
1088
+ # rollup cannot stand beside other group entries the way PostgreSQL's
1089
+ # ROLLUP(...) can.
1090
+ def check_rollup_stands_alone(result)
1091
+ entries = Array(result)
1092
+ return if entries.size == 1
1093
+ return unless entries.any? { |node| node.is_a?(AST::GroupingSets) }
1094
+ return unless Dialect.for(klass).grouping_by_with_rollup?
566
1095
 
567
- # The subquery is written out rather than handed over as a tree: Arel has
568
- # a LATERAL node but only PostgreSQL's visitor writes it, and MySQL can
569
- # read what it will not write. Without a block the join is ON TRUE,
570
- # which is the usual shape -- what the subquery is allowed to see is
571
- # what makes it lateral, and that is said inside it.
572
- def build_lateral_join(relation, join_class, alias_name, &block)
573
- unless relation.lateral_value
574
1096
  raise ArgumentError,
575
- "a relation joins laterally; mark it: joins(sub.lateral, as: :top)"
1097
+ "WITH ROLLUP takes the whole group list; group by the rollup alone"
576
1098
  end
577
- unless alias_name
578
- raise ArgumentError, "a lateral join needs a name: joins(..., as: :top)"
1099
+
1100
+ def to_arel_condition(result)
1101
+ return result if result.is_a?(Arel::Nodes::SqlLiteral)
1102
+ if result.is_a?(::String)
1103
+ raise ArgumentError,
1104
+ "#{result.inspect} is a string, not a condition; sql(...) " \
1105
+ "writes one as SQL"
1106
+ end
1107
+ result.to_arel(table, klass)
579
1108
  end
580
- check_lateral_support
581
1109
 
582
- aliased = Arel::Nodes::TableAlias.new(
583
- Arel::Nodes::SqlLiteral.new("LATERAL (#{relation.to_sql})"), alias_name)
584
- on = block ? evaluate_block(&block).to_arel(table, klass) : Arel::Nodes::True.new
585
- join_class.new(aliased, Arel::Nodes::On.new(on))
586
- end
1110
+ # The top of a select, order or group list. A bare string is refused
1111
+ # rather than passed to Active Record, where it would be SQL: inside a
1112
+ # block a string is a value in every other position, and a literal
1113
+ # whose meaning turns on where it stands is how an interpolation
1114
+ # becomes an injection.
1115
+ def to_arel_fields(result)
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|
1122
+ if node.is_a?(::String) && !node.is_a?(Arel::Nodes::SqlLiteral)
1123
+ raise ArgumentError,
1124
+ "#{node.inspect} could mean SQL or a string; " \
1125
+ "sql(...) says the SQL, value(...) the string"
1126
+ end
1127
+ to_arel_field(node)
1128
+ end
1129
+ end
587
1130
 
588
- # PostgreSQL has LATERAL and so does MySQL, from 8.0.14. SQLite has
589
- # none, and neither has MariaDB, which answers to the same adapter as
590
- # MySQL. An adapter nobody has classified is left to say for itself.
591
- def check_lateral_support
592
- case AST.adapter_family(klass)
593
- when :sqlite
594
- refuse_lateral('sqlite3')
595
- when :mysql
596
- refuse_lateral('MariaDB') if klass.with_connection {|c| c.mariadb? }
1131
+ def to_arel_field(node)
1132
+ case node
1133
+ when AST::Sql then node.field_arel(klass)
1134
+ when AST::Node then node.to_arel(table, klass)
1135
+ when Symbol then table[node]
1136
+ else node
1137
+ end
597
1138
  end
598
- end
599
1139
 
600
- def refuse_lateral(database)
601
- raise NotImplementedError, "a lateral join has no equivalent on #{database}"
602
- end
1140
+ def reject_join_alias(alias_name)
1141
+ return unless alias_name
1142
+ raise ArgumentError, "as: needs a block to write the ON clause with"
1143
+ end
603
1144
 
604
- # MySQL has no FULL OUTER JOIN, and neither has MariaDB; SQLite has had
605
- # one since 3.39 and PostgreSQL always.
606
- def check_full_outer_support
607
- return unless AST.adapter_family(klass) == :mysql
608
- raise NotImplementedError, 'a full outer join has no equivalent on MySQL'
609
- end
1145
+ # The subquery is written out rather than handed over as a tree: Arel has
1146
+ # a LATERAL node but only PostgreSQL's visitor writes it, and MySQL can
1147
+ # read what it will not write. Without a block the join is ON TRUE,
1148
+ # which is the usual shape -- what the subquery is allowed to see is
1149
+ # what makes it lateral, and that is said inside it.
1150
+ def build_lateral_join(relation, join_class, alias_name, &block)
1151
+ unless relation.lateral_value
1152
+ raise ArgumentError,
1153
+ "a relation joins laterally; mark it: joins(sub.lateral, as: :top)"
1154
+ end
1155
+ unless alias_name
1156
+ raise ArgumentError, "a lateral join needs a name: joins(..., as: :top)"
1157
+ end
1158
+ check_lateral_support
610
1159
 
611
- def outer_joins(called, join_class, args, alias_name, &block)
612
- if args.first.is_a?(ActiveRecord::Relation)
613
- return joins(build_lateral_join(args.first, join_class, alias_name, &block))
1160
+ aliased = Arel::Nodes::TableAlias.new(
1161
+ Arel::Nodes::SqlLiteral.new("LATERAL (#{relation.to_sql})"), alias_name)
1162
+ on = block ? evaluate_block(&block).to_arel(table, klass) : Arel::Nodes::True.new
1163
+ join_class.new(aliased, Arel::Nodes::On.new(on))
614
1164
  end
615
- return joins(build_join_node(args.first, join_class, alias_name, &block)) if block
616
1165
 
617
- raise ArgumentError,
618
- "#{called} takes a table and the block that joins it; an association " \
619
- 'is what joins and left_outer_joins read'
620
- end
1166
+ def check_lateral_support
1167
+ Dialect.for(klass).check_lateral(klass)
1168
+ end
621
1169
 
622
- # Arel has a node for every other join and none for this one, and INNER
623
- # JOIN with no ON -- which is a cross join on SQLite and MySQL -- is a
624
- # syntax error on PostgreSQL. So the SQL is written here, the second
625
- # place in the gem that writes any: the keyword is fixed and the names
626
- # are quoted by the adapter, so nothing of the caller's is in it.
627
- def build_cross_join(target_table, alias_name)
628
- joined = klass.with_connection do |connection|
629
- name = connection.quote_table_name(target_table.to_s)
630
- alias_name ? "#{name} #{connection.quote_table_name(alias_name.to_s)}" : name
1170
+ def check_full_outer_support
1171
+ return if Dialect.for(klass).full_outer_join_supported?
1172
+ raise NotImplementedError, "a full outer join has no equivalent on MySQL"
631
1173
  end
632
- Arel::Nodes::StringJoin.new(Arel.sql("CROSS JOIN #{joined}"))
633
- end
634
1174
 
635
- def build_join_node(target_table, join_class, alias_name, &block)
636
- ast = evaluate_block(&block)
637
- arel_table = Arel::Table.new(target_table)
638
- arel_table = arel_table.alias(alias_name) if alias_name
639
- join_class.new(arel_table, Arel::Nodes::On.new(ast.to_arel(table, klass)))
640
- end
1175
+ def outer_joins(called, join_class, args, alias_name, &block)
1176
+ if args.first.is_a?(ActiveRecord::Relation)
1177
+ return joins(build_lateral_join(args.first, join_class, alias_name, &block))
1178
+ end
1179
+ return joins(build_join_node(args.first, join_class, alias_name, &block)) if block
1180
+
1181
+ raise ArgumentError,
1182
+ "#{called} takes a table and the block that joins it; an association " \
1183
+ "is what joins and left_outer_joins read"
1184
+ end
1185
+
1186
+ # Arel has a node for every other join and none for this one, and INNER
1187
+ # JOIN with no ON -- which is a cross join on SQLite and MySQL -- is a
1188
+ # syntax error on PostgreSQL. So the SQL is written here, the second
1189
+ # place in the gem that writes any: the keyword is fixed and the names
1190
+ # are quoted by the adapter, so nothing of the caller's is in it.
1191
+ def build_cross_join(target_table, alias_name)
1192
+ joined = klass.with_connection do |connection|
1193
+ name = connection.quote_table_name(target_table.to_s)
1194
+ alias_name ? "#{name} #{connection.quote_table_name(alias_name.to_s)}" : name
1195
+ end
1196
+ Arel::Nodes::StringJoin.new(Arel.sql("CROSS JOIN #{joined}"))
1197
+ end
1198
+
1199
+ def build_join_node(target_table, join_class, alias_name, &block)
1200
+ ast = evaluate_block(&block)
1201
+ arel_table = Arel::Table.new(target_table)
1202
+ arel_table = arel_table.alias(alias_name) if alias_name
1203
+ join_class.new(arel_table, Arel::Nodes::On.new(ast.to_arel(table, klass)))
1204
+ end
641
1205
  end
642
1206
 
643
1207
  # The writing statements, which live on Relation rather than in
644
1208
  # QueryMethods. What a block adds here is the one thing their arguments
645
1209
  # cannot carry: a value worked out from the row rather than given.
646
1210
  module Writes
647
- # `update_all(likes: :likes)` sets the column to the symbol; the block
648
- # reads a symbol as the column it names, as every other block here does,
649
- # which is what lets the new value be built from the old:
650
- #
651
- # 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) } }
652
1219
  def update_all(updates = nil, &block)
653
1220
  return super(updates) unless block
654
1221
  if updates
@@ -658,16 +1225,20 @@ module ActiveRecord
658
1225
  unless result.is_a?(::Hash)
659
1226
  raise ArgumentError, "the block gives update_all a hash of column => value"
660
1227
  end
661
- super(result.transform_values {|value| to_arel_field(value) })
1228
+ super(result.transform_values { |value| to_arel_field(value) })
662
1229
  end
663
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
+ #
664
1239
  # upsert_all's on_duplicate takes SQL text and nothing else, so this is
665
1240
  # the one place the DSL writes the SQL out itself rather than handing
666
- # Arel a tree. `excluded` is the row that could not be inserted:
667
- #
668
- # Post.upsert_all(rows, unique_by: :title) {
669
- # { likes: :likes + excluded(:likes) }
670
- # }
1241
+ # Arel a tree.
671
1242
  def upsert_all(attributes, **options, &block)
672
1243
  return super(attributes, **options) unless block
673
1244
  if options.key?(:on_duplicate)
@@ -684,19 +1255,18 @@ module ActiveRecord
684
1255
  end
685
1256
 
686
1257
  private
687
-
688
- # The left of each assignment is the column being written, which is bare
689
- # -- the statement is already about one table -- and the right is the
690
- # expression, compiled here because a string is what on_duplicate reads.
691
- def set_clause(updates)
692
- klass.with_connection do |connection|
693
- updates.map do |column, value|
694
- expression = connection.visitor.compile(
695
- to_arel_field(value), Arel::Collectors::SQLString.new)
696
- "#{connection.quote_column_name(column)}=#{expression}"
697
- end.join(', ')
1258
+ # The left of each assignment is the column being written, which is bare
1259
+ # -- the statement is already about one table -- and the right is the
1260
+ # expression, compiled here because a string is what on_duplicate reads.
1261
+ def set_clause(updates)
1262
+ klass.with_connection do |connection|
1263
+ updates.map do |column, value|
1264
+ expression = connection.visitor.compile(
1265
+ to_arel_field(value), Arel::Collectors::SQLString.new)
1266
+ "#{connection.quote_column_name(column)}=#{expression}"
1267
+ end.join(", ")
1268
+ end
698
1269
  end
699
- end
700
1270
  end
701
1271
  end
702
1272
  end