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
@@ -1,1272 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module ActiveRecord
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" }
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
-
71
- refine Symbol do
72
- import_methods AST::Predications
73
- import_methods AST::Arithmetics
74
-
75
- def as(alias_name, quote: true)
76
- AST::As.new(self, alias_name, quote: quote)
77
- end
78
-
79
- def asc
80
- AST::Ordering.new(self, :asc)
81
- end
82
-
83
- def desc
84
- AST::Ordering.new(self, :desc)
85
- end
86
-
87
- def collate(name)
88
- AST::Collate.new(self, name)
89
- end
90
-
91
- def [](column_name)
92
- AST::Column.new(self, column_name)
93
- end
94
- end
95
-
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|
100
- refine klass do
101
- import_methods AST::NumericArithmetics
102
-
103
- def as(alias_name, quote: true)
104
- AST::As.new(AST::Value.new(self), alias_name, quote: quote)
105
- end
106
- end
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
116
- end
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 }
133
- class BlockContext
134
- # The model is only consulted to learn which adapter the query is being
135
- # built for, which is what decides how a scalar function is spelled.
136
- # @api private
137
- def initialize(model)
138
- @model = model
139
- end
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
156
- AGGREGATE_FUNCTIONS = {
157
- sum: :sum, avg: :average, min: :minimum, max: :maximum,
158
- }.freeze
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.
163
- AGGREGATE_FUNCTIONS.each do |name, 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
171
- end
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) }
182
- def count(column, distinct: false)
183
- AST::Aggregate.new(column, :count, distinct: distinct)
184
- end
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
- #
379
- # Scalar functions, defined as real methods so that a typo is a
380
- # NoMethodError and a name Kernel also answers to (format, hash, test)
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|
393
- define_method(name) do |*args|
394
- AST::Function.new(dialect.function_name(name, @model), args)
395
- end
396
- end
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
- #
420
- # The datetime value functions, as the SQL grammar calls them. These
421
- # the grammar has bare -- PostgreSQL and SQLite reject them written with
422
- # parentheses -- and the one thing that does go into parentheses is an
423
- # optional precision, current_timestamp(3), which current_date never
424
- # takes and SQLite never accepts. The table reads like
425
- # SCALAR_FUNCTIONS; current_timestamp is the portable spelling of what
426
- # now means, reaching SQLite where now does not.
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 }
437
- def current_date
438
- AST::DatetimeValueFunction.new(dialect.function_name(:current_date, @model))
439
- end
440
-
441
- (DATETIME_VALUE_FUNCTIONS - [:current_date]).each do |name|
442
- define_method(name) do |precision = nil|
443
- # Built first so that a precision of the wrong type is an
444
- # ArgumentError on every adapter, before SQLite gets to say it takes
445
- # none at all.
446
- node = AST::DatetimeValueFunction.new(
447
- dialect.function_name(name, @model), precision)
448
- if precision && !dialect.datetime_precision_supported?
449
- raise NotImplementedError,
450
- "#{name} takes no precision on #{@model.connection_db_config.adapter}"
451
- end
452
- node
453
- end
454
- end
455
-
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.
468
- def extract(field, expr)
469
- node = AST::Extract.new(field, expr)
470
- unless dialect.extract_supported?
471
- raise NotImplementedError,
472
- "extract has no equivalent on #{@model.connection_db_config.adapter}"
473
- end
474
- node
475
- end
476
-
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
486
- # Sale.group { grouping_sets([: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.
491
- def grouping_sets(*sets)
492
- grouping(:grouping_sets, sets)
493
- end
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) }
501
- def rollup(*columns)
502
- grouping(:rollup, columns)
503
- end
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) }
510
- def cube(*columns)
511
- grouping(:cube, columns)
512
- end
513
-
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) }
524
- def cast(expr, type)
525
- AST::Cast.new(expr, type)
526
- end
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
- #
559
- # The functions that only mean anything with a window. Every adapter
560
- # that has window functions at all spells these the same -- PostgreSQL,
561
- # MySQL 8, SQLite 3.25 -- so unlike the scalar functions there is nothing
562
- # here to translate. Each says so if `over` never arrives.
563
- %i[row_number rank dense_rank percent_rank cume_dist].each do |name|
564
- define_method(name) { AST::WindowFunction.new(name.to_s.upcase, []) }
565
- end
566
-
567
- %i[ntile first_value last_value].each do |name|
568
- define_method(name) { |arg| AST::WindowFunction.new(name.to_s.upcase, [arg]) }
569
- end
570
-
571
- # `NTH_VALUE(expr, nth)`; needs `over`.
572
- # @return [AST::WindowFunction]
573
- def nth_value(expr, nth)
574
- AST::WindowFunction.new("NTH_VALUE", [expr, nth])
575
- end
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
- #
583
- # The offset is written out rather than left to default, so that a
584
- # default value cannot end up where the offset belongs.
585
- def lag(expr, offset = 1, default = nil)
586
- AST::WindowFunction.new("LAG", default.nil? ? [expr, offset] : [expr, offset, default])
587
- end
588
-
589
- # `LEAD(expr, offset, default)`: the value `offset` rows after this
590
- # one; needs `over`.
591
- # @return [AST::WindowFunction]
592
- def lead(expr, offset = 1, default = nil)
593
- AST::WindowFunction.new("LEAD", default.nil? ? [expr, offset] : [expr, offset, default])
594
- end
595
-
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.
613
- def fn(name, *args)
614
- AST::Function.new(
615
- AST.check_name(name, AST::FUNCTION_NAME, "function name").to_s, args)
616
- end
617
-
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
642
- # complement rather than as many as the column happens to be wide.
643
- def bit_count(expr)
644
- dialect.bit_count(expr, @model)
645
- end
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] }) }
656
- def exists?(relation)
657
- AST::Exists.new(relation)
658
- end
659
-
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
665
- # Post.where { :likes > any(Post.published.select(:likes)) }
666
- #
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
670
- # comparisons IN has no spelling for.
671
- def any(relation)
672
- quantified("ANY", relation)
673
- end
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)) }
681
- def all(relation)
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)] }
706
- def value(literal)
707
- AST::Value.new(literal)
708
- end
709
-
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) } }
717
- def excluded(column)
718
- dialect.excluded(column, @model)
719
- end
720
-
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
730
- # self.case(:age).when(10).then(1).else(0)
731
- # self.case.when { :age >= 60 }.then { :age - 60 }
732
- def case(operand = nil)
733
- AST::Case.new(operand)
734
- end
735
-
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) }
743
- def case_when(value = nil, &block)
744
- AST::Case.new.when(value, &block)
745
- end
746
-
747
- private
748
- # @!endgroup
749
- #
750
- # The group closes here rather than above `private`: a comment on
751
- # that line belongs to the `private` call, which reads no
752
- # directives, and the group would run on into the next module.
753
- #
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)
762
- end
763
-
764
- def grouping(kind, sets)
765
- node = AST::GroupingSets.new(kind, sets)
766
- return node if dialect.grouping_supported?(kind)
767
-
768
- raise NotImplementedError,
769
- "#{kind} has no equivalent on #{@model.connection_db_config.adapter}"
770
- end
771
-
772
- def dialect
773
- @dialect ||= Dialect.for(@model)
774
- end
775
- end
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)] }
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%") }
799
- def where(opts = nil, *rest, &block)
800
- if block
801
- super(to_arel_condition(evaluate_block(&block)))
802
- else
803
- super
804
- end
805
- end
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)] }
812
- def select(*fields, &block)
813
- if block
814
- super(*to_arel_fields(evaluate_block(&block)), &nil)
815
- else
816
- super
817
- end
818
- end
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 }
824
- def having(opts = nil, *rest, &block)
825
- if block
826
- super(to_arel_condition(evaluate_block(&block)))
827
- else
828
- super
829
- end
830
- end
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] }
837
- def order(*args, &block)
838
- if block
839
- super(*to_arel_fields(evaluate_block(&block)), &nil)
840
- else
841
- super
842
- end
843
- end
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(:*)] }
851
- def group(*args, &block)
852
- if block
853
- result = evaluate_block(&block)
854
- check_rollup_stands_alone(result)
855
- super(*to_arel_fields(result), &nil)
856
- else
857
- super
858
- end
859
- end
860
-
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.
872
- def from(value, subquery_name = nil, as: nil)
873
- unless value.is_a?(Symbol)
874
- if as
875
- raise ArgumentError, "as: needs the table named as a symbol"
876
- end
877
- return super(value, subquery_name)
878
- end
879
- arel_table = Arel::Table.new(value)
880
- arel_table = arel_table.alias(as) if as
881
- super(arel_table, subquery_name)
882
- end
883
-
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)
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.
894
- # The name is checked against what `with` declares, so that a typo is
895
- # not a query against a table nobody has. Checked when the SQL is
896
- # built, since the CTE may be declared after this in the chain, or by a
897
- # scope merged into it.
898
- def from_cte(name)
899
- unless name.is_a?(Symbol)
900
- raise ArgumentError, "from_cte takes the CTE's name as a symbol"
901
- end
902
- relation = from(name, as: klass.table_name)
903
- relation.from_cte_value = name
904
- relation
905
- end
906
-
907
- # @private
908
- def from_cte_value
909
- @values[:from_cte]
910
- end
911
-
912
- # @private
913
- def from_cte_value=(name)
914
- assert_modifiable!
915
- @values[:from_cte] = name
916
- end
917
-
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] }
924
- #
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.
927
- def distinct_on(*columns, &block)
928
- spawn.distinct_on!(*columns, &block)
929
- end
930
-
931
- # {#distinct_on} on the relation itself.
932
- def distinct_on!(*columns, &block)
933
- columns = Array(evaluate_block(&block)) if block
934
- if columns.empty?
935
- raise ArgumentError, "distinct_on needs a column or an expression"
936
- end
937
- self.distinct_on_values += columns
938
- self
939
- end
940
-
941
- # Active Record generates these for the values it knows about; this one
942
- # is ours, and lives in the same place so that it survives a spawn.
943
- # @private
944
- def distinct_on_values
945
- @values.fetch(:distinct_on, ActiveRecord::QueryMethods::FROZEN_EMPTY_ARRAY)
946
- end
947
-
948
- # @private
949
- def distinct_on_values=(columns)
950
- assert_modifiable!
951
- @values[:distinct_on] = columns
952
- end
953
-
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]] }
961
- def lateral
962
- spawn.lateral!
963
- end
964
-
965
- # {#lateral} on the relation itself.
966
- def lateral!
967
- self.lateral_value = true
968
- self
969
- end
970
-
971
- # @private
972
- def lateral_value
973
- @values[:lateral]
974
- end
975
-
976
- # @private
977
- def lateral_value=(value)
978
- assert_modifiable!
979
- @values[:lateral] = value
980
- end
981
-
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] }
991
- def joins(*args, as: nil, &block)
992
- if args.first.is_a?(ActiveRecord::Relation)
993
- super(build_lateral_join(args.first, Arel::Nodes::InnerJoin, as, &block))
994
- elsif block
995
- super(build_join_node(args.first, Arel::Nodes::InnerJoin, as, &block))
996
- else
997
- reject_join_alias(as)
998
- super(*args, &block)
999
- end
1000
- end
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] }
1006
- def left_outer_joins(*args, as: nil, &block)
1007
- if args.first.is_a?(ActiveRecord::Relation)
1008
- joins(build_lateral_join(args.first, Arel::Nodes::OuterJoin, as, &block))
1009
- elsif block
1010
- joins(build_join_node(args.first, Arel::Nodes::OuterJoin, as, &block))
1011
- else
1012
- reject_join_alias(as)
1013
- super(*args, &block)
1014
- end
1015
- end
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
- #
1023
- # The other two outer joins, which Active Record has no method for and
1024
- # Arel has the nodes for. The rules are joins': the block is the ON,
1025
- # `as` names the table within the query, a relation marked `lateral`
1026
- # joins as one. An association name is not among them -- what Active
1027
- # Record reads out of one is an inner or a left join and nothing else.
1028
- def right_outer_joins(*args, as: nil, &block)
1029
- outer_joins(:right_outer_joins, Arel::Nodes::RightOuterJoin,
1030
- args, as, &block)
1031
- end
1032
-
1033
- # `FULL OUTER JOIN`, as {#right_outer_joins} takes it. The MySQL
1034
- # family has none.
1035
- # @param as [Symbol, nil]
1036
- def full_outer_joins(*args, as: nil, &block)
1037
- check_full_outer_support
1038
- outer_joins(:full_outer_joins, Arel::Nodes::FullOuterJoin,
1039
- args, as, &block)
1040
- end
1041
-
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
1046
- # Post.cross_joins(:authors)
1047
- # Post.cross_joins(:posts, as: :others)
1048
- def cross_joins(*args, as: nil, &block)
1049
- if block
1050
- raise ArgumentError,
1051
- "a cross join has no condition; joins is the one that takes a block"
1052
- end
1053
- joins(build_cross_join(args.first, as))
1054
- end
1055
-
1056
- private
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
1064
- end
1065
-
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) }
1072
-
1073
- declared = with_values.flat_map { |value| value.keys.map(&:to_sym) }
1074
- return if declared.include?(name)
1075
-
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
1081
-
1082
- def evaluate_block(&block)
1083
- refined_block = block.refined(ActiveRecord::Refined::BlockSyntax)
1084
- BlockContext.new(klass).instance_exec(&refined_block)
1085
- end
1086
-
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?
1095
-
1096
- raise ArgumentError,
1097
- "WITH ROLLUP takes the whole group list; group by the rollup alone"
1098
- end
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)
1108
- end
1109
-
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
1130
-
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
1138
- end
1139
-
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
1144
-
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
1159
-
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))
1164
- end
1165
-
1166
- def check_lateral_support
1167
- Dialect.for(klass).check_lateral(klass)
1168
- end
1169
-
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"
1173
- end
1174
-
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
1205
- end
1206
-
1207
- # The writing statements, which live on Relation rather than in
1208
- # QueryMethods. What a block adds here is the one thing their arguments
1209
- # cannot carry: a value worked out from the row rather than given.
1210
- module Writes
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) } }
1219
- def update_all(updates = nil, &block)
1220
- return super(updates) unless block
1221
- if updates
1222
- raise ArgumentError, "update_all takes updates or a block, not both"
1223
- end
1224
- result = evaluate_block(&block)
1225
- unless result.is_a?(::Hash)
1226
- raise ArgumentError, "the block gives update_all a hash of column => value"
1227
- end
1228
- super(result.transform_values { |value| to_arel_field(value) })
1229
- end
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
- #
1239
- # upsert_all's on_duplicate takes SQL text and nothing else, so this is
1240
- # the one place the DSL writes the SQL out itself rather than handing
1241
- # Arel a tree.
1242
- def upsert_all(attributes, **options, &block)
1243
- return super(attributes, **options) unless block
1244
- if options.key?(:on_duplicate)
1245
- raise ArgumentError, "upsert_all takes on_duplicate: or a block, not both"
1246
- end
1247
- result = evaluate_block(&block)
1248
- unless result.is_a?(::Hash)
1249
- raise ArgumentError, "the block gives upsert_all a hash of column => value"
1250
- end
1251
- if result.empty?
1252
- raise ArgumentError, "the block gives upsert_all at least one column to set"
1253
- end
1254
- super(attributes, on_duplicate: Arel.sql(set_clause(result)), **options)
1255
- end
1256
-
1257
- private
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
1269
- end
1270
- end
1271
- end
1272
- end
3
+ require "active_record/refined/version"
4
+ require "active_record/refined/ast"
5
+ require "active_record/refined/dialect"
6
+ require "active_record/refined/block_syntax"
7
+ require "active_record/refined/block_context"
8
+ require "active_record/refined/query_methods"
9
+ require "active_record/refined/writes"