activerecord-refined 0.9.0 → 0.10.1
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.
- checksums.yaml +4 -4
- data/.yardopts +17 -0
- data/README.md +61 -1064
- data/activerecord-refined.gemspec +8 -7
- data/docs/conditions.md +206 -0
- data/docs/ctes.md +65 -0
- data/docs/expressions.md +125 -0
- data/docs/functions.md +219 -0
- data/docs/grouping.md +55 -0
- data/docs/index.md +70 -0
- data/docs/joins.md +73 -0
- data/docs/json.md +230 -0
- data/docs/ordering.md +55 -0
- data/docs/time_zones.md +30 -0
- data/docs/windows.md +41 -0
- data/docs/writing.md +33 -0
- data/examples/aggregations.rb +18 -0
- data/examples/expressions.rb +35 -5
- data/lib/active_record/refined/ast.rb +461 -246
- data/lib/active_record/refined/dialect/mariadb.rb +25 -0
- data/lib/active_record/refined/dialect/mysql.rb +18 -0
- data/lib/active_record/refined/dialect/mysql_compat.rb +67 -0
- data/lib/active_record/refined/dialect/oracle.rb +110 -0
- data/lib/active_record/refined/dialect/postgresql.rb +120 -0
- data/lib/active_record/refined/dialect/sql_server.rb +115 -0
- data/lib/active_record/refined/dialect/sqlite.rb +57 -0
- data/lib/active_record/refined/dialect.rb +340 -0
- data/lib/active_record/refined.rb +682 -192
- data/lib/activerecord-refined/version.rb +1 -1
- data/lib/activerecord-refined.rb +1 -0
- metadata +43 -16
- data/.github/workflows/push_gem.yml +0 -45
- data/.github/workflows/sandbox.yml +0 -295
- data/.github/workflows/test.yml +0 -104
- data/.gitignore +0 -19
- data/.rubocop.yml +0 -393
- data/Gemfile +0 -14
- data/Rakefile +0 -53
- data/benchmark/query_building.rb +0 -129
- data/test/test_block_syntax.rb +0 -2999
- data/test/test_helper.rb +0 -238
|
@@ -2,7 +2,72 @@
|
|
|
2
2
|
|
|
3
3
|
module ActiveRecord
|
|
4
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" }
|
|
5
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
|
+
|
|
6
71
|
refine Symbol do
|
|
7
72
|
import_methods AST::Predications
|
|
8
73
|
import_methods AST::Arithmetics
|
|
@@ -19,6 +84,10 @@ module ActiveRecord
|
|
|
19
84
|
AST::Ordering.new(self, :desc)
|
|
20
85
|
end
|
|
21
86
|
|
|
87
|
+
def collate(name)
|
|
88
|
+
AST::Collate.new(self, name)
|
|
89
|
+
end
|
|
90
|
+
|
|
22
91
|
def [](column_name)
|
|
23
92
|
AST::Column.new(self, column_name)
|
|
24
93
|
end
|
|
@@ -46,93 +115,308 @@ module ActiveRecord
|
|
|
46
115
|
end
|
|
47
116
|
end
|
|
48
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 }
|
|
49
133
|
class BlockContext
|
|
50
134
|
# The model is only consulted to learn which adapter the query is being
|
|
51
135
|
# built for, which is what decides how a scalar function is spelled.
|
|
136
|
+
# @api private
|
|
52
137
|
def initialize(model)
|
|
53
138
|
@model = model
|
|
54
139
|
end
|
|
55
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
|
|
56
156
|
AGGREGATE_FUNCTIONS = {
|
|
57
157
|
sum: :sum, avg: :average, min: :minimum, max: :maximum,
|
|
58
158
|
}.freeze
|
|
59
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.
|
|
60
163
|
AGGREGATE_FUNCTIONS.each do |name, arel_func|
|
|
61
|
-
|
|
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
|
|
62
171
|
end
|
|
63
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) }
|
|
64
182
|
def count(column, distinct: false)
|
|
65
183
|
AST::Aggregate.new(column, :count, distinct: distinct)
|
|
66
184
|
end
|
|
67
185
|
|
|
68
|
-
#
|
|
69
|
-
#
|
|
70
|
-
#
|
|
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) }
|
|
71
193
|
def json_arrayagg(value)
|
|
72
194
|
AST::JsonAggregate.new(:arrayagg, [value])
|
|
73
195
|
end
|
|
74
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) }
|
|
75
202
|
def json_objectagg(key, value)
|
|
76
203
|
AST::JsonAggregate.new(:objectagg, [key, value])
|
|
77
204
|
end
|
|
78
205
|
|
|
79
|
-
#
|
|
80
|
-
#
|
|
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) }
|
|
81
226
|
def json_array(*values)
|
|
82
227
|
AST::JsonBuild.new(:array, values)
|
|
83
228
|
end
|
|
84
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) }
|
|
85
236
|
def json_object(pairs = {})
|
|
86
237
|
AST::JsonBuild.new(:object, pairs)
|
|
87
238
|
end
|
|
88
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
|
+
#
|
|
89
379
|
# Scalar functions, defined as real methods so that a typo is a
|
|
90
380
|
# NoMethodError and a name Kernel also answers to (format, hash, test)
|
|
91
|
-
# cannot quietly mean something else.
|
|
92
|
-
#
|
|
93
|
-
#
|
|
94
|
-
#
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
length: {}, ln: {}, log: {}, log10: {}, lower: {}, ltrim: {},
|
|
104
|
-
mod: {}, nullif: {}, pi: {}, power: {}, radians: {}, replace: {},
|
|
105
|
-
round: {}, rtrim: {}, sign: {}, sin: {}, sqrt: {}, substr: {},
|
|
106
|
-
tan: {}, trim: {}, upper: {},
|
|
107
|
-
char_length: { sqlite: "LENGTH" },
|
|
108
|
-
greatest: { sqlite: "MAX" },
|
|
109
|
-
least: { sqlite: "MIN" },
|
|
110
|
-
# PostgreSQL spells log2(x) as log(2, x), which no renaming carries.
|
|
111
|
-
log2: { postgresql: nil },
|
|
112
|
-
# MySQL's TRUNCATE insists on the second argument, where the others
|
|
113
|
-
# default it to zero; SQLite's trunc takes only the one.
|
|
114
|
-
trunc: { mysql: "TRUNCATE" },
|
|
115
|
-
now: { sqlite: nil },
|
|
116
|
-
# The bit aggregates, which PostgreSQL and MySQL spell alike and
|
|
117
|
-
# SQLite has none of. PostgreSQL gained bit_xor in 14.
|
|
118
|
-
bit_and: { sqlite: nil }, bit_or: { sqlite: nil }, bit_xor: { sqlite: nil },
|
|
119
|
-
date_trunc: { sqlite: nil, mysql: nil },
|
|
120
|
-
# Named for Kernel#rand, which it also takes back: a block calling
|
|
121
|
-
# rand would otherwise get Ruby's and never reach the database.
|
|
122
|
-
rand: { sqlite: "RANDOM", postgresql: "RANDOM" },
|
|
123
|
-
# Two different functions share this name: printf formatting here, and
|
|
124
|
-
# on MySQL the one that puts separators in a number, which reads a
|
|
125
|
-
# printf template as the number zero rather than complaining. The
|
|
126
|
-
# name keeps the one meaning; fn(:format, ...) reaches MySQL's.
|
|
127
|
-
format: { mysql: nil },
|
|
128
|
-
}.freeze
|
|
129
|
-
|
|
130
|
-
SCALAR_FUNCTIONS.each_key do |name|
|
|
381
|
+
# cannot quietly mean something else. Where one is spelled other than as
|
|
382
|
+
# its plain upper-cased name, and where a family has no equivalent, is
|
|
383
|
+
# the dialect's to say; here is only the list of them.
|
|
384
|
+
# @private
|
|
385
|
+
SCALAR_FUNCTIONS = %i[
|
|
386
|
+
abs acos asin atan atan2 ceil coalesce concat cos exp floor length ln
|
|
387
|
+
log lower ltrim mod nullif power replace round rtrim sign sin sqrt
|
|
388
|
+
substr tan trim upper degrees radians pi char_length greatest least
|
|
389
|
+
log2 log10 trunc now bit_and bit_or bit_xor date_trunc rand format
|
|
390
|
+
].freeze
|
|
391
|
+
|
|
392
|
+
SCALAR_FUNCTIONS.each do |name|
|
|
131
393
|
define_method(name) do |*args|
|
|
132
|
-
AST::Function.new(function_name(name,
|
|
394
|
+
AST::Function.new(dialect.function_name(name, @model), args)
|
|
133
395
|
end
|
|
134
396
|
end
|
|
135
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
|
+
#
|
|
136
420
|
# The datetime value functions, as the SQL grammar calls them. These
|
|
137
421
|
# the grammar has bare -- PostgreSQL and SQLite reject them written with
|
|
138
422
|
# parentheses -- and the one thing that does go into parentheses is an
|
|
@@ -140,27 +424,28 @@ module ActiveRecord
|
|
|
140
424
|
# takes and SQLite never accepts. The table reads like
|
|
141
425
|
# SCALAR_FUNCTIONS; current_timestamp is the portable spelling of what
|
|
142
426
|
# now means, reaching SQLite where now does not.
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
current_time
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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 }
|
|
151
437
|
def current_date
|
|
152
|
-
AST::DatetimeValueFunction.new(
|
|
153
|
-
function_name(:current_date, DATETIME_VALUE_FUNCTIONS))
|
|
438
|
+
AST::DatetimeValueFunction.new(dialect.function_name(:current_date, @model))
|
|
154
439
|
end
|
|
155
440
|
|
|
156
|
-
(DATETIME_VALUE_FUNCTIONS
|
|
441
|
+
(DATETIME_VALUE_FUNCTIONS - [:current_date]).each do |name|
|
|
157
442
|
define_method(name) do |precision = nil|
|
|
158
443
|
# Built first so that a precision of the wrong type is an
|
|
159
444
|
# ArgumentError on every adapter, before SQLite gets to say it takes
|
|
160
445
|
# none at all.
|
|
161
446
|
node = AST::DatetimeValueFunction.new(
|
|
162
|
-
function_name(name,
|
|
163
|
-
if precision &&
|
|
447
|
+
dialect.function_name(name, @model), precision)
|
|
448
|
+
if precision && !dialect.datetime_precision_supported?
|
|
164
449
|
raise NotImplementedError,
|
|
165
450
|
"#{name} takes no precision on #{@model.connection_db_config.adapter}"
|
|
166
451
|
end
|
|
@@ -168,48 +453,109 @@ module ActiveRecord
|
|
|
168
453
|
end
|
|
169
454
|
end
|
|
170
455
|
|
|
171
|
-
# EXTRACT(field FROM expr)
|
|
172
|
-
# has to be a plain name
|
|
173
|
-
#
|
|
174
|
-
#
|
|
175
|
-
#
|
|
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.
|
|
176
468
|
def extract(field, expr)
|
|
177
469
|
node = AST::Extract.new(field, expr)
|
|
178
|
-
|
|
470
|
+
unless dialect.extract_supported?
|
|
179
471
|
raise NotImplementedError,
|
|
180
472
|
"extract has no equivalent on #{@model.connection_db_config.adapter}"
|
|
181
473
|
end
|
|
182
474
|
node
|
|
183
475
|
end
|
|
184
476
|
|
|
185
|
-
#
|
|
186
|
-
#
|
|
187
|
-
|
|
188
|
-
#
|
|
189
|
-
#
|
|
190
|
-
#
|
|
191
|
-
#
|
|
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
|
|
192
486
|
# Sale.group { grouping_sets([:region], [:product], []) }
|
|
193
|
-
#
|
|
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.
|
|
194
491
|
def grouping_sets(*sets)
|
|
195
492
|
grouping(:grouping_sets, sets)
|
|
196
493
|
end
|
|
197
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) }
|
|
198
501
|
def rollup(*columns)
|
|
199
502
|
grouping(:rollup, columns)
|
|
200
503
|
end
|
|
201
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) }
|
|
202
510
|
def cube(*columns)
|
|
203
511
|
grouping(:cube, columns)
|
|
204
512
|
end
|
|
205
513
|
|
|
206
|
-
#
|
|
207
|
-
#
|
|
208
|
-
|
|
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) }
|
|
209
524
|
def cast(expr, type)
|
|
210
525
|
AST::Cast.new(expr, type)
|
|
211
526
|
end
|
|
212
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
|
+
#
|
|
213
559
|
# The functions that only mean anything with a window. Every adapter
|
|
214
560
|
# that has window functions at all spells these the same -- PostgreSQL,
|
|
215
561
|
# MySQL 8, SQLite 3.25 -- so unlike the scalar functions there is nothing
|
|
@@ -222,122 +568,193 @@ module ActiveRecord
|
|
|
222
568
|
define_method(name) { |arg| AST::WindowFunction.new(name.to_s.upcase, [arg]) }
|
|
223
569
|
end
|
|
224
570
|
|
|
571
|
+
# `NTH_VALUE(expr, nth)`; needs `over`.
|
|
572
|
+
# @return [AST::WindowFunction]
|
|
225
573
|
def nth_value(expr, nth)
|
|
226
574
|
AST::WindowFunction.new("NTH_VALUE", [expr, nth])
|
|
227
575
|
end
|
|
228
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
|
+
#
|
|
229
583
|
# The offset is written out rather than left to default, so that a
|
|
230
584
|
# default value cannot end up where the offset belongs.
|
|
231
585
|
def lag(expr, offset = 1, default = nil)
|
|
232
586
|
AST::WindowFunction.new("LAG", default.nil? ? [expr, offset] : [expr, offset, default])
|
|
233
587
|
end
|
|
234
588
|
|
|
589
|
+
# `LEAD(expr, offset, default)`: the value `offset` rows after this
|
|
590
|
+
# one; needs `over`.
|
|
591
|
+
# @return [AST::WindowFunction]
|
|
235
592
|
def lead(expr, offset = 1, default = nil)
|
|
236
593
|
AST::WindowFunction.new("LEAD", default.nil? ? [expr, offset] : [expr, offset, default])
|
|
237
594
|
end
|
|
238
595
|
|
|
239
|
-
#
|
|
240
|
-
#
|
|
241
|
-
|
|
242
|
-
#
|
|
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.
|
|
243
613
|
def fn(name, *args)
|
|
244
614
|
AST::Function.new(
|
|
245
615
|
AST.check_name(name, AST::FUNCTION_NAME, "function name").to_s, args)
|
|
246
616
|
end
|
|
247
617
|
|
|
248
|
-
#
|
|
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
|
|
249
626
|
def op(operator, left, right)
|
|
250
627
|
AST::Operation.new(operator, left, right)
|
|
251
628
|
end
|
|
252
629
|
|
|
253
|
-
#
|
|
254
|
-
#
|
|
255
|
-
|
|
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
|
|
256
642
|
# complement rather than as many as the column happens to be wide.
|
|
257
643
|
def bit_count(expr)
|
|
258
|
-
|
|
259
|
-
when :mysql then AST::Function.new("BIT_COUNT", [expr])
|
|
260
|
-
when :postgresql
|
|
261
|
-
AST::Function.new("BIT_COUNT", [AST::Cast.new(expr, "bit(64)")])
|
|
262
|
-
else
|
|
263
|
-
raise NotImplementedError,
|
|
264
|
-
"bit_count has no equivalent on #{@model.connection_db_config.adapter}"
|
|
265
|
-
end
|
|
644
|
+
dialect.bit_count(expr, @model)
|
|
266
645
|
end
|
|
267
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] }) }
|
|
268
656
|
def exists?(relation)
|
|
269
657
|
AST::Exists.new(relation)
|
|
270
658
|
end
|
|
271
659
|
|
|
272
|
-
# ANY
|
|
273
|
-
#
|
|
274
|
-
#
|
|
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
|
|
275
665
|
# Post.where { :likes > any(Post.published.select(:likes)) }
|
|
276
|
-
# Post.where { :likes >= all(Post.select(:likes)) }
|
|
277
666
|
#
|
|
278
|
-
#
|
|
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
|
|
279
670
|
# comparisons IN has no spelling for.
|
|
280
671
|
def any(relation)
|
|
281
672
|
quantified("ANY", relation)
|
|
282
673
|
end
|
|
283
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)) }
|
|
284
681
|
def all(relation)
|
|
285
682
|
quantified("ALL", relation)
|
|
286
683
|
end
|
|
287
684
|
|
|
288
|
-
#
|
|
289
|
-
#
|
|
290
|
-
|
|
291
|
-
#
|
|
292
|
-
#
|
|
293
|
-
#
|
|
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") }
|
|
294
695
|
def sql(statement, *binds)
|
|
295
696
|
AST::Sql.new(statement, binds)
|
|
296
697
|
end
|
|
297
698
|
|
|
298
|
-
# A literal where an expression is expected, quoted like any other
|
|
299
|
-
#
|
|
300
|
-
#
|
|
301
|
-
#
|
|
302
|
-
#
|
|
303
|
-
#
|
|
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)] }
|
|
304
706
|
def value(literal)
|
|
305
707
|
AST::Value.new(literal)
|
|
306
708
|
end
|
|
307
709
|
|
|
308
|
-
# The row an upsert could not insert,
|
|
309
|
-
# PostgreSQL and SQLite
|
|
310
|
-
#
|
|
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) } }
|
|
311
717
|
def excluded(column)
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
quoted = @model.with_connection { |c| c.quote_column_name(column) }
|
|
315
|
-
AST::Function.new("VALUES", [Arel::Nodes::SqlLiteral.new(quoted)])
|
|
718
|
+
dialect.excluded(column, @model)
|
|
316
719
|
end
|
|
317
720
|
|
|
318
|
-
#
|
|
319
|
-
#
|
|
320
|
-
|
|
321
|
-
# `
|
|
322
|
-
#
|
|
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
|
|
323
730
|
# self.case(:age).when(10).then(1).else(0)
|
|
324
731
|
# self.case.when { :age >= 60 }.then { :age - 60 }
|
|
325
732
|
def case(operand = nil)
|
|
326
733
|
AST::Case.new(operand)
|
|
327
734
|
end
|
|
328
735
|
|
|
329
|
-
# The searched CASE
|
|
330
|
-
#
|
|
331
|
-
#
|
|
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) }
|
|
332
743
|
def case_when(value = nil, &block)
|
|
333
744
|
AST::Case.new.when(value, &block)
|
|
334
745
|
end
|
|
335
746
|
|
|
336
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
|
+
#
|
|
337
754
|
# SQLite is the one adapter with no quantifier at all, and what it says
|
|
338
755
|
# when it meets one is a syntax error at the SELECT.
|
|
339
756
|
def quantified(kind, relation)
|
|
340
|
-
|
|
757
|
+
unless dialect.quantifiers_supported?
|
|
341
758
|
raise NotImplementedError,
|
|
342
759
|
"#{kind} has no equivalent on #{@model.connection_db_config.adapter}"
|
|
343
760
|
end
|
|
@@ -346,27 +763,39 @@ module ActiveRecord
|
|
|
346
763
|
|
|
347
764
|
def grouping(kind, sets)
|
|
348
765
|
node = AST::GroupingSets.new(kind, sets)
|
|
349
|
-
return node if
|
|
350
|
-
return node if kind == :rollup && adapter_family == :mysql
|
|
766
|
+
return node if dialect.grouping_supported?(kind)
|
|
351
767
|
|
|
352
768
|
raise NotImplementedError,
|
|
353
769
|
"#{kind} has no equivalent on #{@model.connection_db_config.adapter}"
|
|
354
770
|
end
|
|
355
771
|
|
|
356
|
-
def
|
|
357
|
-
|
|
358
|
-
return name.to_s.upcase unless spellings.key?(adapter_family)
|
|
359
|
-
spellings.fetch(adapter_family) ||
|
|
360
|
-
raise(NotImplementedError,
|
|
361
|
-
"#{name} has no equivalent on #{@model.connection_db_config.adapter}")
|
|
362
|
-
end
|
|
363
|
-
|
|
364
|
-
def adapter_family
|
|
365
|
-
@adapter_family ||= AST.adapter_family(@model)
|
|
772
|
+
def dialect
|
|
773
|
+
@dialect ||= Dialect.for(@model)
|
|
366
774
|
end
|
|
367
775
|
end
|
|
368
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)] }
|
|
369
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%") }
|
|
370
799
|
def where(opts = nil, *rest, &block)
|
|
371
800
|
if block
|
|
372
801
|
super(to_arel_condition(evaluate_block(&block)))
|
|
@@ -375,6 +804,11 @@ module ActiveRecord
|
|
|
375
804
|
end
|
|
376
805
|
end
|
|
377
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)] }
|
|
378
812
|
def select(*fields, &block)
|
|
379
813
|
if block
|
|
380
814
|
super(*to_arel_fields(evaluate_block(&block)), &nil)
|
|
@@ -383,6 +817,10 @@ module ActiveRecord
|
|
|
383
817
|
end
|
|
384
818
|
end
|
|
385
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 }
|
|
386
824
|
def having(opts = nil, *rest, &block)
|
|
387
825
|
if block
|
|
388
826
|
super(to_arel_condition(evaluate_block(&block)))
|
|
@@ -391,6 +829,11 @@ module ActiveRecord
|
|
|
391
829
|
end
|
|
392
830
|
end
|
|
393
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] }
|
|
394
837
|
def order(*args, &block)
|
|
395
838
|
if block
|
|
396
839
|
super(*to_arel_fields(evaluate_block(&block)), &nil)
|
|
@@ -399,6 +842,12 @@ module ActiveRecord
|
|
|
399
842
|
end
|
|
400
843
|
end
|
|
401
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(:*)] }
|
|
402
851
|
def group(*args, &block)
|
|
403
852
|
if block
|
|
404
853
|
result = evaluate_block(&block)
|
|
@@ -409,9 +858,17 @@ module ActiveRecord
|
|
|
409
858
|
end
|
|
410
859
|
end
|
|
411
860
|
|
|
412
|
-
#
|
|
413
|
-
#
|
|
414
|
-
#
|
|
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.
|
|
415
872
|
def from(value, subquery_name = nil, as: nil)
|
|
416
873
|
unless value.is_a?(Symbol)
|
|
417
874
|
if as
|
|
@@ -424,12 +881,16 @@ module ActiveRecord
|
|
|
424
881
|
super(arel_table, subquery_name)
|
|
425
882
|
end
|
|
426
883
|
|
|
427
|
-
# Selects a CTE in place of the model's own table
|
|
428
|
-
#
|
|
429
|
-
#
|
|
430
|
-
#
|
|
431
|
-
#
|
|
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)
|
|
432
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.
|
|
433
894
|
# The name is checked against what `with` declares, so that a typo is
|
|
434
895
|
# not a query against a table nobody has. Checked when the SQL is
|
|
435
896
|
# built, since the CTE may be declared after this in the chain, or by a
|
|
@@ -443,28 +904,31 @@ module ActiveRecord
|
|
|
443
904
|
relation
|
|
444
905
|
end
|
|
445
906
|
|
|
907
|
+
# @private
|
|
446
908
|
def from_cte_value
|
|
447
909
|
@values[:from_cte]
|
|
448
910
|
end
|
|
449
911
|
|
|
912
|
+
# @private
|
|
450
913
|
def from_cte_value=(name)
|
|
451
914
|
assert_modifiable!
|
|
452
915
|
@values[:from_cte] = name
|
|
453
916
|
end
|
|
454
917
|
|
|
455
|
-
# DISTINCT ON (
|
|
456
|
-
# brings up. PostgreSQL has it
|
|
457
|
-
#
|
|
458
|
-
#
|
|
459
|
-
#
|
|
460
|
-
# Post.distinct_on { :
|
|
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] }
|
|
461
924
|
#
|
|
462
|
-
#
|
|
463
|
-
#
|
|
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.
|
|
464
927
|
def distinct_on(*columns, &block)
|
|
465
928
|
spawn.distinct_on!(*columns, &block)
|
|
466
929
|
end
|
|
467
930
|
|
|
931
|
+
# {#distinct_on} on the relation itself.
|
|
468
932
|
def distinct_on!(*columns, &block)
|
|
469
933
|
columns = Array(evaluate_block(&block)) if block
|
|
470
934
|
if columns.empty?
|
|
@@ -476,39 +940,54 @@ module ActiveRecord
|
|
|
476
940
|
|
|
477
941
|
# Active Record generates these for the values it knows about; this one
|
|
478
942
|
# is ours, and lives in the same place so that it survives a spawn.
|
|
943
|
+
# @private
|
|
479
944
|
def distinct_on_values
|
|
480
945
|
@values.fetch(:distinct_on, ActiveRecord::QueryMethods::FROZEN_EMPTY_ARRAY)
|
|
481
946
|
end
|
|
482
947
|
|
|
948
|
+
# @private
|
|
483
949
|
def distinct_on_values=(columns)
|
|
484
950
|
assert_modifiable!
|
|
485
951
|
@values[:distinct_on] = columns
|
|
486
952
|
end
|
|
487
953
|
|
|
488
|
-
# Marks the relation for a
|
|
489
|
-
# joined to -- the top few rows of each group, and the
|
|
490
|
-
#
|
|
491
|
-
#
|
|
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]] }
|
|
492
961
|
def lateral
|
|
493
962
|
spawn.lateral!
|
|
494
963
|
end
|
|
495
964
|
|
|
965
|
+
# {#lateral} on the relation itself.
|
|
496
966
|
def lateral!
|
|
497
967
|
self.lateral_value = true
|
|
498
968
|
self
|
|
499
969
|
end
|
|
500
970
|
|
|
971
|
+
# @private
|
|
501
972
|
def lateral_value
|
|
502
973
|
@values[:lateral]
|
|
503
974
|
end
|
|
504
975
|
|
|
976
|
+
# @private
|
|
505
977
|
def lateral_value=(value)
|
|
506
978
|
assert_modifiable!
|
|
507
979
|
@values[:lateral] = value
|
|
508
980
|
end
|
|
509
981
|
|
|
510
|
-
# `
|
|
511
|
-
#
|
|
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] }
|
|
512
991
|
def joins(*args, as: nil, &block)
|
|
513
992
|
if args.first.is_a?(ActiveRecord::Relation)
|
|
514
993
|
super(build_lateral_join(args.first, Arel::Nodes::InnerJoin, as, &block))
|
|
@@ -520,6 +999,10 @@ module ActiveRecord
|
|
|
520
999
|
end
|
|
521
1000
|
end
|
|
522
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] }
|
|
523
1006
|
def left_outer_joins(*args, as: nil, &block)
|
|
524
1007
|
if args.first.is_a?(ActiveRecord::Relation)
|
|
525
1008
|
joins(build_lateral_join(args.first, Arel::Nodes::OuterJoin, as, &block))
|
|
@@ -531,6 +1014,12 @@ module ActiveRecord
|
|
|
531
1014
|
end
|
|
532
1015
|
end
|
|
533
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
|
+
#
|
|
534
1023
|
# The other two outer joins, which Active Record has no method for and
|
|
535
1024
|
# Arel has the nodes for. The rules are joins': the block is the ON,
|
|
536
1025
|
# `as` names the table within the query, a relation marked `lateral`
|
|
@@ -541,16 +1030,19 @@ module ActiveRecord
|
|
|
541
1030
|
args, as, &block)
|
|
542
1031
|
end
|
|
543
1032
|
|
|
1033
|
+
# `FULL OUTER JOIN`, as {#right_outer_joins} takes it. The MySQL
|
|
1034
|
+
# family has none.
|
|
1035
|
+
# @param as [Symbol, nil]
|
|
544
1036
|
def full_outer_joins(*args, as: nil, &block)
|
|
545
1037
|
check_full_outer_support
|
|
546
1038
|
outer_joins(:full_outer_joins, Arel::Nodes::FullOuterJoin,
|
|
547
1039
|
args, as, &block)
|
|
548
1040
|
end
|
|
549
1041
|
|
|
550
|
-
# CROSS JOIN
|
|
551
|
-
#
|
|
552
|
-
#
|
|
553
|
-
#
|
|
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
|
|
554
1046
|
# Post.cross_joins(:authors)
|
|
555
1047
|
# Post.cross_joins(:posts, as: :others)
|
|
556
1048
|
def cross_joins(*args, as: nil, &block)
|
|
@@ -599,7 +1091,7 @@ module ActiveRecord
|
|
|
599
1091
|
entries = Array(result)
|
|
600
1092
|
return if entries.size == 1
|
|
601
1093
|
return unless entries.any? { |node| node.is_a?(AST::GroupingSets) }
|
|
602
|
-
return unless
|
|
1094
|
+
return unless Dialect.for(klass).grouping_by_with_rollup?
|
|
603
1095
|
|
|
604
1096
|
raise ArgumentError,
|
|
605
1097
|
"WITH ROLLUP takes the whole group list; group by the rollup alone"
|
|
@@ -621,7 +1113,12 @@ module ActiveRecord
|
|
|
621
1113
|
# whose meaning turns on where it stands is how an interpolation
|
|
622
1114
|
# becomes an injection.
|
|
623
1115
|
def to_arel_fields(result)
|
|
624
|
-
|
|
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|
|
|
625
1122
|
if node.is_a?(::String) && !node.is_a?(Arel::Nodes::SqlLiteral)
|
|
626
1123
|
raise ArgumentError,
|
|
627
1124
|
"#{node.inspect} could mean SQL or a string; " \
|
|
@@ -666,26 +1163,12 @@ module ActiveRecord
|
|
|
666
1163
|
join_class.new(aliased, Arel::Nodes::On.new(on))
|
|
667
1164
|
end
|
|
668
1165
|
|
|
669
|
-
# PostgreSQL has LATERAL and so does MySQL, from 8.0.14. SQLite has
|
|
670
|
-
# none, and neither has MariaDB, which answers to the same adapter as
|
|
671
|
-
# MySQL. An adapter nobody has classified is left to say for itself.
|
|
672
1166
|
def check_lateral_support
|
|
673
|
-
|
|
674
|
-
when :sqlite
|
|
675
|
-
refuse_lateral("sqlite3")
|
|
676
|
-
when :mysql
|
|
677
|
-
refuse_lateral("MariaDB") if klass.with_connection { |c| c.mariadb? }
|
|
678
|
-
end
|
|
679
|
-
end
|
|
680
|
-
|
|
681
|
-
def refuse_lateral(database)
|
|
682
|
-
raise NotImplementedError, "a lateral join has no equivalent on #{database}"
|
|
1167
|
+
Dialect.for(klass).check_lateral(klass)
|
|
683
1168
|
end
|
|
684
1169
|
|
|
685
|
-
# MySQL has no FULL OUTER JOIN, and neither has MariaDB; SQLite has had
|
|
686
|
-
# one since 3.39 and PostgreSQL always.
|
|
687
1170
|
def check_full_outer_support
|
|
688
|
-
return
|
|
1171
|
+
return if Dialect.for(klass).full_outer_join_supported?
|
|
689
1172
|
raise NotImplementedError, "a full outer join has no equivalent on MySQL"
|
|
690
1173
|
end
|
|
691
1174
|
|
|
@@ -725,11 +1208,14 @@ module ActiveRecord
|
|
|
725
1208
|
# QueryMethods. What a block adds here is the one thing their arguments
|
|
726
1209
|
# cannot carry: a value worked out from the row rather than given.
|
|
727
1210
|
module Writes
|
|
728
|
-
# `
|
|
729
|
-
#
|
|
730
|
-
#
|
|
731
|
-
#
|
|
732
|
-
#
|
|
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) } }
|
|
733
1219
|
def update_all(updates = nil, &block)
|
|
734
1220
|
return super(updates) unless block
|
|
735
1221
|
if updates
|
|
@@ -742,13 +1228,17 @@ module ActiveRecord
|
|
|
742
1228
|
super(result.transform_values { |value| to_arel_field(value) })
|
|
743
1229
|
end
|
|
744
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
|
+
#
|
|
745
1239
|
# upsert_all's on_duplicate takes SQL text and nothing else, so this is
|
|
746
1240
|
# the one place the DSL writes the SQL out itself rather than handing
|
|
747
|
-
# Arel a tree.
|
|
748
|
-
#
|
|
749
|
-
# Post.upsert_all(rows, unique_by: :title) {
|
|
750
|
-
# { likes: :likes + excluded(:likes) }
|
|
751
|
-
# }
|
|
1241
|
+
# Arel a tree.
|
|
752
1242
|
def upsert_all(attributes, **options, &block)
|
|
753
1243
|
return super(attributes, **options) unless block
|
|
754
1244
|
if options.key?(:on_duplicate)
|