activerecord-refined 0.9.0 → 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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +17 -0
  3. data/README.md +93 -965
  4. data/activerecord-refined.gemspec +12 -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/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 +18 -0
  17. data/examples/expressions.rb +35 -5
  18. data/lib/active_record/refined/ast.rb +461 -246
  19. data/lib/active_record/refined/dialect/mariadb.rb +25 -0
  20. data/lib/active_record/refined/dialect/mysql.rb +18 -0
  21. data/lib/active_record/refined/dialect/mysql_compat.rb +67 -0
  22. data/lib/active_record/refined/dialect/oracle.rb +110 -0
  23. data/lib/active_record/refined/dialect/postgresql.rb +120 -0
  24. data/lib/active_record/refined/dialect/sql_server.rb +115 -0
  25. data/lib/active_record/refined/dialect/sqlite.rb +57 -0
  26. data/lib/active_record/refined/dialect.rb +340 -0
  27. data/lib/active_record/refined.rb +682 -192
  28. data/lib/activerecord-refined/version.rb +1 -1
  29. data/lib/activerecord-refined.rb +1 -0
  30. metadata +58 -16
  31. data/.github/workflows/push_gem.yml +0 -45
  32. data/.github/workflows/sandbox.yml +0 -295
  33. data/.github/workflows/test.yml +0 -104
  34. data/.gitignore +0 -19
  35. data/.rubocop.yml +0 -393
  36. data/Gemfile +0 -14
  37. data/Rakefile +0 -53
  38. data/benchmark/query_building.rb +0 -129
  39. data/test/test_block_syntax.rb +0 -2999
  40. data/test/test_helper.rb +0 -238
@@ -6,37 +6,28 @@ require "json"
6
6
  module ActiveRecord
7
7
  module Refined
8
8
  module AST
9
+ # @private
9
10
  NAME = /[[:alpha:]_][[:alnum:]_$]*/
11
+ # @private
10
12
  ALIAS_NAME = /\A#{NAME}\z/
13
+ # @private
11
14
  FUNCTION_NAME = /\A#{NAME}(\.#{NAME})?\z/
15
+ # A collation name where the family writes it bare, as all but PostgreSQL
16
+ # do. A plain identifier: a hyphen unquoted would read as the collation
17
+ # minus a number, `x COLLATE nocase-1` as `(x COLLATE nocase) - 1`, valid
18
+ # and wrong. PostgreSQL quotes the name and widens this; see its dialect.
19
+ # @private
20
+ COLLATION_NAME = /\A#{NAME}\z/
12
21
  # A SQL type as cast writes it: words, at most parenthesized with
13
22
  # lengths -- double precision, decimal(10,2).
23
+ # @private
14
24
  TYPE_NAME = /\A[[:alpha:]_][[:alnum:]_ ]*(\(\d+(, ?\d+)?\))?\z/
15
25
  # The characters PostgreSQL allows an operator to be made of, the
16
26
  # widest operator alphabet of the three; op admits nothing else, so a
17
27
  # letter, a space or a quote never reaches the SQL as an operator.
28
+ # @private
18
29
  OPERATOR = %r{\A[+\-*/<>=~!@\#%^&|`?]+\z}
19
30
 
20
- # Which family of spellings an adapter belongs to. MariaDB answers to
21
- # the mysql2 adapter and is counted with MySQL, though the two part
22
- # company over JSON. An adapter nobody has classified keeps the
23
- # standard spellings and is left to say for itself what it cannot do.
24
- #
25
- # pglite is PostgreSQL itself compiled to WebAssembly, reached through
26
- # wasmify-rails' adapter; the server it answers for is the same one.
27
- ADAPTER_FAMILIES = {
28
- "sqlite3" => :sqlite,
29
- "postgresql" => :postgresql,
30
- "postgis" => :postgresql,
31
- "pglite" => :postgresql,
32
- "mysql2" => :mysql,
33
- "trilogy" => :mysql,
34
- }.freeze
35
-
36
- def self.adapter_family(model)
37
- ADAPTER_FAMILIES[model.connection_db_config.adapter] || :unknown
38
- end
39
-
40
31
  def self.check_name(name, pattern, what)
41
32
  return name if pattern.match?(name.to_s)
42
33
  raise ArgumentError, "#{name.inspect} is not a plain #{what}"
@@ -48,19 +39,28 @@ module ActiveRecord
48
39
  # functions; the MySQL family, which has no json(), reads it with
49
40
  # JSON_EXTRACT.
50
41
  def self.json_argument(value, model)
51
- json = Arel::Nodes.build_quoted(JSON.generate(value))
52
- if adapter_family(model) == :sqlite
53
- Arel::Nodes::NamedFunction.new("json", [json])
54
- else
55
- Arel::Nodes::NamedFunction.new(
56
- "JSON_EXTRACT", [json, Arel::Nodes.build_quoted("$")])
57
- end
42
+ Dialect.for(model).json_argument(value, model)
58
43
  end
59
44
 
45
+ # The conditions a column or an expression can be put in. Every one
46
+ # gives back a condition that combines with `&`, `|` and `!`, and the
47
+ # comparisons quote a Ruby value on the right the way Active Record
48
+ # does, or take a column, an expression or a subquery there.
49
+ #
50
+ # @example
51
+ # Author.where { :age.between?(20, 40) & :name.like?("A%") }
52
+ # Author.where { :id.in?(Post.select(:author_id)) }
53
+ #
60
54
  # Predicate builders shared by symbols, qualified columns and
61
55
  # expressions. Imported into the Symbol refinement with
62
56
  # Refinement#import_methods, so every method must be defined with def.
63
57
  module Predications
58
+ # `=`. A value, a column, an expression or a scalar subquery on the right; `nil` is refused, since `= NULL` is never true -- {#null?} is the spelling.
59
+ # @return [AST::Predicate]
60
+ # @example
61
+ # Author.where { :name == "alice" }
62
+ # Author.where { :age == Author.select { max(:age) } }
63
+ #
64
64
  # == and != mean SQL = and <>, and = NULL is never true there, so nil
65
65
  # is rejected rather than silently rewritten to IS NULL. null? builds
66
66
  # its node directly and stays clear of this check.
@@ -71,6 +71,8 @@ module ActiveRecord
71
71
  Comparison.new(self, :==, other)
72
72
  end
73
73
 
74
+ # `!=`; `nil` is refused, as with `==`.
75
+ # @return [AST::Predicate]
74
76
  def !=(other)
75
77
  if other.nil?
76
78
  raise ArgumentError, "!= does not take nil; use !null? instead"
@@ -78,30 +80,50 @@ module ActiveRecord
78
80
  Comparison.new(self, :!=, other)
79
81
  end
80
82
 
83
+ # `>`.
84
+ # @return [AST::Predicate]
81
85
  def >(other)
82
86
  Comparison.new(self, :>, other)
83
87
  end
84
88
 
89
+ # `>=`.
90
+ # @return [AST::Predicate]
85
91
  def >=(other)
86
92
  Comparison.new(self, :>=, other)
87
93
  end
88
94
 
95
+ # `<`.
96
+ # @return [AST::Predicate]
89
97
  def <(other)
90
98
  Comparison.new(self, :<, other)
91
99
  end
92
100
 
101
+ # `<=`.
102
+ # @return [AST::Predicate]
93
103
  def <=(other)
94
104
  Comparison.new(self, :<=, other)
95
105
  end
96
106
 
107
+ # A regular expression match: `~` on PostgreSQL, `REGEXP` on MySQL, and what the adapter has elsewhere. A Ruby Regexp's source is the pattern.
108
+ # @param pattern [Regexp, String]
109
+ # @return [AST::Predicate]
110
+ # @example
111
+ # Author.where { :name =~ /^A/ }
97
112
  def =~(pattern)
98
113
  Match.new(self, pattern)
99
114
  end
100
115
 
116
+ # The negated regular expression match.
117
+ # @return [AST::Predicate]
101
118
  def !~(pattern)
102
119
  Match.new(self, pattern, negated: true)
103
120
  end
104
121
 
122
+ # `IS NULL`.
123
+ # @return [AST::Predicate]
124
+ # @example
125
+ # Author.where { :country.null? }
126
+ #
105
127
  # `!` negates any predicate, so these are here for the four that SQL
106
128
  # spells for itself: IS NOT NULL rather than NOT (... IS NULL), and
107
129
  # likewise NOT IN and NOT LIKE. They mean the same thing either way,
@@ -110,10 +132,17 @@ module ActiveRecord
110
132
  Comparison.new(self, :==, nil)
111
133
  end
112
134
 
135
+ # `IS NOT NULL`.
136
+ # @return [AST::Predicate]
113
137
  def not_null?
114
138
  Comparison.new(self, :!=, nil)
115
139
  end
116
140
 
141
+ # `IS TRUE`: true of the rows where the boolean is true, false where it is false or NULL -- where `== true` would be NULL.
142
+ # @return [AST::Predicate]
143
+ # @example
144
+ # Post.where { :published.true? }
145
+ #
117
146
  # IS TRUE and IS FALSE differ from a comparison against the literal in
118
147
  # what they make of NULL: `flag = TRUE` is itself NULL there, and a
119
148
  # NULL predicate selects nothing, while these two answer false. So the
@@ -123,36 +152,62 @@ module ActiveRecord
123
152
  TruthValue.new(self, true)
124
153
  end
125
154
 
155
+ # `IS NOT TRUE`: keeps the NULL rows that `!(:flag == true)` drops.
156
+ # @return [AST::Predicate]
126
157
  def not_true?
127
158
  TruthValue.new(self, true, negated: true)
128
159
  end
129
160
 
161
+ # `IS FALSE`.
162
+ # @return [AST::Predicate]
130
163
  def false?
131
164
  TruthValue.new(self, false)
132
165
  end
133
166
 
167
+ # `IS NOT FALSE`.
168
+ # @return [AST::Predicate]
134
169
  def not_false?
135
170
  TruthValue.new(self, false, negated: true)
136
171
  end
137
172
 
173
+ # `IN (...)`: an array of values, a range, or a relation as a subquery.
174
+ # @param values [Array, Range, ActiveRecord::Relation]
175
+ # @return [AST::Predicate]
176
+ # @example
177
+ # Author.where { :country.in?(%w[JP US]) }
178
+ # Author.where { :id.in?(Post.select(:author_id)) }
138
179
  def in?(values)
139
180
  In.new(self, values)
140
181
  end
141
182
 
183
+ # `NOT IN (...)`.
184
+ # @return [AST::Predicate]
142
185
  def not_in?(values)
143
186
  In.new(self, values, negated: true)
144
187
  end
145
188
 
189
+ # `BETWEEN min AND max`, with either end a value, a column or an expression.
190
+ # @return [AST::Predicate]
191
+ # @example
192
+ # Author.where { :age.between?(20, 40) }
193
+ #
146
194
  # Not min..max: an endpoint may be an expression, which Range would
147
195
  # refuse to hold, since expressions do not compare among themselves.
148
196
  def between?(min, max)
149
197
  In.new(self, In::QuotedRange.new(min, max, false))
150
198
  end
151
199
 
200
+ # `NOT BETWEEN min AND max`.
201
+ # @return [AST::Predicate]
152
202
  def not_between?(min, max)
153
203
  In.new(self, In::QuotedRange.new(min, max, false), negated: true)
154
204
  end
155
205
 
206
+ # `CASE column WHEN value THEN ...`: a CASE with this as the operand, each `when` a value it is compared against, followed by `then` and finally `else`.
207
+ # @return [AST::Case::When]
208
+ # @example
209
+ # Author.select { :country.when("JP").then("Japan").else("elsewhere").as(:where) }
210
+ #
156
211
  # CASE with this as the operand, compared against each `when`:
157
212
  # `:age.when(10).then(1).else(0)`. The other shape, where each `when`
158
213
  # carries its own condition, starts at `case_when`.
@@ -160,22 +215,38 @@ module ActiveRecord
160
215
  Case.new(self).when(value, &block)
161
216
  end
162
217
 
218
+ # `LIKE pattern`, the pattern as written: `%` and `_` are its wildcards.
219
+ # @param pattern [String]
220
+ # @return [AST::Predicate]
221
+ # @example
222
+ # Author.where { :name.like?("A%") }
163
223
  def like?(pattern)
164
224
  Like.new(self, pattern)
165
225
  end
166
226
 
227
+ # `NOT LIKE pattern`.
228
+ # @return [AST::Predicate]
167
229
  def not_like?(pattern)
168
230
  Like.new(self, pattern, negated: true)
169
231
  end
170
232
 
233
+ # A case-insensitive `LIKE`: `ILIKE` on PostgreSQL, and `LIKE` over both sides lower-cased elsewhere.
234
+ # @return [AST::Predicate]
171
235
  def ilike?(pattern)
172
236
  Like.new(self, pattern, nil, case_sensitive: false)
173
237
  end
174
238
 
239
+ # The negated case-insensitive `LIKE`.
240
+ # @return [AST::Predicate]
175
241
  def not_ilike?(pattern)
176
242
  Like.new(self, pattern, nil, case_sensitive: false, negated: true)
177
243
  end
178
244
 
245
+ # Case-insensitive equality: `LOWER(column) = LOWER(value)`.
246
+ # @return [AST::Predicate]
247
+ # @example
248
+ # Author.where { :name.casecmp?("Alice") }
249
+ #
179
250
  # Case-insensitive equality, folded on both sides rather than left to
180
251
  # the collation, so it means the same thing on every adapter.
181
252
  def casecmp?(value)
@@ -186,16 +257,27 @@ module ActiveRecord
186
257
  Function.new("LOWER", [value]))
187
258
  end
188
259
 
260
+ # `IS DISTINCT FROM`: `!=` that treats NULL as a value. `IS NOT` on SQLite, `NOT <=>` on MySQL.
261
+ # @return [AST::Predicate]
262
+ #
189
263
  # Null-safe comparison: unlike = and <>, these treat NULL as a value,
190
264
  # so not_distinct_from? is the one equality that may take nil.
191
265
  def distinct_from?(value)
192
266
  DistinctFrom.new(self, value, negated: true)
193
267
  end
194
268
 
269
+ # `IS NOT DISTINCT FROM`: `=` that treats NULL as a value, so this is the one equality that takes `nil`.
270
+ # @return [AST::Predicate]
271
+ # @example
272
+ # Author.where { :country.not_distinct_from?(nil) }
195
273
  def not_distinct_from?(value)
196
274
  DistinctFrom.new(self, value)
197
275
  end
198
276
 
277
+ # `LIKE 'prefix%'`, the prefix escaped so that a `%` or `_` in it is itself; several prefixes are `OR`ed.
278
+ # @return [AST::Predicate]
279
+ # @example
280
+ # Author.where { :name.start_with?("A", "B") }
199
281
  def start_with?(*prefixes)
200
282
  if prefixes.empty?
201
283
  raise ArgumentError, "start_with? needs at least one prefix"
@@ -203,6 +285,8 @@ module ActiveRecord
203
285
  Like.any(self, prefixes.map { |prefix| "#{Like.escape(prefix)}%" })
204
286
  end
205
287
 
288
+ # `LIKE '%suffix'`, escaped as {#start_with?} escapes.
289
+ # @return [AST::Predicate]
206
290
  def end_with?(*suffixes)
207
291
  if suffixes.empty?
208
292
  raise ArgumentError, "end_with? needs at least one suffix"
@@ -210,10 +294,19 @@ module ActiveRecord
210
294
  Like.any(self, suffixes.map { |suffix| "%#{Like.escape(suffix)}" })
211
295
  end
212
296
 
297
+ # `LIKE '%substring%'`, escaped as {#start_with?} escapes.
298
+ # @return [AST::Predicate]
299
+ # @example
300
+ # Post.where { :title.include?("ruby") }
213
301
  def include?(substring)
214
302
  Like.new(self, "%#{Like.escape(substring)}%", Like::ESCAPE)
215
303
  end
216
304
 
305
+ # Whether a PostgreSQL array column holds the element: `@> ARRAY[element]`.
306
+ # @return [AST::Predicate]
307
+ # @example
308
+ # Post.where { :tags.member?("ruby") }
309
+ #
217
310
  # The array comparisons carry the meaning of their Ruby namesakes.
218
311
  # member? is Enumerable's element test, so an Array argument is
219
312
  # rejected rather than quietly meaning something Array#member? does
@@ -226,18 +319,32 @@ module ActiveRecord
226
319
  ArrayPredicate.new(self, :"@>", [element])
227
320
  end
228
321
 
322
+ # Whether an array column holds every element given: `@>`.
323
+ # @return [AST::Predicate]
229
324
  def superset?(elements)
230
325
  ArrayPredicate.new(self, :"@>", ArrayPredicate.elements(elements, "superset?"))
231
326
  end
232
327
 
328
+ # Whether every element of an array column is among those given: `<@`.
329
+ # @return [AST::Predicate]
233
330
  def subset?(elements)
234
331
  ArrayPredicate.new(self, :"<@", ArrayPredicate.elements(elements, "subset?"))
235
332
  end
236
333
 
334
+ # Whether an array column and the elements given share any: `&&`.
335
+ # @return [AST::Predicate]
336
+ # @example
337
+ # Post.where { :tags.intersect?(%w[ruby sql]) }
237
338
  def intersect?(elements)
238
339
  ArrayPredicate.new(self, :"&&", ArrayPredicate.elements(elements, "intersect?"))
239
340
  end
240
341
 
342
+ # The JSON at a path into a JSON column, still JSON -- to be dug further, compared with a Ruby value, or asked {#key?} and the rest. A string or a symbol steps into an object, an integer into an array. `#>` on PostgreSQL, `JSON_EXTRACT` on MySQL, `->` on SQLite.
343
+ # @return [AST::JsonPath]
344
+ # @example
345
+ # Doc.select { :meta.dig(:author).as(:author) }
346
+ # Doc.where { :meta.dig(:author).key?(:name) }
347
+ #
241
348
  # Reading inside a JSON document, by the name of what Hash does. A
242
349
  # string or symbol steps into an object, an integer into an array, and
243
350
  # what comes back is still JSON, the way Hash#dig hands back the
@@ -248,10 +355,19 @@ module ActiveRecord
248
355
  JsonPath.new(self, path)
249
356
  end
250
357
 
358
+ # The value at a path as text, which is what a comparison against a string wants where the JSON type would not do: `#>>` on PostgreSQL, `JSON_UNQUOTE(JSON_EXTRACT(...))` on MySQL, `->>` on SQLite.
359
+ # @return [AST::JsonPath]
360
+ # @example
361
+ # Doc.where { :meta.dig_text(:author, :name) == "alice" }
251
362
  def dig_text(*path)
252
363
  JsonPath.new(self, path, json_value: false)
253
364
  end
254
365
 
366
+ # The document without the keys given, as Hash#except gives it; an expression, for `update_all` to write back.
367
+ # @return [AST::JsonExcept]
368
+ # @example
369
+ # Doc.update_all { { meta: :meta.except(:draft) } }
370
+ #
255
371
  # Keys taken out of a JSON document, by the name of what Hash does,
256
372
  # and taking keys as Hash#except takes them. Like bury it gives back
257
373
  # the document changed rather than writing it anywhere.
@@ -259,6 +375,11 @@ module ActiveRecord
259
375
  JsonExcept.new(self, keys)
260
376
  end
261
377
 
378
+ # The document with a value set at a path, as {#dig} reads one; an expression, for `update_all` to write back.
379
+ # @return [AST::JsonSet]
380
+ # @example
381
+ # Doc.update_all { { meta: :meta.bury(:author, :name, "alice") } }
382
+ #
262
383
  # What dig reads, bury sets: the last argument is the value and the
263
384
  # rest are the path to it. The document comes back changed rather
264
385
  # than being written anywhere, which update_all is for.
@@ -266,12 +387,22 @@ module ActiveRecord
266
387
  JsonSet.new(self, path, value)
267
388
  end
268
389
 
390
+ # Whether the document contains the Ruby document given, which SQL calls containment: `@>` on PostgreSQL, `JSON_CONTAINS` on MySQL. SQLite and MariaDB have none.
391
+ # @return [AST::Predicate]
392
+ # @example
393
+ # Doc.where { :meta.contains?(author: { name: "alice" }) }
394
+ #
269
395
  # Whether the document holds what is given, which SQL calls
270
396
  # containment. SQLite has no equivalent.
271
397
  def contains?(value)
272
398
  JsonContains.new(self, value)
273
399
  end
274
400
 
401
+ # Whether the object has the key, as Hash#key? asks.
402
+ # @return [AST::Predicate]
403
+ # @example
404
+ # Doc.where { :meta.key?(:author) }
405
+ #
275
406
  # Whether the key is there at all, as Hash#key? asks. Hash has
276
407
  # has_key? too; one name is enough, and this is the one Ruby's own
277
408
  # style prefers.
@@ -279,32 +410,55 @@ module ActiveRecord
279
410
  JsonHasKey.new(self, key)
280
411
  end
281
412
 
413
+ # The keys of the object as a JSON array, as Hash#keys gives them. Oracle has none.
414
+ # @return [AST::JsonKeys]
415
+ #
282
416
  # The keys of the document, as Hash#keys gives them: a JSON array.
283
417
  def keys
284
418
  JsonKeys.new(self)
285
419
  end
286
420
  end
287
421
 
422
+ # The arithmetic and the bitwise operators on a column or an
423
+ # expression. Ruby puts all of them above the comparisons, so
424
+ # `:price * :quantity > 100` groups the way it reads, and a number on
425
+ # the left -- `20 - :quantity` -- builds the same expression.
426
+ #
427
+ # @example
428
+ # LineItem.where { :price * :quantity > 1000 }
429
+ # LineItem.select { (:flags & 4).as(:featured) }
430
+ #
288
431
  # Arithmetic builders shared by symbols, qualified columns and
289
432
  # expressions. Imported into the Symbol refinement like Predications,
290
433
  # so every method must be defined with def.
291
434
  module Arithmetics
435
+ # `+`; with an Active Support duration on the right, a date moved: `:due_on + 3.days`.
436
+ # @return [AST::Arithmetic]
292
437
  def +(other)
293
438
  Arithmetic.new(self, :+, other)
294
439
  end
295
440
 
441
+ # `-`; with a duration on the right, a date moved back.
442
+ # @return [AST::Arithmetic]
296
443
  def -(other)
297
444
  Arithmetic.new(self, :-, other)
298
445
  end
299
446
 
447
+ # `*`.
448
+ # @return [AST::Arithmetic]
300
449
  def *(other)
301
450
  Arithmetic.new(self, :*, other)
302
451
  end
303
452
 
453
+ # `/`.
454
+ # @return [AST::Arithmetic]
304
455
  def /(other)
305
456
  Arithmetic.new(self, :/, other)
306
457
  end
307
458
 
459
+ # Bitwise AND. `&` between two conditions is AND, which leaves this free to mean the SQL operator.
460
+ # @return [AST::Bitwise]
461
+ #
308
462
  # SQL's bitwise operators. & and | are AND and OR between conditions
309
463
  # and are defined there, which is what leaves them free to mean here
310
464
  # what SQL means by them. Ruby's precedence puts all six above the
@@ -313,22 +467,32 @@ module ActiveRecord
313
467
  Bitwise.new(self, :&, other)
314
468
  end
315
469
 
470
+ # Bitwise OR.
471
+ # @return [AST::Bitwise]
316
472
  def |(other)
317
473
  Bitwise.new(self, :|, other)
318
474
  end
319
475
 
476
+ # Bitwise XOR: `#` on PostgreSQL, `^` on MySQL, and the two operations it is made of on SQLite.
477
+ # @return [AST::Bitwise]
320
478
  def ^(other)
321
479
  Bitwise.new(self, :^, other)
322
480
  end
323
481
 
482
+ # A shift left.
483
+ # @return [AST::Bitwise]
324
484
  def <<(other)
325
485
  Bitwise.new(self, :<<, other)
326
486
  end
327
487
 
488
+ # A shift right.
489
+ # @return [AST::Bitwise]
328
490
  def >>(other)
329
491
  Bitwise.new(self, :>>, other)
330
492
  end
331
493
 
494
+ # Bitwise NOT.
495
+ # @return [AST::BitwiseNot]
332
496
  def ~
333
497
  BitwiseNot.new(self)
334
498
  end
@@ -339,6 +503,7 @@ module ActiveRecord
339
503
  # a column or an expression on the right means a query; anything else
340
504
  # goes back to the number through super, so 1 + 2 is 3 inside a block
341
505
  # too.
506
+ # @private
342
507
  module NumericArithmetics
343
508
  def +(other)
344
509
  return super unless other.is_a?(::Symbol) || other.is_a?(Node)
@@ -396,18 +561,31 @@ module ActiveRecord
396
561
  raise ScriptError, "subclass must override this method"
397
562
  end
398
563
 
564
+ # The expression under an alias, as {BlockSyntax#as} gives a column
565
+ # one.
566
+ # @return [AST::As]
399
567
  def as(alias_name, quote: true)
400
568
  As.new(self, alias_name, quote: quote)
401
569
  end
402
570
 
571
+ # An ascending ordering by the expression.
572
+ # @return [AST::Ordering]
403
573
  def asc
404
574
  Ordering.new(self, :asc)
405
575
  end
406
576
 
577
+ # A descending ordering by the expression.
578
+ # @return [AST::Ordering]
407
579
  def desc
408
580
  Ordering.new(self, :desc)
409
581
  end
410
582
 
583
+ # The expression under a collation, as {BlockSyntax#collate}.
584
+ # @return [AST::Collate]
585
+ def collate(name)
586
+ Collate.new(self, name)
587
+ end
588
+
411
589
  private
412
590
  # Resolves an operand denoting a column or an expression. A number
413
591
  # rides along for Arel to write out, which it can do for Integer and
@@ -561,16 +739,23 @@ module ActiveRecord
561
739
  @default = default
562
740
  end
563
741
 
742
+ # The next `WHEN`: a value to compare the operand against, or a condition as a value or a block.
743
+ # @return [AST::Case::When]
564
744
  def when(value = nil, &block)
565
745
  Pending.new(self, Case.argument(:when, value, block))
566
746
  end
567
747
 
748
+ # `THEN`, which belongs after a `when`; here it says so.
749
+ # @raise [ArgumentError]
750
+ #
568
751
  # Kernel#then is on every object, so `then` in the wrong place would be
569
752
  # answered by it -- with no block, silently, with an Enumerator.
570
753
  def then(*)
571
754
  raise ArgumentError, "then follows a when, and there is none to follow here"
572
755
  end
573
756
 
757
+ # `ELSE value`, as a value or a block, closing the CASE. Without one the CASE gives NULL where no `when` matched.
758
+ # @return [AST::Case]
574
759
  def else(value = nil, &block)
575
760
  Case.new(operand, whens, Case.argument(:else, value, block))
576
761
  end
@@ -609,6 +794,8 @@ module ActiveRecord
609
794
  @condition = condition
610
795
  end
611
796
 
797
+ # `THEN value`, as a value or a block, for the `when` before it.
798
+ # @return [AST::Case]
612
799
  def then(value = nil, &block)
613
800
  Case.new(@kase.operand,
614
801
  @kase.whens + [[@condition, Case.argument(:then, value, block)]],
@@ -774,27 +961,8 @@ module ActiveRecord
774
961
  end
775
962
 
776
963
  def to_arel(_table, model)
777
- json = Arel::Nodes.build_quoted(JSON.generate(value))
778
- case AST.adapter_family(model)
779
- when :postgresql then json
780
- when :mysql then mysql_literal(json, model)
781
- else refuse(model.connection_db_config.adapter)
782
- end
964
+ Dialect.for(model).json_literal(Arel::Nodes.build_quoted(JSON.generate(value)), model)
783
965
  end
784
-
785
- private
786
- # MariaDB answers to the same adapter and has no JSON type at all.
787
- def mysql_literal(json, model)
788
- refuse("MariaDB") if model.with_connection { |c| c.mariadb? }
789
- Arel::Nodes::NamedFunction.new(
790
- "CAST", [Arel::Nodes::As.new(json, Arel::Nodes::SqlLiteral.new("JSON"))])
791
- end
792
-
793
- def refuse(database)
794
- raise NotImplementedError,
795
- "a JSON comparison has no equivalent on #{database}; " \
796
- "dig_text gives the value"
797
- end
798
966
  end
799
967
 
800
968
  # The JSON operations read a document, and what dig gives is one:
@@ -851,24 +1019,9 @@ module ActiveRecord
851
1019
  end
852
1020
 
853
1021
  def to_arel(table, model)
854
- document = to_arel_operand(operand, table, model)
855
- case AST.adapter_family(model)
856
- when :postgresql
857
- Arel::Nodes::InfixOperation.new(
858
- json_value? ? :"#>" : :"#>>", document, Arel::Nodes.build_quoted(steps_array))
859
- when :mysql
860
- extracted = Arel::Nodes::NamedFunction.new(
861
- "JSON_EXTRACT", [document, Arel::Nodes.build_quoted(dollar_path)])
862
- json_value? ? extracted : Arel::Nodes::NamedFunction.new("JSON_UNQUOTE", [extracted])
863
- else
864
- extracted = Arel::Nodes::InfixOperation.new(
865
- json_value? ? :"->" : :"->>", document, Arel::Nodes.build_quoted(dollar_path))
866
- # SQLite's ->> gives back the value with its type, where the other
867
- # two give text. Cast so that `dig_text(:n) == '5'` means the
868
- # same thing everywhere, and a number wants a cast everywhere too.
869
- json_value? ? extracted : Arel::Nodes::NamedFunction.new(
870
- "CAST", [Arel::Nodes::As.new(extracted, Arel::Nodes::SqlLiteral.new("text"))])
871
- end
1022
+ Dialect.for(model).json_path(
1023
+ to_arel_operand(operand, table, model),
1024
+ dollar_path, steps_array, json_value?, model)
872
1025
  end
873
1026
 
874
1027
  private
@@ -899,40 +1052,13 @@ module ActiveRecord
899
1052
  end
900
1053
 
901
1054
  def to_arel(table, model)
902
- document = to_arel_operand(operand, table, model)
903
- if AST.adapter_family(model) == :postgresql
904
- Arel::Nodes::NamedFunction.new(
905
- "jsonb_set",
906
- [document, Arel::Nodes.build_quoted(steps_array), postgresql_value(table, model)])
907
- else
908
- Arel::Nodes::NamedFunction.new(
909
- "JSON_SET",
910
- [document, Arel::Nodes.build_quoted(dollar_path), other_value(table, model)])
911
- end
1055
+ Dialect.for(model).json_set(
1056
+ to_arel_operand(operand, table, model),
1057
+ steps_array, dollar_path, value,
1058
+ (to_arel_operand(value, table, model) if expression?), model)
912
1059
  end
913
1060
 
914
1061
  private
915
- # jsonb_set takes jsonb, so an expression is turned into it and a Ruby
916
- # value goes in as the JSON that says it -- '"x"' rather than 'x',
917
- # which is not a document at all.
918
- def postgresql_value(table, model)
919
- return Arel::Nodes::NamedFunction.new(
920
- "to_jsonb", [to_arel_operand(value, table, model)]) if expression?
921
- Arel::Nodes.build_quoted(JSON.generate(value))
922
- end
923
-
924
- # The others take the value as it is, except a whole document or a
925
- # boolean, which go in as JSON through json_argument.
926
- def other_value(table, model)
927
- return to_arel_operand(value, table, model) if expression?
928
- unless value.is_a?(::Hash) || value.is_a?(::Array) ||
929
- value == true || value == false
930
- return Arel::Nodes.build_quoted(value)
931
- end
932
-
933
- AST.json_argument(value, model)
934
- end
935
-
936
1062
  def expression?
937
1063
  value.is_a?(Node) || value.is_a?(::Symbol)
938
1064
  end
@@ -961,31 +1087,13 @@ module ActiveRecord
961
1087
  end
962
1088
 
963
1089
  def to_arel(table, model)
964
- document = to_arel_operand(operand, table, model)
965
- if AST.adapter_family(model) == :postgresql
966
- # Grouped because - binds tighter than #>: dug out of a document,
967
- # the subtraction would otherwise take the path literal first.
968
- return Arel::Nodes::InfixOperation.new(
969
- :-, Arel::Nodes::Grouping.new(document), key_array)
970
- end
971
-
972
- Arel::Nodes::NamedFunction.new(
973
- "JSON_REMOVE",
974
- [document, *keys.map { |key| Arel::Nodes.build_quoted("$#{dollar_step(key)}") }])
1090
+ Dialect.for(model).json_remove(
1091
+ to_arel_operand(operand, table, model),
1092
+ keys.map { |key| "$#{dollar_step(key)}" },
1093
+ steps_array(keys), model)
975
1094
  end
976
1095
 
977
1096
  private
978
- # jsonb has three subtractions -- a key, an array of keys, an element
979
- # by index -- and an array literal written without a type is read as
980
- # the first of them: `meta - '{draft}'` takes out the key spelled
981
- # {draft}, which is nothing, and says nothing about it.
982
- def key_array
983
- Arel::Nodes::NamedFunction.new(
984
- "CAST",
985
- [Arel::Nodes::As.new(Arel::Nodes.build_quoted(steps_array(keys)),
986
- Arel::Nodes::SqlLiteral.new("text[]"))])
987
- end
988
-
989
1097
  # Keys, as Hash#except takes them: an index into an array is not what
990
1098
  # the name says anywhere, and is bury's business through a path.
991
1099
  def check_keys(keys)
@@ -1013,17 +1121,9 @@ module ActiveRecord
1013
1121
  end
1014
1122
 
1015
1123
  def to_arel(table, model)
1016
- document = to_arel_operand(operand, table, model)
1017
- json = Arel::Nodes.build_quoted(JSON.generate(value))
1018
- case AST.adapter_family(model)
1019
- when :postgresql then Arel::Nodes::Contains.new(document, json)
1020
- when :mysql
1021
- Arel::Nodes::NamedFunction.new("JSON_CONTAINS", [document, json])
1022
- else
1023
- # Later than the others, since the adapter is only known here.
1024
- raise NotImplementedError,
1025
- "contains? has no equivalent on #{model.connection_db_config.adapter}"
1026
- end
1124
+ Dialect.for(model).json_contains(
1125
+ to_arel_operand(operand, table, model),
1126
+ Arel::Nodes.build_quoted(JSON.generate(value)), model)
1027
1127
  end
1028
1128
  end
1029
1129
 
@@ -1041,18 +1141,10 @@ module ActiveRecord
1041
1141
  end
1042
1142
 
1043
1143
  def to_arel(table, model)
1044
- document = to_arel_operand(operand, table, model)
1045
- name = Arel::Nodes.build_quoted(key.to_s)
1046
- path = Arel::Nodes.build_quoted("$.#{key}")
1047
- case AST.adapter_family(model)
1048
- when :postgresql
1049
- Arel::Nodes::InfixOperation.new(:"?", document, name)
1050
- when :mysql
1051
- Arel::Nodes::NamedFunction.new(
1052
- "JSON_CONTAINS_PATH", [document, Arel::Nodes.build_quoted("one"), path])
1053
- else
1054
- Arel::Nodes::NamedFunction.new("json_type", [document, path]).not_eq(nil)
1055
- end
1144
+ Dialect.for(model).json_has_key(
1145
+ to_arel_operand(operand, table, model),
1146
+ Arel::Nodes.build_quoted(key.to_s),
1147
+ Arel::Nodes.build_quoted("$.#{key}"), model)
1056
1148
  end
1057
1149
  end
1058
1150
 
@@ -1075,30 +1167,10 @@ module ActiveRecord
1075
1167
  end
1076
1168
 
1077
1169
  def to_arel(table, model)
1078
- document = to_arel_operand(operand, table, model)
1079
- case AST.adapter_family(model)
1080
- when :sqlite
1081
- sql = compile(document, model)
1082
- Arel.sql("CASE WHEN json_type(#{sql}) = 'object' " \
1083
- "THEN (SELECT json_group_array(key) FROM json_each(#{sql})) END")
1084
- when :postgresql
1085
- sql = compile(document, model)
1086
- Arel.sql("CASE WHEN jsonb_typeof(#{sql}) = 'object' " \
1087
- "THEN COALESCE((SELECT jsonb_agg(k) FROM jsonb_object_keys(#{sql}) k), " \
1088
- "CAST('[]' AS jsonb)) END")
1089
- else
1090
- Arel::Nodes::NamedFunction.new("JSON_KEYS", [document])
1091
- end
1170
+ Dialect.for(model).json_keys(to_arel_operand(operand, table, model), model)
1092
1171
  end
1093
1172
 
1094
1173
  private
1095
- # The document appears more than once, the way SQLite's XOR names
1096
- # its operands twice; the connection's own visitor compiles it, so
1097
- # its quoting is the adapter's.
1098
- def compile(document, model)
1099
- model.with_connection { |connection| connection.visitor.compile(document) }
1100
- end
1101
-
1102
1174
  def json_source
1103
1175
  "keys"
1104
1176
  end
@@ -1113,11 +1185,6 @@ module ActiveRecord
1113
1185
  include JsonComparable
1114
1186
  include ComputedJson
1115
1187
 
1116
- NAMES = {
1117
- array: { postgresql: "jsonb_build_array" },
1118
- object: { postgresql: "jsonb_build_object" },
1119
- }.freeze
1120
-
1121
1188
  attr_reader :kind, :values
1122
1189
 
1123
1190
  def initialize(kind, values)
@@ -1126,39 +1193,23 @@ module ActiveRecord
1126
1193
  end
1127
1194
 
1128
1195
  def to_arel(table, model)
1129
- Arel::Nodes::NamedFunction.new(
1130
- NAMES.fetch(kind).fetch(AST.adapter_family(model)) { "JSON_#{kind.to_s.upcase}" },
1131
- arguments(table, model))
1196
+ dialect = Dialect.for(model)
1197
+ if kind == :array
1198
+ dialect.json_build(:array, nil,
1199
+ values.map { |value| build_argument(value, dialect, table, model) }, model)
1200
+ else
1201
+ dialect.json_build(:object, values.keys.map(&:to_s),
1202
+ values.values.map { |value| build_argument(value, dialect, table, model) }, model)
1203
+ end
1132
1204
  end
1133
1205
 
1134
1206
  private
1135
- def arguments(table, model)
1136
- if kind == :array
1137
- values.map { |value| build_argument(value, table, model) }
1138
- else
1139
- values.flat_map do |key, value|
1140
- [Arel::Nodes.build_quoted(key.to_s),
1141
- build_argument(value, table, model)]
1142
- end
1143
- end
1144
- end
1145
-
1146
- # An expression is itself and a document or a boolean goes in as
1147
- # JSON, as bury takes them. PostgreSQL builds from typed
1148
- # arguments, so its JSON literal is cast -- left untyped it would
1149
- # be text, and land as a string.
1150
- def build_argument(value, table, model)
1207
+ # An expression is itself and a bare scalar is quoted; a document or
1208
+ # a boolean the dialect embeds as JSON, as bury takes it.
1209
+ def build_argument(value, dialect, table, model)
1151
1210
  case value
1152
1211
  when Node, ::Symbol then to_arel_operand(value, table, model)
1153
- when ::Hash, ::Array, true, false
1154
- if AST.adapter_family(model) == :postgresql
1155
- Arel::Nodes::NamedFunction.new(
1156
- "CAST", [Arel::Nodes::As.new(
1157
- Arel::Nodes.build_quoted(JSON.generate(value)),
1158
- Arel::Nodes::SqlLiteral.new("jsonb"))])
1159
- else
1160
- AST.json_argument(value, model)
1161
- end
1212
+ when ::Hash, ::Array, true, false then dialect.json_build_argument(value, model)
1162
1213
  when ::Rational then quote_number(value)
1163
1214
  else Arel::Nodes.build_quoted(value)
1164
1215
  end
@@ -1194,6 +1245,7 @@ module ActiveRecord
1194
1245
  # Each set is a list of its own, so grouping_sets takes lists and rollup
1195
1246
  # and cube take the columns themselves.
1196
1247
  class GroupingSets < Node
1248
+ # @private
1197
1249
  KINDS = {
1198
1250
  grouping_sets: Arel::Nodes::GroupingSet,
1199
1251
  rollup: Arel::Nodes::RollUp,
@@ -1209,7 +1261,7 @@ module ActiveRecord
1209
1261
  end
1210
1262
 
1211
1263
  def to_arel(table, model)
1212
- return with_rollup(table, model) if AST.adapter_family(model) == :mysql
1264
+ return with_rollup(table, model) if Dialect.for(model).grouping_by_with_rollup?
1213
1265
 
1214
1266
  KINDS.fetch(kind).new(
1215
1267
  if kind == :grouping_sets
@@ -1256,10 +1308,25 @@ module ActiveRecord
1256
1308
  # Arithmetic on columns and expressions. Ruby's precedence puts these
1257
1309
  # above the comparison operators, so :price * :quantity > 100 groups the
1258
1310
  # way it reads.
1311
+ #
1312
+ # A Duration on the right moves a date: `:due_on + 3.days`. No two
1313
+ # families spell the move alike, so the dialect writes it, a part of the
1314
+ # duration at a time.
1259
1315
  class Arithmetic < Node
1260
1316
  include Predications
1261
1317
  include Arithmetics
1262
1318
 
1319
+ # Active Support's parts, as the units the SQL takes. A week is seven
1320
+ # days: SQLite and Oracle have no week.
1321
+ # @private
1322
+ UNITS = {
1323
+ years: :year, months: :month, weeks: :day, days: :day,
1324
+ hours: :hour, minutes: :minute, seconds: :second,
1325
+ }.freeze
1326
+
1327
+ # @private
1328
+ DATE_UNITS = %i[year month day].freeze
1329
+
1263
1330
  attr_reader :left, :operator, :right
1264
1331
 
1265
1332
  def initialize(left, operator, right)
@@ -1270,11 +1337,53 @@ module ActiveRecord
1270
1337
 
1271
1338
  def to_arel(table, model)
1272
1339
  arel_left = to_arel_operand(left, table, model)
1340
+ return move_date(arel_left, model) if right.is_a?(::ActiveSupport::Duration)
1273
1341
  # The operator dispatches Arel's Math, which a bare number carries
1274
1342
  # none of; quoted, it is a node with the same methods.
1275
1343
  arel_left = Arel::Nodes.build_quoted(arel_left) if arel_left.is_a?(::Numeric)
1276
1344
  arel_left.public_send(operator, to_arel_operand(right, table, model))
1277
1345
  end
1346
+
1347
+ # SQLite has no date type, and its datetime() gives whatever it is
1348
+ # handed a time of day, so a date column moved by a day would come
1349
+ # back a midnight and sort past the same day written bare. Its
1350
+ # dialect has date() for what is a date to begin with, and this is
1351
+ # what says so: a column the model declares a date, CURRENT_DATE, or
1352
+ # one of those already moved by a date's units. The other families
1353
+ # keep the type themselves and never ask.
1354
+ def self.date_operand?(operand, model)
1355
+ case operand
1356
+ when ::Symbol then model.type_for_attribute(operand).type == :date
1357
+ when DatetimeValueFunction then operand.name == "CURRENT_DATE"
1358
+ when Arithmetic
1359
+ operand.right.is_a?(::ActiveSupport::Duration) &&
1360
+ date_operand?(operand.left, model) &&
1361
+ operand.right.parts.keys.all? { |part| DATE_UNITS.include?(UNITS[part]) }
1362
+ else false
1363
+ end
1364
+ end
1365
+
1366
+ private
1367
+ # Each amount is written into the SQL as a number, and a fraction
1368
+ # of a unit is not one every family takes, so it has to be a whole
1369
+ # one.
1370
+ def move_date(date, model)
1371
+ unless operator == :+ || operator == :-
1372
+ raise ArgumentError,
1373
+ "a duration is added to a date or subtracted from it, not #{operator}"
1374
+ end
1375
+ dialect = Dialect.for(model)
1376
+ date_only = Arithmetic.date_operand?(left, model)
1377
+ right.parts.reduce(date) do |arel, (part, amount)|
1378
+ unless amount.is_a?(::Integer)
1379
+ raise ArgumentError, "#{amount.inspect} #{part} is not a whole number of them"
1380
+ end
1381
+ unit = UNITS.fetch(part)
1382
+ amount *= 7 if part == :weeks
1383
+ dialect.add_interval(arel, amount, unit, operator == :-,
1384
+ date_only && DATE_UNITS.include?(unit))
1385
+ end
1386
+ end
1278
1387
  end
1279
1388
 
1280
1389
  # What the bitwise operators refuse. Both refusals are there because
@@ -1310,6 +1419,7 @@ module ActiveRecord
1310
1419
  include Arithmetics
1311
1420
  include BitwiseOperands
1312
1421
 
1422
+ # @private
1313
1423
  NODES = {
1314
1424
  :& => Arel::Nodes::BitwiseAnd,
1315
1425
  :| => Arel::Nodes::BitwiseOr,
@@ -1345,14 +1455,7 @@ module ActiveRecord
1345
1455
  # it cannot be the portable one either. SQLite has no XOR at all;
1346
1456
  # (a | b) - (a & b) is it, at the cost of naming each operand twice.
1347
1457
  def xor(left, right, model)
1348
- case AST.adapter_family(model)
1349
- when :postgresql then Arel::Nodes::InfixOperation.new("#", left, right)
1350
- when :mysql then Arel::Nodes::BitwiseXor.new(left, right)
1351
- else
1352
- Arel::Nodes::Subtraction.new(
1353
- Arel::Nodes::Grouping.new(Arel::Nodes::BitwiseOr.new(left, right)),
1354
- Arel::Nodes::Grouping.new(Arel::Nodes::BitwiseAnd.new(left, right)))
1355
- end
1458
+ Dialect.for(model).bitwise_xor(left, right)
1356
1459
  end
1357
1460
  end
1358
1461
 
@@ -1380,6 +1483,12 @@ module ActiveRecord
1380
1483
  # OVER, on the two things that can carry a window: an aggregate, and a
1381
1484
  # function.
1382
1485
  module Windowing
1486
+ # `OVER ()`, an empty window to be filled by {Over#partition},
1487
+ # {Over#order}, {Over#rows} and {Over#range}.
1488
+ # @return [AST::Over]
1489
+ # @example
1490
+ # Author.select { avg(:age).over.partition(:country).as(:country_average) }
1491
+ # Post.select { sum(:likes).over.order(:created_at).rows(..0).as(:running) }
1383
1492
  def over
1384
1493
  Over.new(self)
1385
1494
  end
@@ -1401,20 +1510,30 @@ module ActiveRecord
1401
1510
  @frame = frame
1402
1511
  end
1403
1512
 
1513
+ # `PARTITION BY`, the columns or expressions given.
1514
+ # @return [AST::Over]
1404
1515
  def partition(*exprs)
1405
1516
  raise ArgumentError, "partition needs an expression" if exprs.empty?
1406
1517
  Over.new(function, partitions + exprs, orders, frame)
1407
1518
  end
1408
1519
 
1520
+ # `ORDER BY` within the window: columns, or orderings such as `:age.desc`.
1521
+ # @return [AST::Over]
1409
1522
  def order(*exprs)
1410
1523
  raise ArgumentError, "order needs an expression" if exprs.empty?
1411
1524
  Over.new(function, partitions, orders + exprs, frame)
1412
1525
  end
1413
1526
 
1527
+ # `ROWS BETWEEN`, as a range of rows counted from the current one: negative before it, positive after, `0` the row itself, an open end unbounded. `rows(..0)` is a running total, `rows(-1..1)` the row and its neighbours.
1528
+ # @param bounds [Range]
1529
+ # @return [AST::Over]
1414
1530
  def rows(bounds)
1415
1531
  Over.new(function, partitions, orders, framing(:rows, bounds))
1416
1532
  end
1417
1533
 
1534
+ # `RANGE BETWEEN`, with the bounds as {#rows} takes them.
1535
+ # @param bounds [Range]
1536
+ # @return [AST::Over]
1418
1537
  def range(bounds)
1419
1538
  Over.new(function, partitions, orders, framing(:range, bounds))
1420
1539
  end
@@ -1425,9 +1544,9 @@ module ActiveRecord
1425
1544
  orders.each { |expr| window.order(to_arel_operand(expr, table, model)) }
1426
1545
  frame_arel(window) if frame
1427
1546
 
1428
- # The JSON aggregates cannot ride a window everywhere; the node
1429
- # itself says where, once the adapter is known.
1430
- function.check_window(model) if function.is_a?(JsonAggregate)
1547
+ # Not every aggregate can ride a window everywhere; the node itself
1548
+ # says where, once the adapter is known.
1549
+ function.check_window(model) if function.respond_to?(:check_window)
1431
1550
 
1432
1551
  # A window-only function refuses to build on its own; here is where
1433
1552
  # it is asked for the call itself.
@@ -1481,11 +1600,17 @@ module ActiveRecord
1481
1600
  include Arithmetics
1482
1601
  include Windowing
1483
1602
 
1603
+ # The aggregates DISTINCT changes: over each value once, count counts
1604
+ # fewer and sum and avg reckon less. min and max give the same
1605
+ # either way, so a DISTINCT there is refused as saying nothing.
1606
+ # @private
1607
+ DISTINCT_FUNCTIONS = %i[count sum average].freeze
1608
+
1484
1609
  attr_reader :operand, :function, :distinct, :condition
1485
1610
 
1486
1611
  def initialize(operand, function, distinct: false, condition: nil)
1487
- if distinct && function != :count
1488
- raise ArgumentError, "#{function} does not take distinct"
1612
+ if distinct && !DISTINCT_FUNCTIONS.include?(function)
1613
+ raise ArgumentError, "#{function} does not take distinct; it would give the same"
1489
1614
  end
1490
1615
  if distinct && operand == :*
1491
1616
  raise ArgumentError, "count(:*) does not take distinct; name a column"
@@ -1496,8 +1621,12 @@ module ActiveRecord
1496
1621
  @condition = condition
1497
1622
  end
1498
1623
 
1499
- # FILTER (WHERE ...): the aggregate is taken over the rows the
1500
- # condition holds for. A value or a block, as `when` takes them.
1624
+ # `FILTER (WHERE condition)`: the aggregate taken over the rows the
1625
+ # condition holds for, as a value or a block. Where there is no
1626
+ # FILTER clause -- MySQL, SQL Server -- the CASE that means the same.
1627
+ # @return [AST::Aggregate]
1628
+ # @example
1629
+ # Author.select { [count(:*).as(:all), count(:*).filter { :age < 50 }.as(:young)] }
1501
1630
  def filter(condition = nil, &block)
1502
1631
  Aggregate.new(operand, function, distinct: distinct,
1503
1632
  condition: Case.argument(:filter, condition, block))
@@ -1506,11 +1635,11 @@ module ActiveRecord
1506
1635
  def to_arel(table, model)
1507
1636
  return aggregate(operand, table, model) unless condition
1508
1637
 
1509
- # MySQL has no FILTER clause. An aggregate passes over a NULL, so
1510
- # the case that yields nothing for the rows the condition misses is
1511
- # the same aggregate over the same rows -- count(*) has no operand to
1512
- # keep, and counts a 1 instead.
1513
- if AST.adapter_family(model) == :mysql
1638
+ # A family without a FILTER clause gets the CASE that means the same.
1639
+ # An aggregate passes over a NULL, so the case that yields nothing for
1640
+ # the rows the condition misses is the same aggregate over the same
1641
+ # rows -- count(*) has no operand to keep, and counts a 1 instead.
1642
+ unless Dialect.for(model).filter_supported?
1514
1643
  kept = Case.new.when(condition).then(operand == :* ? 1 : operand)
1515
1644
  return aggregate(kept, table, model)
1516
1645
  end
@@ -1521,11 +1650,10 @@ module ActiveRecord
1521
1650
  private
1522
1651
  def aggregate(over, table, model)
1523
1652
  arel_operand = to_arel_operand(over, table, model)
1524
- if function == :count
1525
- arel_operand.count(distinct)
1526
- else
1527
- arel_operand.public_send(function)
1528
- end
1653
+ return arel_operand.count(distinct) if function == :count
1654
+ call = arel_operand.public_send(function)
1655
+ call.distinct = distinct
1656
+ call
1529
1657
  end
1530
1658
  end
1531
1659
 
@@ -1540,14 +1668,6 @@ module ActiveRecord
1540
1668
  include ComputedJson
1541
1669
  include Windowing
1542
1670
 
1543
- # The standard names, which are also the DSL's own and MySQL's, serve
1544
- # any adapter the table does not list.
1545
- NAMES = {
1546
- arrayagg: { sqlite: "json_group_array", postgresql: "jsonb_agg" },
1547
- objectagg: { sqlite: "json_group_object",
1548
- postgresql: "jsonb_object_agg" },
1549
- }.freeze
1550
-
1551
1671
  attr_reader :kind, :operands, :condition
1552
1672
 
1553
1673
  def initialize(kind, operands, condition: nil)
@@ -1556,31 +1676,32 @@ module ActiveRecord
1556
1676
  @condition = condition
1557
1677
  end
1558
1678
 
1679
+ # `FILTER (WHERE condition)`, as {Aggregate#filter}; refused on the
1680
+ # MySQL family, where the CASE that stands in would leave a JSON null
1681
+ # for every row it drops.
1682
+ # @return [AST::JsonAggregate]
1559
1683
  def filter(condition = nil, &block)
1560
1684
  JsonAggregate.new(kind, operands,
1561
1685
  condition: Case.argument(:filter, condition, block))
1562
1686
  end
1563
1687
 
1564
- # MariaDB takes every other aggregate as a window function, but not
1565
- # these two; Over asks here before writing one.
1688
+ # Over asks here before writing a window, since a family may take
1689
+ # every other aggregate as one but not these two.
1566
1690
  def check_window(model)
1567
- return unless AST.adapter_family(model) == :mysql
1568
- return unless model.with_connection { |connection| connection.mariadb? }
1569
- raise NotImplementedError,
1570
- "#{json_source} over a window has no equivalent on MariaDB"
1691
+ Dialect.for(model).check_json_aggregate_window(json_source, model)
1571
1692
  end
1572
1693
 
1573
1694
  def to_arel(table, model)
1574
- family = AST.adapter_family(model)
1695
+ dialect = Dialect.for(model)
1575
1696
  call = Arel::Nodes::NamedFunction.new(
1576
- NAMES.fetch(kind).fetch(family) { "JSON_#{kind.to_s.upcase}" },
1697
+ dialect.json_aggregate_name(kind),
1577
1698
  operands.map { |operand| to_arel_argument(operand, table, model) })
1578
1699
  return call unless condition
1579
1700
 
1580
- # The CASE that stands in for FILTER elsewhere hands the aggregate
1581
- # a NULL for every row the condition misses, and these two keep a
1582
- # NULL -- as JSON null -- rather than passing over it.
1583
- if family == :mysql
1701
+ # The CASE that stands in for FILTER elsewhere hands the aggregate a
1702
+ # NULL for every row the condition misses, and these two keep a NULL
1703
+ # -- as JSON null -- rather than passing over it, so it is refused.
1704
+ unless dialect.json_aggregate_filter_supported?
1584
1705
  raise NotImplementedError,
1585
1706
  "#{json_source}.filter has no equivalent on " \
1586
1707
  "#{model.connection_db_config.adapter}; a CASE would leave a " \
@@ -1595,6 +1716,70 @@ module ActiveRecord
1595
1716
  end
1596
1717
  end
1597
1718
 
1719
+ # The strings of a group joined into one, a separator between:
1720
+ # `string_agg(:title, ", ")`, with `.order` for the order they are
1721
+ # joined in. Every family has it under a name of its own with the
1722
+ # ORDER BY in a place of its own, and Arel has no node for an ORDER BY
1723
+ # inside a call, so the dialect writes the call. A NULL is passed
1724
+ # over as by any aggregate, so the CASE that stands in for FILTER
1725
+ # means the same here and is not refused as the JSON aggregates' is.
1726
+ class StringAggregate < Node
1727
+ include Predications
1728
+ include Windowing
1729
+
1730
+ attr_reader :operand, :separator, :orders, :condition
1731
+
1732
+ def initialize(operand, separator, orders: [], condition: nil)
1733
+ unless separator.is_a?(::String)
1734
+ raise ArgumentError, "#{separator.inspect} is not a String separator"
1735
+ end
1736
+ @operand = operand
1737
+ @separator = separator
1738
+ @orders = orders
1739
+ @condition = condition
1740
+ end
1741
+
1742
+ # The order the strings are joined in: columns, or orderings such as
1743
+ # `:title.desc`.
1744
+ # @return [AST::StringAggregate]
1745
+ def order(*exprs)
1746
+ raise ArgumentError, "order needs an expression" if exprs.empty?
1747
+ StringAggregate.new(operand, separator, orders: orders + exprs, condition: condition)
1748
+ end
1749
+
1750
+ # `FILTER (WHERE condition)`, as {Aggregate#filter}.
1751
+ # @return [AST::StringAggregate]
1752
+ def filter(condition = nil, &block)
1753
+ StringAggregate.new(operand, separator, orders: orders,
1754
+ condition: Case.argument(:filter, condition, block))
1755
+ end
1756
+
1757
+ def check_window(model)
1758
+ Dialect.for(model).check_string_aggregate_window(model)
1759
+ end
1760
+
1761
+ def to_arel(table, model)
1762
+ dialect = Dialect.for(model)
1763
+ kept = condition && !dialect.filter_supported? ?
1764
+ Case.new.when(condition).then(operand) : operand
1765
+ call = dialect.string_agg(
1766
+ to_arel_argument(kept, table, model), separator,
1767
+ orders.map { |expr| to_arel_operand(expr, table, model) },
1768
+ string_operand?(model), model)
1769
+ return call unless condition && dialect.filter_supported?
1770
+ Arel::Nodes::Filter.new(call, condition.to_arel(table, model))
1771
+ end
1772
+
1773
+ private
1774
+ # Whether the operand is a column the model declares a string.
1775
+ # PostgreSQL asks, its STRING_AGG taking text and nothing else; the
1776
+ # others convert for themselves.
1777
+ def string_operand?(model)
1778
+ operand.is_a?(::Symbol) &&
1779
+ %i[string text].include?(model.type_for_attribute(operand).type)
1780
+ end
1781
+ end
1782
+
1598
1783
  # A column alias, quoted by the adapter, so that the name asked for is
1599
1784
  # the name that comes back: unquoted, PostgreSQL folds a capital away
1600
1785
  # and the other two keep it, which is one block meaning two things.
@@ -1628,6 +1813,29 @@ module ActiveRecord
1628
1813
  end
1629
1814
  end
1630
1815
 
1816
+ # A collation named for a comparison or an ordering: `:name.collate(:ci)`.
1817
+ # It stands as an expression -- compared, ordered by, selected -- and
1818
+ # gives back one of its own, so the collation carries through.
1819
+ #
1820
+ # The name follows COLLATE as a bare identifier with no Arel node of its
1821
+ # own. What names are safe turns on whether the family quotes it -- only
1822
+ # PostgreSQL does -- so the dialect checks the name as it builds the
1823
+ # clause, rather than this node holding one rule for all of them.
1824
+ class Collate < Node
1825
+ include Predications
1826
+
1827
+ attr_reader :operand, :name
1828
+
1829
+ def initialize(operand, name)
1830
+ @name = name.to_s
1831
+ @operand = operand
1832
+ end
1833
+
1834
+ def to_arel(table, model)
1835
+ Dialect.for(model).collate(to_arel_operand(operand, table, model), name, model)
1836
+ end
1837
+ end
1838
+
1631
1839
  class Ordering < Node
1632
1840
  attr_reader :operand, :direction, :nulls
1633
1841
 
@@ -1637,12 +1845,18 @@ module ActiveRecord
1637
1845
  @nulls = nulls
1638
1846
  end
1639
1847
 
1848
+ # `NULLS FIRST`; portable, since Arel emulates it where MySQL has
1849
+ # none.
1850
+ # @return [AST::Ordering]
1851
+ #
1640
1852
  # MySQL has no NULLS FIRST/LAST, but Arel emulates it there with a
1641
1853
  # leading IS NULL ordering, so these are portable.
1642
1854
  def nulls_first
1643
1855
  Ordering.new(operand, direction, :nulls_first)
1644
1856
  end
1645
1857
 
1858
+ # `NULLS LAST`.
1859
+ # @return [AST::Ordering]
1646
1860
  def nulls_last
1647
1861
  Ordering.new(operand, direction, :nulls_last)
1648
1862
  end
@@ -1798,6 +2012,7 @@ module ActiveRecord
1798
2012
  # or an Array compares against a PostgreSQL range or array column, the way
1799
2013
  # Active Record's own force_equality? types do.
1800
2014
  class Comparison < Predicate
2015
+ # @private
1801
2016
  OPERATOR_MAP = {
1802
2017
  :== => :eq, :!= => :not_eq,
1803
2018
  :> => :gt, :>= => :gteq, :< => :lt, :<= => :lteq
@@ -1848,9 +2063,8 @@ module ActiveRecord
1848
2063
  end
1849
2064
 
1850
2065
  def to_arel(table, model)
1851
- literal = value ? Arel::Nodes::True.new : Arel::Nodes::False.new
1852
- Arel::Nodes::InfixOperation.new(negated ? "IS NOT" : "IS",
1853
- to_arel_operand(operand, table, model), literal)
2066
+ Dialect.for(model).truth_value(
2067
+ to_arel_operand(operand, table, model), value, negated, model)
1854
2068
  end
1855
2069
  end
1856
2070
 
@@ -1939,12 +2153,12 @@ module ActiveRecord
1939
2153
  # they resolve.
1940
2154
  def json_between?(model)
1941
2155
  (values.begin.is_a?(JsonLiteral) || values.end.is_a?(JsonLiteral)) &&
1942
- AST.adapter_family(model) == :mysql
2156
+ Dialect.for(model).json_list_by_element?
1943
2157
  end
1944
2158
 
1945
2159
  def json_list?(model)
1946
2160
  values.any? { |value| value.is_a?(JsonLiteral) } &&
1947
- AST.adapter_family(model) == :mysql
2161
+ Dialect.for(model).json_list_by_element?
1948
2162
  end
1949
2163
 
1950
2164
  def json_list(arel_operand, elements)
@@ -2003,6 +2217,7 @@ module ActiveRecord
2003
2217
  end
2004
2218
 
2005
2219
  class Like < Predicate
2220
+ # @private
2006
2221
  ESCAPE = "\\"
2007
2222
 
2008
2223
  # Escapes % and _ so that they match literally. The pattern built from