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