activerecord-refined 0.10.2 → 0.12.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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -1
  3. data/README.md +19 -22
  4. data/activerecord-refined.gemspec +4 -3
  5. data/docs/conditions.md +3 -1
  6. data/docs/ctes.md +2 -2
  7. data/docs/expressions.md +37 -20
  8. data/docs/functions.md +14 -10
  9. data/docs/index.md +2 -17
  10. data/docs/windows.md +2 -2
  11. data/examples/expressions.rb +21 -15
  12. data/lib/active_record/refined/ast/arithmetics.rb +137 -0
  13. data/lib/active_record/refined/ast/conditions.rb +451 -0
  14. data/lib/active_record/refined/ast/expressions.rb +304 -0
  15. data/lib/active_record/refined/ast/functions.rb +254 -0
  16. data/lib/active_record/refined/ast/grouping.rb +63 -0
  17. data/lib/active_record/refined/ast/json.rb +530 -0
  18. data/lib/active_record/refined/ast/node.rb +298 -0
  19. data/lib/active_record/refined/ast/ordering.rb +104 -0
  20. data/lib/active_record/refined/ast/predications.rb +384 -0
  21. data/lib/active_record/refined/ast/windows.rb +126 -0
  22. data/lib/active_record/refined/ast.rb +39 -2364
  23. data/lib/active_record/refined/block_context.rb +671 -0
  24. data/lib/active_record/refined/block_syntax.rb +128 -0
  25. data/lib/active_record/refined/dialect/mysql_compat.rb +16 -0
  26. data/lib/active_record/refined/dialect/mysqlish_json_functions.rb +45 -0
  27. data/lib/active_record/refined/dialect/oracle.rb +1 -7
  28. data/lib/active_record/refined/dialect/postgresql.rb +8 -0
  29. data/lib/active_record/refined/dialect/sql_server.rb +29 -5
  30. data/lib/active_record/refined/dialect/sqlite.rb +23 -0
  31. data/lib/active_record/refined/dialect.rb +132 -66
  32. data/lib/active_record/refined/query_methods.rb +444 -0
  33. data/lib/{activerecord-refined → active_record/refined}/version.rb +3 -2
  34. data/lib/active_record/refined/writes.rb +72 -0
  35. data/lib/active_record/refined.rb +7 -1270
  36. data/lib/activerecord-refined.rb +0 -4
  37. metadata +36 -5
@@ -0,0 +1,671 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record/refined/ast"
4
+ require "active_record/refined/dialect"
5
+
6
+ module ActiveRecord
7
+ module Refined
8
+ # What a block can call: the aggregates, the functions, CASE, and the
9
+ # escape hatches. A block is evaluated with one of these as `self`, so
10
+ # its methods are called bare -- `count(:*)`, `upper(:name)` -- and each
11
+ # gives back an expression that compares, aliases and orders like a
12
+ # column does (see {BlockSyntax}).
13
+ #
14
+ # Where a function is spelled differently from one database to the next,
15
+ # the method names the one meaning and the adapter gets its own
16
+ # spelling; where a database has no equivalent, the method raises
17
+ # `NotImplementedError` as the block is read, rather than leaving the
18
+ # database to reject the SQL.
19
+ #
20
+ # @example
21
+ # Author.select { [upper(:name).as(:author), count(:*).as(:posts)] }
22
+ # Author.having { count(:*) > 1 }
23
+ class BlockContext
24
+ # The model is only consulted to learn which adapter the query is being
25
+ # built for, which is what decides how a scalar function is spelled.
26
+ # @api private
27
+ def initialize(model)
28
+ @model = model
29
+ end
30
+
31
+ # @!group Aggregates
32
+
33
+ # @!method sum(column, distinct: false)
34
+ # `SUM(column)`, or `SUM(DISTINCT column)`.
35
+ # @return [AST::Aggregate]
36
+ # @!method avg(column, distinct: false)
37
+ # `AVG(column)`, or `AVG(DISTINCT column)`.
38
+ # @return [AST::Aggregate]
39
+ # @!method min(column)
40
+ # `MIN(column)`.
41
+ # @return [AST::Aggregate]
42
+ # @!method max(column)
43
+ # `MAX(column)`.
44
+ # @return [AST::Aggregate]
45
+ # @private
46
+ AGGREGATE_FUNCTIONS = {
47
+ sum: :sum, avg: :average, min: :minimum, max: :maximum,
48
+ }.freeze
49
+
50
+ # count, sum and avg take distinct: true, for the aggregate over each
51
+ # value once; min and max would answer the same with or without it,
52
+ # so they take no such thing.
53
+ AGGREGATE_FUNCTIONS.each do |name, arel_func|
54
+ if AST::Aggregate::DISTINCT_FUNCTIONS.include?(arel_func)
55
+ define_method(name) do |column, distinct: false|
56
+ AST::Aggregate.new(column, arel_func, distinct: distinct)
57
+ end
58
+ else
59
+ define_method(name) { |column| AST::Aggregate.new(column, arel_func) }
60
+ end
61
+ end
62
+
63
+ # `COUNT(column)`; `:*` for `COUNT(*)`, `distinct: true` for
64
+ # `COUNT(DISTINCT column)`. Every aggregate takes {AST::Aggregate#filter}
65
+ # for the rows it is taken over, and {AST::Windowing#over} for a window.
66
+ # @param column [Symbol, AST::Node, :*]
67
+ # @return [AST::Aggregate]
68
+ # @example
69
+ # Author.group { :country }.having { count(:*) > 1 }
70
+ # Post.select { count(:author_id, distinct: true) }
71
+ # Author.select { count(:*).filter { :age < 50 }.as(:young) }
72
+ def count(column, distinct: false)
73
+ AST::Aggregate.new(column, :count, distinct: distinct)
74
+ end
75
+
76
+ # The rows of a group gathered into one JSON array, a value from each:
77
+ # `jsonb_agg` on PostgreSQL, `json_group_array` on SQLite,
78
+ # `JSON_ARRAYAGG` on the MySQL family and Oracle; SQL Server has none.
79
+ # What it gives is JSON, which compares as a dug value does.
80
+ # @return [AST::JsonAggregate]
81
+ # @example
82
+ # Post.group { :author_id }.select { json_arrayagg(:title).as(:titles) }
83
+ def json_arrayagg(value)
84
+ AST::JsonAggregate.new(:arrayagg, [value])
85
+ end
86
+
87
+ # The rows of a group gathered into one JSON object, a key and a value
88
+ # from each, named as {#json_arrayagg} is; SQL Server has none.
89
+ # @return [AST::JsonAggregate]
90
+ # @example
91
+ # Post.select { json_objectagg(:title, :meta.dig(:stars)).as(:stars) }
92
+ def json_objectagg(key, value)
93
+ AST::JsonAggregate.new(:objectagg, [key, value])
94
+ end
95
+
96
+ # The strings of a group joined into one, a separator between:
97
+ # `STRING_AGG` on PostgreSQL and SQL Server, `group_concat` on SQLite,
98
+ # `GROUP_CONCAT` on MySQL, `LISTAGG` on Oracle. Takes
99
+ # {AST::StringAggregate#order} for the order they are joined in.
100
+ # @param separator [String] the comma GROUP_CONCAT defaults to, unless given
101
+ # @return [AST::StringAggregate]
102
+ # @example
103
+ # Post.group { :author_id }.
104
+ # select { string_agg(:title, ", ").order(:title).as(:titles) }
105
+ def string_agg(value, separator = ",")
106
+ AST::StringAggregate.new(value, separator)
107
+ end
108
+
109
+ # @!endgroup
110
+ # @!group JSON
111
+
112
+ # A JSON array built in the row from the values given. SQL Server
113
+ # spells the pair its own way and is not carried yet.
114
+ # @return [AST::JsonBuild]
115
+ # @example
116
+ # Post.select { json_array(:title, :likes).as(:pair) }
117
+ def json_array(*values)
118
+ AST::JsonBuild.new(:array, values)
119
+ end
120
+
121
+ # A JSON object built in the row from a hash whose values are
122
+ # expressions. SQL Server spells the pair its own way and is not
123
+ # carried yet.
124
+ # @param pairs [Hash{Symbol, String => Object}]
125
+ # @return [AST::JsonBuild]
126
+ # @example
127
+ # Post.select { json_object(title: :title, stars: :meta.dig(:stars)).as(:doc) }
128
+ def json_object(pairs = {})
129
+ AST::JsonBuild.new(:object, pairs)
130
+ end
131
+
132
+ # @!endgroup
133
+ # @!group Scalar functions
134
+
135
+ # @!method abs(x)
136
+ # `ABS(x)`.
137
+ # @return [AST::Function]
138
+ # @!method acos(x)
139
+ # `ACOS(x)`.
140
+ # @return [AST::Function]
141
+ # @!method asin(x)
142
+ # `ASIN(x)`.
143
+ # @return [AST::Function]
144
+ # @!method atan(x)
145
+ # `ATAN(x)`.
146
+ # @return [AST::Function]
147
+ # @!method atan2(y, x)
148
+ # `ATAN2(y, x)`: `ATN2` on SQL Server.
149
+ # @return [AST::Function]
150
+ # @!method ceil(x)
151
+ # `CEIL(x)`: `CEILING` on SQL Server.
152
+ # @return [AST::Function]
153
+ # @!method coalesce(*values)
154
+ # `COALESCE(a, b, ...)`: the first that is not NULL.
155
+ # @return [AST::Function]
156
+ # @!method concat(*strings)
157
+ # `CONCAT(a, b, ...)`. Oracle's takes exactly two.
158
+ # @return [AST::Function]
159
+ # @!method cos(x)
160
+ # `COS(x)`.
161
+ # @return [AST::Function]
162
+ # @!method exp(x)
163
+ # `EXP(x)`.
164
+ # @return [AST::Function]
165
+ # @!method floor(x)
166
+ # `FLOOR(x)`.
167
+ # @return [AST::Function]
168
+ # @!method length(string)
169
+ # `LENGTH(string)`: `LEN` on SQL Server. What it counts is the
170
+ # family's own -- bytes on MySQL, characters elsewhere, and `LEN`
171
+ # leaves trailing spaces out; {#char_length} is the portable count.
172
+ # @return [AST::Function]
173
+ # @!method ln(x)
174
+ # `LN(x)`: `LOG` on SQL Server.
175
+ # @return [AST::Function]
176
+ # @!method log(base, x)
177
+ # `LOG(base, x)`. SQL Server takes the arguments the other way round, and is refused.
178
+ # @return [AST::Function]
179
+ # @!method lower(string)
180
+ # `LOWER(string)`.
181
+ # @return [AST::Function]
182
+ # @!method ltrim(string)
183
+ # `LTRIM(string)`.
184
+ # @return [AST::Function]
185
+ # @!method mod(x, y)
186
+ # `MOD(x, y)`. SQL Server has only the % operator.
187
+ # @return [AST::Function]
188
+ # @!method nullif(x, y)
189
+ # `NULLIF(x, y)`: NULL where the two are equal, x otherwise.
190
+ # @return [AST::Function]
191
+ # @!method power(x, y)
192
+ # `POWER(x, y)`.
193
+ # @return [AST::Function]
194
+ # @!method replace(string, from, to)
195
+ # `REPLACE(string, from, to)`.
196
+ # @return [AST::Function]
197
+ # @!method round(x, places = 0)
198
+ # `ROUND(x, places)`.
199
+ # @return [AST::Function]
200
+ # @!method rtrim(string)
201
+ # `RTRIM(string)`.
202
+ # @return [AST::Function]
203
+ # @!method sign(x)
204
+ # `SIGN(x)`.
205
+ # @return [AST::Function]
206
+ # @!method sin(x)
207
+ # `SIN(x)`.
208
+ # @return [AST::Function]
209
+ # @!method sqrt(x)
210
+ # `SQRT(x)`.
211
+ # @return [AST::Function]
212
+ # @!method substr(string, from, length = nil)
213
+ # `SUBSTR(string, from, length)`: `SUBSTRING` on SQL Server, which insists on the length.
214
+ # @return [AST::Function]
215
+ # @!method tan(x)
216
+ # `TAN(x)`.
217
+ # @return [AST::Function]
218
+ # @!method trim(string)
219
+ # `TRIM(string)`.
220
+ # @return [AST::Function]
221
+ # @!method upper(string)
222
+ # `UPPER(string)`.
223
+ # @return [AST::Function]
224
+ # @!method degrees(x)
225
+ # `DEGREES(x)`. Oracle has none.
226
+ # @return [AST::Function]
227
+ # @!method radians(x)
228
+ # `RADIANS(x)`. Oracle has none.
229
+ # @return [AST::Function]
230
+ # @!method pi
231
+ # `PI()`. Oracle has none.
232
+ # @return [AST::Function]
233
+ # @!method char_length(string)
234
+ # `CHAR_LENGTH(string)`: `LENGTH` on SQLite and Oracle, `LEN` on SQL Server.
235
+ # @return [AST::Function]
236
+ # @!method greatest(*values)
237
+ # `GREATEST(a, b, ...)`: `MAX` on SQLite.
238
+ # @return [AST::Function]
239
+ # @!method least(*values)
240
+ # `LEAST(a, b, ...)`: `MIN` on SQLite.
241
+ # @return [AST::Function]
242
+ # @!method log2(x)
243
+ # `LOG2(x)`. PostgreSQL and Oracle have none -- `log(2, x)` is their spelling -- and SQL Server has neither.
244
+ # @return [AST::Function]
245
+ # @!method log10(x)
246
+ # `LOG10(x)`. Oracle has none.
247
+ # @return [AST::Function]
248
+ # @!method trunc(x, places = 0)
249
+ # `TRUNC(x, places)`: `TRUNCATE` on MySQL, which insists on the places. SQL Server has none.
250
+ # @return [AST::Function]
251
+ # @!method now
252
+ # `NOW()`. SQLite, Oracle and SQL Server have none; {#current_timestamp} reaches all three.
253
+ # @return [AST::Function]
254
+ # @!method bit_and(column)
255
+ # `BIT_AND(column)`, an aggregate. PostgreSQL and MySQL have it.
256
+ # @return [AST::Function]
257
+ # @!method bit_or(column)
258
+ # `BIT_OR(column)`, an aggregate. PostgreSQL and MySQL have it.
259
+ # @return [AST::Function]
260
+ # @!method bit_xor(column)
261
+ # `BIT_XOR(column)`, an aggregate. PostgreSQL and MySQL have it.
262
+ # @return [AST::Function]
263
+ # @!method date_trunc(field, timestamp)
264
+ # `date_trunc('day', timestamp)`. PostgreSQL has it; the others do not.
265
+ # @return [AST::Function]
266
+ # @!method rand
267
+ # `RAND()`, a random number per row: `RANDOM()` on PostgreSQL and SQLite. Oracle and SQL Server have none.
268
+ # @return [AST::Function]
269
+ # @!method format(template, *values)
270
+ # printf-style `FORMAT(template, ...)`. PostgreSQL and SQLite have it; MySQL's FORMAT is a different function, reached through {#fn}.
271
+ # @return [AST::Function]
272
+ #
273
+ # Scalar functions, defined as real methods so that a typo is a
274
+ # NoMethodError and a name Kernel also answers to (format, hash, test)
275
+ # cannot quietly mean something else. Where one is spelled other than as
276
+ # its plain upper-cased name, and where a family has no equivalent, is
277
+ # the dialect's to say; here is only the list of them.
278
+ # @private
279
+ SCALAR_FUNCTIONS = %i[
280
+ abs acos asin atan atan2 ceil coalesce concat cos exp floor length ln
281
+ log lower ltrim mod nullif power replace round rtrim sign sin sqrt
282
+ substr tan trim upper degrees radians pi char_length greatest least
283
+ log2 log10 trunc now bit_and bit_or bit_xor date_trunc rand format
284
+ ].freeze
285
+
286
+ SCALAR_FUNCTIONS.each do |name|
287
+ define_method(name) do |*args|
288
+ AST::Function.new(dialect.function_name(name, @model), args)
289
+ end
290
+ end
291
+
292
+ # @!endgroup
293
+ # @!group Datetime value functions
294
+
295
+ # @!method current_timestamp(precision = nil)
296
+ # `CURRENT_TIMESTAMP`, the server's clock in the session's zone; the
297
+ # portable spelling of what {#now} means. A precision --
298
+ # `current_timestamp(3)` -- goes into parentheses, which SQLite and
299
+ # SQL Server refuse.
300
+ # @return [AST::DatetimeValueFunction]
301
+ # @example
302
+ # Post.where { :published_at <= current_timestamp }
303
+ # Post.where { :created_at > current_timestamp - 7.days }
304
+ # @!method current_time(precision = nil)
305
+ # `CURRENT_TIME`. SQL Server has none.
306
+ # @return [AST::DatetimeValueFunction]
307
+ # @!method localtime(precision = nil)
308
+ # `LOCALTIME`. SQLite and SQL Server have none.
309
+ # @return [AST::DatetimeValueFunction]
310
+ # @!method localtimestamp(precision = nil)
311
+ # `LOCALTIMESTAMP`. SQLite and SQL Server have none.
312
+ # @return [AST::DatetimeValueFunction]
313
+ #
314
+ # The datetime value functions, as the SQL grammar calls them. These
315
+ # the grammar has bare -- PostgreSQL and SQLite reject them written with
316
+ # parentheses -- and the one thing that does go into parentheses is an
317
+ # optional precision, current_timestamp(3), which current_date never
318
+ # takes and SQLite never accepts. The table reads like
319
+ # SCALAR_FUNCTIONS; current_timestamp is the portable spelling of what
320
+ # now means, reaching SQLite where now does not.
321
+ # @private
322
+ DATETIME_VALUE_FUNCTIONS = %i[
323
+ current_date current_time current_timestamp localtime localtimestamp
324
+ ].freeze
325
+
326
+ # `CURRENT_DATE`, today in the session's zone -- UTC where Active
327
+ # Record has set it so. Takes no precision. SQL Server has none.
328
+ # @return [AST::DatetimeValueFunction]
329
+ # @example
330
+ # Task.where { :due_on < current_date }
331
+ def current_date
332
+ AST::DatetimeValueFunction.new(dialect.function_name(:current_date, @model))
333
+ end
334
+
335
+ (DATETIME_VALUE_FUNCTIONS - [:current_date]).each do |name|
336
+ define_method(name) do |precision = nil|
337
+ # Built first so that a precision of the wrong type is an
338
+ # ArgumentError on every adapter, before SQLite gets to say it takes
339
+ # none at all.
340
+ node = AST::DatetimeValueFunction.new(
341
+ dialect.function_name(name, @model), precision)
342
+ if precision && !dialect.datetime_precision_supported?
343
+ raise NotImplementedError,
344
+ "#{name} takes no precision on #{@model.connection_db_config.adapter}"
345
+ end
346
+ node
347
+ end
348
+ end
349
+
350
+ # `EXTRACT(field FROM expr)`: a year, a month, a day of a date. The
351
+ # field is a keyword and has to be a plain name. SQLite and SQL Server
352
+ # have none.
353
+ # @param field [Symbol, String] `:year`, `:month`, `:day`, `:hour`, ...
354
+ # @return [AST::Extract]
355
+ # @example
356
+ # Post.where { extract(:year, :created_at) == 2026 }
357
+ #
358
+ # The field is a keyword, not a value, so it has to be a plain name;
359
+ # the node checks it. SQLite spells all of this as strftime formats,
360
+ # which no renaming carries, so it raises there -- after the node is
361
+ # built, so that a bad field is an ArgumentError on every adapter.
362
+ def extract(field, expr)
363
+ node = AST::Extract.new(field, expr)
364
+ unless dialect.extract_supported?
365
+ raise NotImplementedError,
366
+ "extract has no equivalent on #{@model.connection_db_config.adapter}"
367
+ end
368
+ node
369
+ end
370
+
371
+ # @!endgroup
372
+ # @!group Grouping
373
+
374
+ # `GROUP BY GROUPING SETS ((a), (b), ())`: several groupings in one
375
+ # query, an empty set for the grand total. PostgreSQL has it; the
376
+ # others do not.
377
+ # @param sets [Array<Array<Symbol, AST::Node>>]
378
+ # @return [AST::GroupingSets]
379
+ # @example
380
+ # Sale.group { grouping_sets([:region], [:product], []) }
381
+ #
382
+ # Arel has the nodes and writes them for PostgreSQL alone, so what it
383
+ # would raise elsewhere says nothing; this says it here, as extract
384
+ # does, while the block is being read.
385
+ def grouping_sets(*sets)
386
+ grouping(:grouping_sets, sets)
387
+ end
388
+
389
+ # `GROUP BY ROLLUP (a, b)`: subtotals up the list and a grand total.
390
+ # PostgreSQL has it, and the MySQL family as `WITH ROLLUP` trailing
391
+ # the group list, which the node spells there.
392
+ # @return [AST::GroupingSets]
393
+ # @example
394
+ # Sale.group { rollup(:region, :product) }
395
+ def rollup(*columns)
396
+ grouping(:rollup, columns)
397
+ end
398
+
399
+ # `GROUP BY CUBE (a, b)`: every subtotal there is. PostgreSQL has it;
400
+ # the others do not.
401
+ # @return [AST::GroupingSets]
402
+ # @example
403
+ # Sale.group { cube(:region, :product) }
404
+ def cube(*columns)
405
+ grouping(:cube, columns)
406
+ end
407
+
408
+ # @!endgroup
409
+ # @!group Conversions
410
+
411
+ # `CAST(expr AS type)`. The type is the adapter's own name for it --
412
+ # `decimal(10,2)`, `double precision` -- and has to look like one;
413
+ # whether it exists is the database's to say.
414
+ # @param type [Symbol, String]
415
+ # @return [AST::Cast]
416
+ # @example
417
+ # Post.select { cast(:price, "decimal(10,2)").as(:price) }
418
+ def cast(expr, type)
419
+ AST::Cast.new(expr, type)
420
+ end
421
+
422
+ # @!endgroup
423
+ # @!group Window functions
424
+
425
+ # @!method row_number
426
+ # `ROW_NUMBER()`. Means nothing without {AST::Windowing#over}, and
427
+ # says so.
428
+ # @return [AST::WindowFunction]
429
+ # @example
430
+ # Author.select { row_number.over.partition(:country).order(:age.desc).as(:rank) }
431
+ # @!method rank
432
+ # `RANK()`; needs `over`.
433
+ # @return [AST::WindowFunction]
434
+ # @!method dense_rank
435
+ # `DENSE_RANK()`; needs `over`.
436
+ # @return [AST::WindowFunction]
437
+ # @!method percent_rank
438
+ # `PERCENT_RANK()`; needs `over`.
439
+ # @return [AST::WindowFunction]
440
+ # @!method cume_dist
441
+ # `CUME_DIST()`; needs `over`.
442
+ # @return [AST::WindowFunction]
443
+ # @!method ntile(buckets)
444
+ # `NTILE(buckets)`; needs `over`.
445
+ # @return [AST::WindowFunction]
446
+ # @!method first_value(expr)
447
+ # `FIRST_VALUE(expr)`; needs `over`.
448
+ # @return [AST::WindowFunction]
449
+ # @!method last_value(expr)
450
+ # `LAST_VALUE(expr)`; needs `over`.
451
+ # @return [AST::WindowFunction]
452
+ #
453
+ # The functions that only mean anything with a window. Every adapter
454
+ # that has window functions at all spells these the same -- PostgreSQL,
455
+ # MySQL 8, SQLite 3.25 -- so unlike the scalar functions there is nothing
456
+ # here to translate. Each says so if `over` never arrives.
457
+ %i[row_number rank dense_rank percent_rank cume_dist].each do |name|
458
+ define_method(name) { AST::WindowFunction.new(name.to_s.upcase, []) }
459
+ end
460
+
461
+ %i[ntile first_value last_value].each do |name|
462
+ define_method(name) { |arg| AST::WindowFunction.new(name.to_s.upcase, [arg]) }
463
+ end
464
+
465
+ # `NTH_VALUE(expr, nth)`; needs `over`.
466
+ # @return [AST::WindowFunction]
467
+ def nth_value(expr, nth)
468
+ AST::WindowFunction.new("NTH_VALUE", [expr, nth])
469
+ end
470
+
471
+ # `LAG(expr, offset, default)`: the value `offset` rows before this
472
+ # one; needs `over`.
473
+ # @return [AST::WindowFunction]
474
+ # @example
475
+ # Post.select { (:likes - lag(:likes).over.order(:created_at)).as(:gain) }
476
+ #
477
+ # The offset is written out rather than left to default, so that a
478
+ # default value cannot end up where the offset belongs.
479
+ def lag(expr, offset = 1, default = nil)
480
+ AST::WindowFunction.new("LAG", default.nil? ? [expr, offset] : [expr, offset, default])
481
+ end
482
+
483
+ # `LEAD(expr, offset, default)`: the value `offset` rows after this
484
+ # one; needs `over`.
485
+ # @return [AST::WindowFunction]
486
+ def lead(expr, offset = 1, default = nil)
487
+ AST::WindowFunction.new("LEAD", default.nil? ? [expr, offset] : [expr, offset, default])
488
+ end
489
+
490
+ # @!endgroup
491
+ # @!group Escape hatches
492
+
493
+ # Any function by name: `fn(:date_part, "year", :created_at)`. The name
494
+ # is written as given -- so a case-sensitive one can be spelled exactly
495
+ # -- and has to be a plain name, optionally qualified by a schema;
496
+ # the arguments are quoted as values unless they are columns or
497
+ # expressions.
498
+ # @param name [Symbol, String]
499
+ # @return [AST::Function]
500
+ # @example
501
+ # Post.select { fn(:date_part, "year", :created_at).as(:year) }
502
+ #
503
+ # The name is emitted as written, so a case-sensitive one can be
504
+ # spelled exactly, and for that reason it has to be a plain name,
505
+ # optionally qualified by a schema; anything else is refused rather
506
+ # than written into the SQL.
507
+ def fn(name, *args)
508
+ AST::Function.new(
509
+ AST.check_name(name, AST::FUNCTION_NAME, "function name").to_s, args)
510
+ end
511
+
512
+ # Any binary operator by its spelling: `op("&&", :tags, "{ruby,sql}")`.
513
+ # The operator has to be made of operator characters; the operands are
514
+ # quoted as values unless they are columns or expressions, and
515
+ # parenthesized, since the operator's precedence is not known.
516
+ # @param operator [String]
517
+ # @return [AST::Operation]
518
+ # @example
519
+ # Post.where { op("&&", :tags, "{ruby,sql}") } # PostgreSQL arrays
520
+ def op(operator, left, right)
521
+ AST::Operation.new(operator, left, right)
522
+ end
523
+
524
+ # @!endgroup
525
+ # @!group Bits
526
+
527
+ # `BIT_COUNT(expr)`, the bits set in a number. MySQL and PostgreSQL
528
+ # have it; SQLite, Oracle and SQL Server do not.
529
+ # @return [AST::Function]
530
+ # @example
531
+ # Post.select { bit_count(:flags).as(:set) }
532
+ #
533
+ # MySQL counts the bits of a number; PostgreSQL counts those of a bit
534
+ # string, so the argument is cast, and to bit(64) because that is what
535
+ # makes a negative come back as MySQL has it -- 64 bits of two's
536
+ # complement rather than as many as the column happens to be wide.
537
+ def bit_count(expr)
538
+ dialect.bit_count(expr, @model)
539
+ end
540
+
541
+ # @!endgroup
542
+ # @!group Subqueries
543
+
544
+ # `EXISTS (subquery)`. The subquery is a relation, which may refer to
545
+ # the outer row through a qualified column.
546
+ # @param relation [ActiveRecord::Relation]
547
+ # @return [AST::Exists]
548
+ # @example
549
+ # Author.where { exists?(Post.where { :posts[:author_id] == :authors[:id] }) }
550
+ def exists?(relation)
551
+ AST::Exists.new(relation)
552
+ end
553
+
554
+ # `ANY (subquery)`, on the right of a comparison: true of the rows the
555
+ # comparison holds for any row of the subquery. SQLite has none.
556
+ # @param relation [ActiveRecord::Relation]
557
+ # @return [AST::Quantified]
558
+ # @example
559
+ # Post.where { :likes > any(Post.published.select(:likes)) }
560
+ #
561
+ # ANY and ALL quantify a comparison over a subquery, which is what a
562
+ # scalar subquery cannot do: it has to return the one row. `== any`
563
+ # is IN and `!= all` is NOT IN, so what these add is the four
564
+ # comparisons IN has no spelling for.
565
+ def any(relation)
566
+ quantified("ANY", relation)
567
+ end
568
+
569
+ # `ALL (subquery)`, on the right of a comparison: true of the rows the
570
+ # comparison holds for every row of the subquery. SQLite has none.
571
+ # @param relation [ActiveRecord::Relation]
572
+ # @return [AST::Quantified]
573
+ # @example
574
+ # Post.where { :likes >= all(Post.select(:likes)) }
575
+ def all(relation)
576
+ quantified("ALL", relation)
577
+ end
578
+
579
+ # @!endgroup
580
+ # @!group Escape hatches
581
+
582
+ # SQL as written, the one way a string means SQL inside a block. `?`
583
+ # and `:name` placeholders take quoted values, as `where` takes them.
584
+ # @param statement [String]
585
+ # @return [AST::Sql]
586
+ # @example
587
+ # Post.where { sql("length(title) > ?", 10) }
588
+ # Post.select { sql("count(*) FILTER (WHERE score > 0) AS positive") }
589
+ def sql(statement, *binds)
590
+ AST::Sql.new(statement, binds)
591
+ end
592
+
593
+ # A literal where an expression is expected, quoted like any other
594
+ # value. A number or a string takes `as` for itself -- `0.as(:depth)`
595
+ # -- so this is the spelling for the rest: `true`, `nil`, a date.
596
+ # @return [AST::Value]
597
+ # @example
598
+ # Node.select { [:id, value(0).as(:depth)] }
599
+ # Post.select { [:title, value(nil).as(:score)] }
600
+ def value(literal)
601
+ AST::Value.new(literal)
602
+ end
603
+
604
+ # The row an upsert could not insert, in the block `upsert_all` takes:
605
+ # `"excluded"."column"` on PostgreSQL and SQLite, `VALUES(column)` on
606
+ # MySQL.
607
+ # @param column [Symbol]
608
+ # @return [AST::Node]
609
+ # @example
610
+ # Tally.upsert_all(rows, unique_by: :page) { { hits: :hits + excluded(:hits) } }
611
+ def excluded(column)
612
+ dialect.excluded(column, @model)
613
+ end
614
+
615
+ # @!endgroup
616
+ # @!group CASE
617
+
618
+ # `CASE`, in either shape: with an operand each `when` is compared
619
+ # against, or without one, each `when` carrying its own condition.
620
+ # `case` is a keyword, so this one is reached as `self.case`; the
621
+ # shorthands `:age.when(...)` and {#case_when} need no receiver.
622
+ # @return [AST::Case]
623
+ # @example
624
+ # self.case(:age).when(10).then(1).else(0)
625
+ # self.case.when { :age >= 60 }.then { :age - 60 }
626
+ def case(operand = nil)
627
+ AST::Case.new(operand)
628
+ end
629
+
630
+ # The searched `CASE`, started at its first `when`: each `when` is a
631
+ # condition, as a value or a block, and `then` and `else` give the
632
+ # values.
633
+ # @return [AST::Case::When]
634
+ # @example
635
+ # Author.select { case_when { :age >= 60 }.then("senior").else("adult").as(:band) }
636
+ # Author.select { sum(case_when { :age >= 60 }.then(1).else(0)).as(:seniors) }
637
+ def case_when(value = nil, &block)
638
+ AST::Case.new.when(value, &block)
639
+ end
640
+
641
+ private
642
+ # @!endgroup
643
+ #
644
+ # The group closes here rather than above `private`: a comment on
645
+ # that line belongs to the `private` call, which reads no
646
+ # directives, and the group would run on into the next module.
647
+ #
648
+ # SQLite is the one adapter with no quantifier at all, and what it says
649
+ # when it meets one is a syntax error at the SELECT.
650
+ def quantified(kind, relation)
651
+ unless dialect.quantifiers_supported?
652
+ raise NotImplementedError,
653
+ "#{kind} has no equivalent on #{@model.connection_db_config.adapter}"
654
+ end
655
+ AST::Quantified.new(kind, relation)
656
+ end
657
+
658
+ def grouping(kind, sets)
659
+ node = AST::GroupingSets.new(kind, sets)
660
+ return node if dialect.grouping_supported?(kind)
661
+
662
+ raise NotImplementedError,
663
+ "#{kind} has no equivalent on #{@model.connection_db_config.adapter}"
664
+ end
665
+
666
+ def dialect
667
+ @dialect ||= Dialect.for(@model)
668
+ end
669
+ end
670
+ end
671
+ end