activerecord-refined 0.8.1 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.yardopts +17 -0
- data/README.md +111 -815
- data/activerecord-refined.gemspec +36 -17
- 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/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 +31 -11
- data/examples/complex_joins.rb +12 -10
- data/examples/ctes.rb +22 -20
- data/examples/expressions.rb +110 -45
- data/examples/json.rb +77 -38
- data/examples/postgresql.rb +64 -53
- data/examples/predicates.rb +35 -33
- data/examples/subqueries.rb +20 -18
- data/examples/windows.rb +23 -21
- data/examples/writes.rb +26 -24
- data/lib/active_record/refined/ast.rb +1117 -373
- 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 +877 -307
- data/lib/activerecord-refined/version.rb +3 -1
- data/lib/activerecord-refined.rb +9 -5
- metadata +186 -15
- data/.github/workflows/push_gem.yml +0 -45
- data/.github/workflows/sandbox.yml +0 -295
- data/.github/workflows/test.yml +0 -90
- data/.gitignore +0 -19
- data/Gemfile +0 -12
- data/Rakefile +0 -25
- data/benchmark/query_building.rb +0 -129
- data/test/test_block_syntax.rb +0 -2493
- data/test/test_helper.rb +0 -221
|
@@ -1,6 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module ActiveRecord
|
|
2
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" }
|
|
3
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
|
+
|
|
4
71
|
refine Symbol do
|
|
5
72
|
import_methods AST::Predications
|
|
6
73
|
import_methods AST::Arithmetics
|
|
@@ -17,90 +84,339 @@ module ActiveRecord
|
|
|
17
84
|
AST::Ordering.new(self, :desc)
|
|
18
85
|
end
|
|
19
86
|
|
|
87
|
+
def collate(name)
|
|
88
|
+
AST::Collate.new(self, name)
|
|
89
|
+
end
|
|
90
|
+
|
|
20
91
|
def [](column_name)
|
|
21
92
|
AST::Column.new(self, column_name)
|
|
22
93
|
end
|
|
23
94
|
end
|
|
24
95
|
|
|
25
|
-
# Shorthand for `value(0).as(:depth)` and the like
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
|
|
29
|
-
[Integer, Float].each do |klass|
|
|
96
|
+
# Shorthand for `value(0).as(:depth)` and the like, and arithmetic with
|
|
97
|
+
# the number on the left: 20 - :quantity. BigDecimal is a number here
|
|
98
|
+
# because that is what a decimal column's values are.
|
|
99
|
+
[Integer, Float, BigDecimal].each do |klass|
|
|
30
100
|
refine klass do
|
|
101
|
+
import_methods AST::NumericArithmetics
|
|
102
|
+
|
|
31
103
|
def as(alias_name, quote: true)
|
|
32
104
|
AST::As.new(AST::Value.new(self), alias_name, quote: quote)
|
|
33
105
|
end
|
|
34
106
|
end
|
|
35
107
|
end
|
|
108
|
+
|
|
109
|
+
# A string is a value here as it is in every other position of a block;
|
|
110
|
+
# SQL is asked for by name, with sql().
|
|
111
|
+
refine String do
|
|
112
|
+
def as(alias_name, quote: true)
|
|
113
|
+
AST::As.new(AST::Value.new(self), alias_name, quote: quote)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
36
116
|
end
|
|
37
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 }
|
|
38
133
|
class BlockContext
|
|
39
134
|
# The model is only consulted to learn which adapter the query is being
|
|
40
135
|
# built for, which is what decides how a scalar function is spelled.
|
|
136
|
+
# @api private
|
|
41
137
|
def initialize(model)
|
|
42
138
|
@model = model
|
|
43
139
|
end
|
|
44
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
|
|
45
156
|
AGGREGATE_FUNCTIONS = {
|
|
46
157
|
sum: :sum, avg: :average, min: :minimum, max: :maximum,
|
|
47
158
|
}.freeze
|
|
48
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.
|
|
49
163
|
AGGREGATE_FUNCTIONS.each do |name, arel_func|
|
|
50
|
-
|
|
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
|
|
51
171
|
end
|
|
52
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) }
|
|
53
182
|
def count(column, distinct: false)
|
|
54
183
|
AST::Aggregate.new(column, :count, distinct: distinct)
|
|
55
184
|
end
|
|
56
185
|
|
|
186
|
+
# The rows of a group gathered into one JSON array, a value from each:
|
|
187
|
+
# `jsonb_agg` on PostgreSQL, `json_group_array` on SQLite,
|
|
188
|
+
# `JSON_ARRAYAGG` elsewhere. What it gives is JSON, which compares
|
|
189
|
+
# as a dug value does.
|
|
190
|
+
# @return [AST::JsonAggregate]
|
|
191
|
+
# @example
|
|
192
|
+
# Post.group { :author_id }.select { json_arrayagg(:title).as(:titles) }
|
|
193
|
+
def json_arrayagg(value)
|
|
194
|
+
AST::JsonAggregate.new(:arrayagg, [value])
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# The rows of a group gathered into one JSON object, a key and a value
|
|
198
|
+
# from each.
|
|
199
|
+
# @return [AST::JsonAggregate]
|
|
200
|
+
# @example
|
|
201
|
+
# Post.select { json_objectagg(:title, :meta.dig(:stars)).as(:stars) }
|
|
202
|
+
def json_objectagg(key, value)
|
|
203
|
+
AST::JsonAggregate.new(:objectagg, [key, value])
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# The strings of a group joined into one, a separator between:
|
|
207
|
+
# `STRING_AGG` on PostgreSQL and SQL Server, `group_concat` on SQLite,
|
|
208
|
+
# `GROUP_CONCAT` on MySQL, `LISTAGG` on Oracle. Takes
|
|
209
|
+
# {AST::StringAggregate#order} for the order they are joined in.
|
|
210
|
+
# @param separator [String] the comma GROUP_CONCAT defaults to, unless given
|
|
211
|
+
# @return [AST::StringAggregate]
|
|
212
|
+
# @example
|
|
213
|
+
# Post.group { :author_id }.
|
|
214
|
+
# select { string_agg(:title, ", ").order(:title).as(:titles) }
|
|
215
|
+
def string_agg(value, separator = ",")
|
|
216
|
+
AST::StringAggregate.new(value, separator)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# @!endgroup
|
|
220
|
+
# @!group JSON
|
|
221
|
+
|
|
222
|
+
# A JSON array built in the row from the values given.
|
|
223
|
+
# @return [AST::JsonBuild]
|
|
224
|
+
# @example
|
|
225
|
+
# Post.select { json_array(:title, :likes).as(:pair) }
|
|
226
|
+
def json_array(*values)
|
|
227
|
+
AST::JsonBuild.new(:array, values)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# A JSON object built in the row from a hash whose values are
|
|
231
|
+
# expressions.
|
|
232
|
+
# @param pairs [Hash{Symbol, String => Object}]
|
|
233
|
+
# @return [AST::JsonBuild]
|
|
234
|
+
# @example
|
|
235
|
+
# Post.select { json_object(title: :title, stars: :meta.dig(:stars)).as(:doc) }
|
|
236
|
+
def json_object(pairs = {})
|
|
237
|
+
AST::JsonBuild.new(:object, pairs)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# @!endgroup
|
|
241
|
+
# @!group Scalar functions
|
|
242
|
+
|
|
243
|
+
# @!method abs(x)
|
|
244
|
+
# `ABS(x)`.
|
|
245
|
+
# @return [AST::Function]
|
|
246
|
+
# @!method acos(x)
|
|
247
|
+
# `ACOS(x)`.
|
|
248
|
+
# @return [AST::Function]
|
|
249
|
+
# @!method asin(x)
|
|
250
|
+
# `ASIN(x)`.
|
|
251
|
+
# @return [AST::Function]
|
|
252
|
+
# @!method atan(x)
|
|
253
|
+
# `ATAN(x)`.
|
|
254
|
+
# @return [AST::Function]
|
|
255
|
+
# @!method atan2(y, x)
|
|
256
|
+
# `ATAN2(y, x)`.
|
|
257
|
+
# @return [AST::Function]
|
|
258
|
+
# @!method ceil(x)
|
|
259
|
+
# `CEIL(x)`.
|
|
260
|
+
# @return [AST::Function]
|
|
261
|
+
# @!method coalesce(*values)
|
|
262
|
+
# `COALESCE(a, b, ...)`: the first that is not NULL.
|
|
263
|
+
# @return [AST::Function]
|
|
264
|
+
# @!method concat(*strings)
|
|
265
|
+
# `CONCAT(a, b, ...)`.
|
|
266
|
+
# @return [AST::Function]
|
|
267
|
+
# @!method cos(x)
|
|
268
|
+
# `COS(x)`.
|
|
269
|
+
# @return [AST::Function]
|
|
270
|
+
# @!method exp(x)
|
|
271
|
+
# `EXP(x)`.
|
|
272
|
+
# @return [AST::Function]
|
|
273
|
+
# @!method floor(x)
|
|
274
|
+
# `FLOOR(x)`.
|
|
275
|
+
# @return [AST::Function]
|
|
276
|
+
# @!method length(string)
|
|
277
|
+
# `LENGTH(string)`.
|
|
278
|
+
# @return [AST::Function]
|
|
279
|
+
# @!method ln(x)
|
|
280
|
+
# `LN(x)`.
|
|
281
|
+
# @return [AST::Function]
|
|
282
|
+
# @!method log(base, x)
|
|
283
|
+
# `LOG(base, x)`.
|
|
284
|
+
# @return [AST::Function]
|
|
285
|
+
# @!method lower(string)
|
|
286
|
+
# `LOWER(string)`.
|
|
287
|
+
# @return [AST::Function]
|
|
288
|
+
# @!method ltrim(string)
|
|
289
|
+
# `LTRIM(string)`.
|
|
290
|
+
# @return [AST::Function]
|
|
291
|
+
# @!method mod(x, y)
|
|
292
|
+
# `MOD(x, y)`.
|
|
293
|
+
# @return [AST::Function]
|
|
294
|
+
# @!method nullif(x, y)
|
|
295
|
+
# `NULLIF(x, y)`: NULL where the two are equal, x otherwise.
|
|
296
|
+
# @return [AST::Function]
|
|
297
|
+
# @!method power(x, y)
|
|
298
|
+
# `POWER(x, y)`.
|
|
299
|
+
# @return [AST::Function]
|
|
300
|
+
# @!method replace(string, from, to)
|
|
301
|
+
# `REPLACE(string, from, to)`.
|
|
302
|
+
# @return [AST::Function]
|
|
303
|
+
# @!method round(x, places = 0)
|
|
304
|
+
# `ROUND(x, places)`.
|
|
305
|
+
# @return [AST::Function]
|
|
306
|
+
# @!method rtrim(string)
|
|
307
|
+
# `RTRIM(string)`.
|
|
308
|
+
# @return [AST::Function]
|
|
309
|
+
# @!method sign(x)
|
|
310
|
+
# `SIGN(x)`.
|
|
311
|
+
# @return [AST::Function]
|
|
312
|
+
# @!method sin(x)
|
|
313
|
+
# `SIN(x)`.
|
|
314
|
+
# @return [AST::Function]
|
|
315
|
+
# @!method sqrt(x)
|
|
316
|
+
# `SQRT(x)`.
|
|
317
|
+
# @return [AST::Function]
|
|
318
|
+
# @!method substr(string, from, length = nil)
|
|
319
|
+
# `SUBSTR(string, from, length)`.
|
|
320
|
+
# @return [AST::Function]
|
|
321
|
+
# @!method tan(x)
|
|
322
|
+
# `TAN(x)`.
|
|
323
|
+
# @return [AST::Function]
|
|
324
|
+
# @!method trim(string)
|
|
325
|
+
# `TRIM(string)`.
|
|
326
|
+
# @return [AST::Function]
|
|
327
|
+
# @!method upper(string)
|
|
328
|
+
# `UPPER(string)`.
|
|
329
|
+
# @return [AST::Function]
|
|
330
|
+
# @!method degrees(x)
|
|
331
|
+
# `DEGREES(x)`. Oracle has none.
|
|
332
|
+
# @return [AST::Function]
|
|
333
|
+
# @!method radians(x)
|
|
334
|
+
# `RADIANS(x)`. Oracle has none.
|
|
335
|
+
# @return [AST::Function]
|
|
336
|
+
# @!method pi
|
|
337
|
+
# `PI()`. Oracle has none.
|
|
338
|
+
# @return [AST::Function]
|
|
339
|
+
# @!method char_length(string)
|
|
340
|
+
# `CHAR_LENGTH(string)`: `LENGTH` on SQLite and Oracle, `LEN` on SQL Server.
|
|
341
|
+
# @return [AST::Function]
|
|
342
|
+
# @!method greatest(*values)
|
|
343
|
+
# `GREATEST(a, b, ...)`: `MAX` on SQLite.
|
|
344
|
+
# @return [AST::Function]
|
|
345
|
+
# @!method least(*values)
|
|
346
|
+
# `LEAST(a, b, ...)`: `MIN` on SQLite.
|
|
347
|
+
# @return [AST::Function]
|
|
348
|
+
# @!method log2(x)
|
|
349
|
+
# `LOG2(x)`. PostgreSQL and Oracle have none; `log(2, x)` is their spelling.
|
|
350
|
+
# @return [AST::Function]
|
|
351
|
+
# @!method log10(x)
|
|
352
|
+
# `LOG10(x)`. Oracle has none.
|
|
353
|
+
# @return [AST::Function]
|
|
354
|
+
# @!method trunc(x, places = 0)
|
|
355
|
+
# `TRUNC(x, places)`: `TRUNCATE` on MySQL, which insists on the places.
|
|
356
|
+
# @return [AST::Function]
|
|
357
|
+
# @!method now
|
|
358
|
+
# `NOW()`. SQLite and Oracle have none; {#current_timestamp} reaches both.
|
|
359
|
+
# @return [AST::Function]
|
|
360
|
+
# @!method bit_and(column)
|
|
361
|
+
# `BIT_AND(column)`, an aggregate. PostgreSQL and MySQL have it.
|
|
362
|
+
# @return [AST::Function]
|
|
363
|
+
# @!method bit_or(column)
|
|
364
|
+
# `BIT_OR(column)`, an aggregate. PostgreSQL and MySQL have it.
|
|
365
|
+
# @return [AST::Function]
|
|
366
|
+
# @!method bit_xor(column)
|
|
367
|
+
# `BIT_XOR(column)`, an aggregate. PostgreSQL and MySQL have it.
|
|
368
|
+
# @return [AST::Function]
|
|
369
|
+
# @!method date_trunc(field, timestamp)
|
|
370
|
+
# `date_trunc('day', timestamp)`. PostgreSQL has it; the others do not.
|
|
371
|
+
# @return [AST::Function]
|
|
372
|
+
# @!method rand
|
|
373
|
+
# `RAND()`, a random number per row: `RANDOM()` on PostgreSQL and SQLite. Oracle and SQL Server have none.
|
|
374
|
+
# @return [AST::Function]
|
|
375
|
+
# @!method format(template, *values)
|
|
376
|
+
# printf-style `FORMAT(template, ...)`. PostgreSQL and SQLite have it; MySQL's FORMAT is a different function, reached through {#fn}.
|
|
377
|
+
# @return [AST::Function]
|
|
378
|
+
#
|
|
57
379
|
# Scalar functions, defined as real methods so that a typo is a
|
|
58
380
|
# NoMethodError and a name Kernel also answers to (format, hash, test)
|
|
59
|
-
# cannot quietly mean something else.
|
|
60
|
-
#
|
|
61
|
-
#
|
|
62
|
-
#
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
length: {}, ln: {}, log: {}, log10: {}, lower: {}, ltrim: {},
|
|
72
|
-
mod: {}, nullif: {}, pi: {}, power: {}, radians: {}, replace: {},
|
|
73
|
-
round: {}, rtrim: {}, sign: {}, sin: {}, sqrt: {}, substr: {},
|
|
74
|
-
tan: {}, trim: {}, upper: {},
|
|
75
|
-
char_length: {sqlite: 'LENGTH'},
|
|
76
|
-
greatest: {sqlite: 'MAX'},
|
|
77
|
-
least: {sqlite: 'MIN'},
|
|
78
|
-
# PostgreSQL spells log2(x) as log(2, x), which no renaming carries.
|
|
79
|
-
log2: {postgresql: nil},
|
|
80
|
-
# MySQL's TRUNCATE insists on the second argument, where the others
|
|
81
|
-
# default it to zero; SQLite's trunc takes only the one.
|
|
82
|
-
trunc: {mysql: 'TRUNCATE'},
|
|
83
|
-
now: {sqlite: nil},
|
|
84
|
-
# The bit aggregates, which PostgreSQL and MySQL spell alike and
|
|
85
|
-
# SQLite has none of. PostgreSQL gained bit_xor in 14.
|
|
86
|
-
bit_and: {sqlite: nil}, bit_or: {sqlite: nil}, bit_xor: {sqlite: nil},
|
|
87
|
-
date_trunc: {sqlite: nil, mysql: nil},
|
|
88
|
-
# Named for Kernel#rand, which it also takes back: a block calling
|
|
89
|
-
# rand would otherwise get Ruby's and never reach the database.
|
|
90
|
-
rand: {sqlite: 'RANDOM', postgresql: 'RANDOM'},
|
|
91
|
-
# Two different functions share this name: printf formatting here, and
|
|
92
|
-
# on MySQL the one that puts separators in a number, which reads a
|
|
93
|
-
# printf template as the number zero rather than complaining. The
|
|
94
|
-
# name keeps the one meaning; fn(:format, ...) reaches MySQL's.
|
|
95
|
-
format: {mysql: nil},
|
|
96
|
-
}.freeze
|
|
97
|
-
|
|
98
|
-
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|
|
|
99
393
|
define_method(name) do |*args|
|
|
100
|
-
AST::Function.new(function_name(name,
|
|
394
|
+
AST::Function.new(dialect.function_name(name, @model), args)
|
|
101
395
|
end
|
|
102
396
|
end
|
|
103
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
|
+
#
|
|
104
420
|
# The datetime value functions, as the SQL grammar calls them. These
|
|
105
421
|
# the grammar has bare -- PostgreSQL and SQLite reject them written with
|
|
106
422
|
# parentheses -- and the one thing that does go into parentheses is an
|
|
@@ -108,27 +424,28 @@ module ActiveRecord
|
|
|
108
424
|
# takes and SQLite never accepts. The table reads like
|
|
109
425
|
# SCALAR_FUNCTIONS; current_timestamp is the portable spelling of what
|
|
110
426
|
# now means, reaching SQLite where now does not.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
current_time
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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 }
|
|
119
437
|
def current_date
|
|
120
|
-
AST::DatetimeValueFunction.new(
|
|
121
|
-
function_name(:current_date, DATETIME_VALUE_FUNCTIONS))
|
|
438
|
+
AST::DatetimeValueFunction.new(dialect.function_name(:current_date, @model))
|
|
122
439
|
end
|
|
123
440
|
|
|
124
|
-
(DATETIME_VALUE_FUNCTIONS
|
|
441
|
+
(DATETIME_VALUE_FUNCTIONS - [:current_date]).each do |name|
|
|
125
442
|
define_method(name) do |precision = nil|
|
|
126
443
|
# Built first so that a precision of the wrong type is an
|
|
127
444
|
# ArgumentError on every adapter, before SQLite gets to say it takes
|
|
128
445
|
# none at all.
|
|
129
446
|
node = AST::DatetimeValueFunction.new(
|
|
130
|
-
function_name(name,
|
|
131
|
-
if precision &&
|
|
447
|
+
dialect.function_name(name, @model), precision)
|
|
448
|
+
if precision && !dialect.datetime_precision_supported?
|
|
132
449
|
raise NotImplementedError,
|
|
133
450
|
"#{name} takes no precision on #{@model.connection_db_config.adapter}"
|
|
134
451
|
end
|
|
@@ -136,47 +453,109 @@ module ActiveRecord
|
|
|
136
453
|
end
|
|
137
454
|
end
|
|
138
455
|
|
|
139
|
-
# EXTRACT(field FROM expr)
|
|
140
|
-
# has to be a plain name
|
|
141
|
-
#
|
|
142
|
-
#
|
|
143
|
-
#
|
|
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.
|
|
144
468
|
def extract(field, expr)
|
|
145
469
|
node = AST::Extract.new(field, expr)
|
|
146
|
-
|
|
470
|
+
unless dialect.extract_supported?
|
|
147
471
|
raise NotImplementedError,
|
|
148
472
|
"extract has no equivalent on #{@model.connection_db_config.adapter}"
|
|
149
473
|
end
|
|
150
474
|
node
|
|
151
475
|
end
|
|
152
476
|
|
|
153
|
-
#
|
|
154
|
-
#
|
|
155
|
-
|
|
156
|
-
#
|
|
157
|
-
#
|
|
158
|
-
#
|
|
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
|
|
159
486
|
# Sale.group { grouping_sets([:region], [:product], []) }
|
|
160
|
-
#
|
|
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.
|
|
161
491
|
def grouping_sets(*sets)
|
|
162
492
|
grouping(:grouping_sets, sets)
|
|
163
493
|
end
|
|
164
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) }
|
|
165
501
|
def rollup(*columns)
|
|
166
502
|
grouping(:rollup, columns)
|
|
167
503
|
end
|
|
168
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) }
|
|
169
510
|
def cube(*columns)
|
|
170
511
|
grouping(:cube, columns)
|
|
171
512
|
end
|
|
172
513
|
|
|
173
|
-
#
|
|
174
|
-
#
|
|
175
|
-
|
|
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) }
|
|
176
524
|
def cast(expr, type)
|
|
177
525
|
AST::Cast.new(expr, type)
|
|
178
526
|
end
|
|
179
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
|
+
#
|
|
180
559
|
# The functions that only mean anything with a window. Every adapter
|
|
181
560
|
# that has window functions at all spells these the same -- PostgreSQL,
|
|
182
561
|
# MySQL 8, SQLite 3.25 -- so unlike the scalar functions there is nothing
|
|
@@ -186,190 +565,310 @@ module ActiveRecord
|
|
|
186
565
|
end
|
|
187
566
|
|
|
188
567
|
%i[ntile first_value last_value].each do |name|
|
|
189
|
-
define_method(name) {|arg| AST::WindowFunction.new(name.to_s.upcase, [arg]) }
|
|
568
|
+
define_method(name) { |arg| AST::WindowFunction.new(name.to_s.upcase, [arg]) }
|
|
190
569
|
end
|
|
191
570
|
|
|
571
|
+
# `NTH_VALUE(expr, nth)`; needs `over`.
|
|
572
|
+
# @return [AST::WindowFunction]
|
|
192
573
|
def nth_value(expr, nth)
|
|
193
|
-
AST::WindowFunction.new(
|
|
574
|
+
AST::WindowFunction.new("NTH_VALUE", [expr, nth])
|
|
194
575
|
end
|
|
195
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
|
+
#
|
|
196
583
|
# The offset is written out rather than left to default, so that a
|
|
197
584
|
# default value cannot end up where the offset belongs.
|
|
198
585
|
def lag(expr, offset = 1, default = nil)
|
|
199
|
-
AST::WindowFunction.new(
|
|
586
|
+
AST::WindowFunction.new("LAG", default.nil? ? [expr, offset] : [expr, offset, default])
|
|
200
587
|
end
|
|
201
588
|
|
|
589
|
+
# `LEAD(expr, offset, default)`: the value `offset` rows after this
|
|
590
|
+
# one; needs `over`.
|
|
591
|
+
# @return [AST::WindowFunction]
|
|
202
592
|
def lead(expr, offset = 1, default = nil)
|
|
203
|
-
AST::WindowFunction.new(
|
|
593
|
+
AST::WindowFunction.new("LEAD", default.nil? ? [expr, offset] : [expr, offset, default])
|
|
204
594
|
end
|
|
205
595
|
|
|
206
|
-
#
|
|
207
|
-
#
|
|
208
|
-
|
|
209
|
-
#
|
|
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.
|
|
210
613
|
def fn(name, *args)
|
|
211
614
|
AST::Function.new(
|
|
212
615
|
AST.check_name(name, AST::FUNCTION_NAME, "function name").to_s, args)
|
|
213
616
|
end
|
|
214
617
|
|
|
215
|
-
#
|
|
216
|
-
#
|
|
217
|
-
#
|
|
618
|
+
# Any binary operator by its spelling: `op("&&", :tags, "{ruby,sql}")`.
|
|
619
|
+
# The operator has to be made of operator characters; the operands are
|
|
620
|
+
# quoted as values unless they are columns or expressions, and
|
|
621
|
+
# parenthesized, since the operator's precedence is not known.
|
|
622
|
+
# @param operator [String]
|
|
623
|
+
# @return [AST::Operation]
|
|
624
|
+
# @example
|
|
625
|
+
# Post.where { op("&&", :tags, "{ruby,sql}") } # PostgreSQL arrays
|
|
626
|
+
def op(operator, left, right)
|
|
627
|
+
AST::Operation.new(operator, left, right)
|
|
628
|
+
end
|
|
629
|
+
|
|
630
|
+
# @!endgroup
|
|
631
|
+
# @!group Bits
|
|
632
|
+
|
|
633
|
+
# `BIT_COUNT(expr)`, the bits set in a number. MySQL and PostgreSQL
|
|
634
|
+
# have it; SQLite, Oracle and SQL Server do not.
|
|
635
|
+
# @return [AST::Function]
|
|
636
|
+
# @example
|
|
637
|
+
# Post.select { bit_count(:flags).as(:set) }
|
|
638
|
+
#
|
|
639
|
+
# MySQL counts the bits of a number; PostgreSQL counts those of a bit
|
|
640
|
+
# string, so the argument is cast, and to bit(64) because that is what
|
|
641
|
+
# makes a negative come back as MySQL has it -- 64 bits of two's
|
|
218
642
|
# complement rather than as many as the column happens to be wide.
|
|
219
643
|
def bit_count(expr)
|
|
220
|
-
|
|
221
|
-
when :mysql then AST::Function.new('BIT_COUNT', [expr])
|
|
222
|
-
when :postgresql
|
|
223
|
-
AST::Function.new('BIT_COUNT', [AST::Cast.new(expr, 'bit(64)')])
|
|
224
|
-
else
|
|
225
|
-
raise NotImplementedError,
|
|
226
|
-
"bit_count has no equivalent on #{@model.connection_db_config.adapter}"
|
|
227
|
-
end
|
|
644
|
+
dialect.bit_count(expr, @model)
|
|
228
645
|
end
|
|
229
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] }) }
|
|
230
656
|
def exists?(relation)
|
|
231
657
|
AST::Exists.new(relation)
|
|
232
658
|
end
|
|
233
659
|
|
|
234
|
-
# ANY
|
|
235
|
-
#
|
|
236
|
-
#
|
|
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
|
|
237
665
|
# Post.where { :likes > any(Post.published.select(:likes)) }
|
|
238
|
-
# Post.where { :likes >= all(Post.select(:likes)) }
|
|
239
666
|
#
|
|
240
|
-
#
|
|
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
|
|
241
670
|
# comparisons IN has no spelling for.
|
|
242
671
|
def any(relation)
|
|
243
|
-
quantified(
|
|
672
|
+
quantified("ANY", relation)
|
|
244
673
|
end
|
|
245
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)) }
|
|
246
681
|
def all(relation)
|
|
247
|
-
quantified(
|
|
248
|
-
end
|
|
249
|
-
|
|
250
|
-
#
|
|
251
|
-
#
|
|
252
|
-
|
|
253
|
-
#
|
|
254
|
-
#
|
|
255
|
-
#
|
|
256
|
-
#
|
|
682
|
+
quantified("ALL", relation)
|
|
683
|
+
end
|
|
684
|
+
|
|
685
|
+
# @!endgroup
|
|
686
|
+
# @!group Escape hatches
|
|
687
|
+
|
|
688
|
+
# SQL as written, the one way a string means SQL inside a block. `?`
|
|
689
|
+
# and `:name` placeholders take quoted values, as `where` takes them.
|
|
690
|
+
# @param statement [String]
|
|
691
|
+
# @return [AST::Sql]
|
|
692
|
+
# @example
|
|
693
|
+
# Post.where { sql("length(title) > ?", 10) }
|
|
694
|
+
# Post.select { sql("count(*) FILTER (WHERE score > 0) AS positive") }
|
|
695
|
+
def sql(statement, *binds)
|
|
696
|
+
AST::Sql.new(statement, binds)
|
|
697
|
+
end
|
|
698
|
+
|
|
699
|
+
# A literal where an expression is expected, quoted like any other
|
|
700
|
+
# value. A number or a string takes `as` for itself -- `0.as(:depth)`
|
|
701
|
+
# -- so this is the spelling for the rest: `true`, `nil`, a date.
|
|
702
|
+
# @return [AST::Value]
|
|
703
|
+
# @example
|
|
704
|
+
# Node.select { [:id, value(0).as(:depth)] }
|
|
705
|
+
# Post.select { [:title, value(nil).as(:score)] }
|
|
257
706
|
def value(literal)
|
|
258
707
|
AST::Value.new(literal)
|
|
259
708
|
end
|
|
260
709
|
|
|
261
|
-
# The row an upsert could not insert,
|
|
262
|
-
# PostgreSQL and SQLite
|
|
263
|
-
#
|
|
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) } }
|
|
264
717
|
def excluded(column)
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
quoted = @model.with_connection {|c| c.quote_column_name(column) }
|
|
268
|
-
AST::Function.new('VALUES', [Arel::Nodes::SqlLiteral.new(quoted)])
|
|
718
|
+
dialect.excluded(column, @model)
|
|
269
719
|
end
|
|
270
720
|
|
|
271
|
-
#
|
|
272
|
-
#
|
|
273
|
-
|
|
274
|
-
# `
|
|
275
|
-
#
|
|
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
|
|
276
730
|
# self.case(:age).when(10).then(1).else(0)
|
|
277
731
|
# self.case.when { :age >= 60 }.then { :age - 60 }
|
|
278
732
|
def case(operand = nil)
|
|
279
733
|
AST::Case.new(operand)
|
|
280
734
|
end
|
|
281
735
|
|
|
282
|
-
# The searched CASE
|
|
283
|
-
#
|
|
284
|
-
#
|
|
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) }
|
|
285
743
|
def case_when(value = nil, &block)
|
|
286
744
|
AST::Case.new.when(value, &block)
|
|
287
745
|
end
|
|
288
746
|
|
|
289
747
|
private
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
748
|
+
# @!endgroup
|
|
749
|
+
#
|
|
750
|
+
# The group closes here rather than above `private`: a comment on
|
|
751
|
+
# that line belongs to the `private` call, which reads no
|
|
752
|
+
# directives, and the group would run on into the next module.
|
|
753
|
+
#
|
|
754
|
+
# SQLite is the one adapter with no quantifier at all, and what it says
|
|
755
|
+
# when it meets one is a syntax error at the SELECT.
|
|
756
|
+
def quantified(kind, relation)
|
|
757
|
+
unless dialect.quantifiers_supported?
|
|
758
|
+
raise NotImplementedError,
|
|
759
|
+
"#{kind} has no equivalent on #{@model.connection_db_config.adapter}"
|
|
760
|
+
end
|
|
761
|
+
AST::Quantified.new(kind, relation)
|
|
297
762
|
end
|
|
298
|
-
AST::Quantified.new(kind, relation)
|
|
299
|
-
end
|
|
300
763
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
764
|
+
def grouping(kind, sets)
|
|
765
|
+
node = AST::GroupingSets.new(kind, sets)
|
|
766
|
+
return node if dialect.grouping_supported?(kind)
|
|
767
|
+
|
|
304
768
|
raise NotImplementedError,
|
|
305
769
|
"#{kind} has no equivalent on #{@model.connection_db_config.adapter}"
|
|
306
770
|
end
|
|
307
|
-
node
|
|
308
|
-
end
|
|
309
771
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
spellings.fetch(adapter_family) ||
|
|
314
|
-
raise(NotImplementedError,
|
|
315
|
-
"#{name} has no equivalent on #{@model.connection_db_config.adapter}")
|
|
316
|
-
end
|
|
317
|
-
|
|
318
|
-
def adapter_family
|
|
319
|
-
@adapter_family ||= AST.adapter_family(@model)
|
|
320
|
-
end
|
|
772
|
+
def dialect
|
|
773
|
+
@dialect ||= Dialect.for(@model)
|
|
774
|
+
end
|
|
321
775
|
end
|
|
322
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)] }
|
|
323
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%") }
|
|
324
799
|
def where(opts = nil, *rest, &block)
|
|
325
800
|
if block
|
|
326
|
-
super(evaluate_block(&block)
|
|
801
|
+
super(to_arel_condition(evaluate_block(&block)))
|
|
327
802
|
else
|
|
328
803
|
super
|
|
329
804
|
end
|
|
330
805
|
end
|
|
331
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)] }
|
|
332
812
|
def select(*fields, &block)
|
|
333
813
|
if block
|
|
334
|
-
|
|
335
|
-
arel = Array(result).map {|node| to_arel_field(node) }
|
|
336
|
-
super(*arel, &nil)
|
|
814
|
+
super(*to_arel_fields(evaluate_block(&block)), &nil)
|
|
337
815
|
else
|
|
338
816
|
super
|
|
339
817
|
end
|
|
340
818
|
end
|
|
341
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 }
|
|
342
824
|
def having(opts = nil, *rest, &block)
|
|
343
825
|
if block
|
|
344
|
-
super(evaluate_block(&block)
|
|
826
|
+
super(to_arel_condition(evaluate_block(&block)))
|
|
345
827
|
else
|
|
346
828
|
super
|
|
347
829
|
end
|
|
348
830
|
end
|
|
349
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] }
|
|
350
837
|
def order(*args, &block)
|
|
351
838
|
if block
|
|
352
|
-
|
|
353
|
-
arel = Array(result).map {|node| to_arel_field(node) }
|
|
354
|
-
super(*arel, &nil)
|
|
839
|
+
super(*to_arel_fields(evaluate_block(&block)), &nil)
|
|
355
840
|
else
|
|
356
841
|
super
|
|
357
842
|
end
|
|
358
843
|
end
|
|
359
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(:*)] }
|
|
360
851
|
def group(*args, &block)
|
|
361
852
|
if block
|
|
362
853
|
result = evaluate_block(&block)
|
|
363
|
-
|
|
364
|
-
super(*
|
|
854
|
+
check_rollup_stands_alone(result)
|
|
855
|
+
super(*to_arel_fields(result), &nil)
|
|
365
856
|
else
|
|
366
857
|
super
|
|
367
858
|
end
|
|
368
859
|
end
|
|
369
860
|
|
|
370
|
-
#
|
|
371
|
-
#
|
|
372
|
-
#
|
|
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.
|
|
373
872
|
def from(value, subquery_name = nil, as: nil)
|
|
374
873
|
unless value.is_a?(Symbol)
|
|
375
874
|
if as
|
|
@@ -382,12 +881,16 @@ module ActiveRecord
|
|
|
382
881
|
super(arel_table, subquery_name)
|
|
383
882
|
end
|
|
384
883
|
|
|
385
|
-
# Selects a CTE in place of the model's own table
|
|
386
|
-
#
|
|
387
|
-
#
|
|
388
|
-
#
|
|
389
|
-
#
|
|
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)
|
|
390
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.
|
|
391
894
|
# The name is checked against what `with` declares, so that a typo is
|
|
392
895
|
# not a query against a table nobody has. Checked when the SQL is
|
|
393
896
|
# built, since the CTE may be declared after this in the chain, or by a
|
|
@@ -401,28 +904,31 @@ module ActiveRecord
|
|
|
401
904
|
relation
|
|
402
905
|
end
|
|
403
906
|
|
|
907
|
+
# @private
|
|
404
908
|
def from_cte_value
|
|
405
909
|
@values[:from_cte]
|
|
406
910
|
end
|
|
407
911
|
|
|
912
|
+
# @private
|
|
408
913
|
def from_cte_value=(name)
|
|
409
914
|
assert_modifiable!
|
|
410
915
|
@values[:from_cte] = name
|
|
411
916
|
end
|
|
412
917
|
|
|
413
|
-
# DISTINCT ON (
|
|
414
|
-
# brings up. PostgreSQL has it
|
|
415
|
-
#
|
|
416
|
-
#
|
|
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] }
|
|
417
924
|
#
|
|
418
|
-
#
|
|
419
|
-
#
|
|
420
|
-
# The portable shape is a row_number window in a subquery, which the
|
|
421
|
-
# README shows.
|
|
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.
|
|
422
927
|
def distinct_on(*columns, &block)
|
|
423
928
|
spawn.distinct_on!(*columns, &block)
|
|
424
929
|
end
|
|
425
930
|
|
|
931
|
+
# {#distinct_on} on the relation itself.
|
|
426
932
|
def distinct_on!(*columns, &block)
|
|
427
933
|
columns = Array(evaluate_block(&block)) if block
|
|
428
934
|
if columns.empty?
|
|
@@ -434,39 +940,54 @@ module ActiveRecord
|
|
|
434
940
|
|
|
435
941
|
# Active Record generates these for the values it knows about; this one
|
|
436
942
|
# is ours, and lives in the same place so that it survives a spawn.
|
|
943
|
+
# @private
|
|
437
944
|
def distinct_on_values
|
|
438
945
|
@values.fetch(:distinct_on, ActiveRecord::QueryMethods::FROZEN_EMPTY_ARRAY)
|
|
439
946
|
end
|
|
440
947
|
|
|
948
|
+
# @private
|
|
441
949
|
def distinct_on_values=(columns)
|
|
442
950
|
assert_modifiable!
|
|
443
951
|
@values[:distinct_on] = columns
|
|
444
952
|
end
|
|
445
953
|
|
|
446
|
-
# Marks the relation for a
|
|
447
|
-
# joined to -- the top few rows of each group, and the
|
|
448
|
-
#
|
|
449
|
-
#
|
|
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]] }
|
|
450
961
|
def lateral
|
|
451
962
|
spawn.lateral!
|
|
452
963
|
end
|
|
453
964
|
|
|
965
|
+
# {#lateral} on the relation itself.
|
|
454
966
|
def lateral!
|
|
455
967
|
self.lateral_value = true
|
|
456
968
|
self
|
|
457
969
|
end
|
|
458
970
|
|
|
971
|
+
# @private
|
|
459
972
|
def lateral_value
|
|
460
973
|
@values[:lateral]
|
|
461
974
|
end
|
|
462
975
|
|
|
976
|
+
# @private
|
|
463
977
|
def lateral_value=(value)
|
|
464
978
|
assert_modifiable!
|
|
465
979
|
@values[:lateral] = value
|
|
466
980
|
end
|
|
467
981
|
|
|
468
|
-
# `
|
|
469
|
-
#
|
|
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] }
|
|
470
991
|
def joins(*args, as: nil, &block)
|
|
471
992
|
if args.first.is_a?(ActiveRecord::Relation)
|
|
472
993
|
super(build_lateral_join(args.first, Arel::Nodes::InnerJoin, as, &block))
|
|
@@ -478,6 +999,10 @@ module ActiveRecord
|
|
|
478
999
|
end
|
|
479
1000
|
end
|
|
480
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] }
|
|
481
1006
|
def left_outer_joins(*args, as: nil, &block)
|
|
482
1007
|
if args.first.is_a?(ActiveRecord::Relation)
|
|
483
1008
|
joins(build_lateral_join(args.first, Arel::Nodes::OuterJoin, as, &block))
|
|
@@ -489,6 +1014,12 @@ module ActiveRecord
|
|
|
489
1014
|
end
|
|
490
1015
|
end
|
|
491
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
|
+
#
|
|
492
1023
|
# The other two outer joins, which Active Record has no method for and
|
|
493
1024
|
# Arel has the nodes for. The rules are joins': the block is the ON,
|
|
494
1025
|
# `as` names the table within the query, a relation marked `lateral`
|
|
@@ -499,156 +1030,192 @@ module ActiveRecord
|
|
|
499
1030
|
args, as, &block)
|
|
500
1031
|
end
|
|
501
1032
|
|
|
1033
|
+
# `FULL OUTER JOIN`, as {#right_outer_joins} takes it. The MySQL
|
|
1034
|
+
# family has none.
|
|
1035
|
+
# @param as [Symbol, nil]
|
|
502
1036
|
def full_outer_joins(*args, as: nil, &block)
|
|
503
1037
|
check_full_outer_support
|
|
504
1038
|
outer_joins(:full_outer_joins, Arel::Nodes::FullOuterJoin,
|
|
505
1039
|
args, as, &block)
|
|
506
1040
|
end
|
|
507
1041
|
|
|
508
|
-
# CROSS JOIN
|
|
509
|
-
#
|
|
510
|
-
#
|
|
511
|
-
#
|
|
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
|
|
512
1046
|
# Post.cross_joins(:authors)
|
|
513
1047
|
# Post.cross_joins(:posts, as: :others)
|
|
514
1048
|
def cross_joins(*args, as: nil, &block)
|
|
515
1049
|
if block
|
|
516
1050
|
raise ArgumentError,
|
|
517
|
-
|
|
1051
|
+
"a cross join has no condition; joins is the one that takes a block"
|
|
518
1052
|
end
|
|
519
1053
|
joins(build_cross_join(args.first, as))
|
|
520
1054
|
end
|
|
521
1055
|
|
|
522
1056
|
private
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
1057
|
+
def build_arel(...)
|
|
1058
|
+
check_from_cte
|
|
1059
|
+
arel = super
|
|
1060
|
+
unless distinct_on_values.empty?
|
|
1061
|
+
arel.distinct_on(distinct_on_values.map { |column| to_arel_field(column) })
|
|
1062
|
+
end
|
|
1063
|
+
arel
|
|
529
1064
|
end
|
|
530
|
-
arel
|
|
531
|
-
end
|
|
532
1065
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
1066
|
+
# Only when every `with` is one this can read the names out of; anything
|
|
1067
|
+
# else and there is nothing to be sure about, so nothing is said.
|
|
1068
|
+
def check_from_cte
|
|
1069
|
+
name = from_cte_value
|
|
1070
|
+
return unless name
|
|
1071
|
+
return unless with_values.all? { |value| value.is_a?(::Hash) }
|
|
539
1072
|
|
|
540
|
-
|
|
541
|
-
|
|
1073
|
+
declared = with_values.flat_map { |value| value.keys.map(&:to_sym) }
|
|
1074
|
+
return if declared.include?(name)
|
|
542
1075
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
def evaluate_block(&block)
|
|
550
|
-
refined_block = block.refined(ActiveRecord::Refined::BlockSyntax)
|
|
551
|
-
BlockContext.new(klass).instance_exec(&refined_block)
|
|
552
|
-
end
|
|
1076
|
+
raise ArgumentError,
|
|
1077
|
+
"from_cte(#{name.inspect}) names no CTE; " +
|
|
1078
|
+
(declared.empty? ? "this query declares none" :
|
|
1079
|
+
"this query declares #{declared.map(&:inspect).join(', ')}")
|
|
1080
|
+
end
|
|
553
1081
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
when Symbol then table[node]
|
|
558
|
-
else node
|
|
1082
|
+
def evaluate_block(&block)
|
|
1083
|
+
refined_block = block.refined(ActiveRecord::Refined::BlockSyntax)
|
|
1084
|
+
BlockContext.new(klass).instance_exec(&refined_block)
|
|
559
1085
|
end
|
|
560
|
-
end
|
|
561
1086
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
1087
|
+
# WITH ROLLUP trails the whole group list, so on the MySQL family a
|
|
1088
|
+
# rollup cannot stand beside other group entries the way PostgreSQL's
|
|
1089
|
+
# ROLLUP(...) can.
|
|
1090
|
+
def check_rollup_stands_alone(result)
|
|
1091
|
+
entries = Array(result)
|
|
1092
|
+
return if entries.size == 1
|
|
1093
|
+
return unless entries.any? { |node| node.is_a?(AST::GroupingSets) }
|
|
1094
|
+
return unless Dialect.for(klass).grouping_by_with_rollup?
|
|
566
1095
|
|
|
567
|
-
# The subquery is written out rather than handed over as a tree: Arel has
|
|
568
|
-
# a LATERAL node but only PostgreSQL's visitor writes it, and MySQL can
|
|
569
|
-
# read what it will not write. Without a block the join is ON TRUE,
|
|
570
|
-
# which is the usual shape -- what the subquery is allowed to see is
|
|
571
|
-
# what makes it lateral, and that is said inside it.
|
|
572
|
-
def build_lateral_join(relation, join_class, alias_name, &block)
|
|
573
|
-
unless relation.lateral_value
|
|
574
1096
|
raise ArgumentError,
|
|
575
|
-
"
|
|
1097
|
+
"WITH ROLLUP takes the whole group list; group by the rollup alone"
|
|
576
1098
|
end
|
|
577
|
-
|
|
578
|
-
|
|
1099
|
+
|
|
1100
|
+
def to_arel_condition(result)
|
|
1101
|
+
return result if result.is_a?(Arel::Nodes::SqlLiteral)
|
|
1102
|
+
if result.is_a?(::String)
|
|
1103
|
+
raise ArgumentError,
|
|
1104
|
+
"#{result.inspect} is a string, not a condition; sql(...) " \
|
|
1105
|
+
"writes one as SQL"
|
|
1106
|
+
end
|
|
1107
|
+
result.to_arel(table, klass)
|
|
579
1108
|
end
|
|
580
|
-
check_lateral_support
|
|
581
1109
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
1110
|
+
# The top of a select, order or group list. A bare string is refused
|
|
1111
|
+
# rather than passed to Active Record, where it would be SQL: inside a
|
|
1112
|
+
# block a string is a value in every other position, and a literal
|
|
1113
|
+
# whose meaning turns on where it stands is how an interpolation
|
|
1114
|
+
# becomes an injection.
|
|
1115
|
+
def to_arel_fields(result)
|
|
1116
|
+
fields =
|
|
1117
|
+
if result.nil? then []
|
|
1118
|
+
elsif result.is_a?(::Array) then result
|
|
1119
|
+
else [result]
|
|
1120
|
+
end
|
|
1121
|
+
fields.map do |node|
|
|
1122
|
+
if node.is_a?(::String) && !node.is_a?(Arel::Nodes::SqlLiteral)
|
|
1123
|
+
raise ArgumentError,
|
|
1124
|
+
"#{node.inspect} could mean SQL or a string; " \
|
|
1125
|
+
"sql(...) says the SQL, value(...) the string"
|
|
1126
|
+
end
|
|
1127
|
+
to_arel_field(node)
|
|
1128
|
+
end
|
|
1129
|
+
end
|
|
587
1130
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
when :mysql
|
|
596
|
-
refuse_lateral('MariaDB') if klass.with_connection {|c| c.mariadb? }
|
|
1131
|
+
def to_arel_field(node)
|
|
1132
|
+
case node
|
|
1133
|
+
when AST::Sql then node.field_arel(klass)
|
|
1134
|
+
when AST::Node then node.to_arel(table, klass)
|
|
1135
|
+
when Symbol then table[node]
|
|
1136
|
+
else node
|
|
1137
|
+
end
|
|
597
1138
|
end
|
|
598
|
-
end
|
|
599
1139
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
1140
|
+
def reject_join_alias(alias_name)
|
|
1141
|
+
return unless alias_name
|
|
1142
|
+
raise ArgumentError, "as: needs a block to write the ON clause with"
|
|
1143
|
+
end
|
|
603
1144
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
1145
|
+
# The subquery is written out rather than handed over as a tree: Arel has
|
|
1146
|
+
# a LATERAL node but only PostgreSQL's visitor writes it, and MySQL can
|
|
1147
|
+
# read what it will not write. Without a block the join is ON TRUE,
|
|
1148
|
+
# which is the usual shape -- what the subquery is allowed to see is
|
|
1149
|
+
# what makes it lateral, and that is said inside it.
|
|
1150
|
+
def build_lateral_join(relation, join_class, alias_name, &block)
|
|
1151
|
+
unless relation.lateral_value
|
|
1152
|
+
raise ArgumentError,
|
|
1153
|
+
"a relation joins laterally; mark it: joins(sub.lateral, as: :top)"
|
|
1154
|
+
end
|
|
1155
|
+
unless alias_name
|
|
1156
|
+
raise ArgumentError, "a lateral join needs a name: joins(..., as: :top)"
|
|
1157
|
+
end
|
|
1158
|
+
check_lateral_support
|
|
610
1159
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
1160
|
+
aliased = Arel::Nodes::TableAlias.new(
|
|
1161
|
+
Arel::Nodes::SqlLiteral.new("LATERAL (#{relation.to_sql})"), alias_name)
|
|
1162
|
+
on = block ? evaluate_block(&block).to_arel(table, klass) : Arel::Nodes::True.new
|
|
1163
|
+
join_class.new(aliased, Arel::Nodes::On.new(on))
|
|
614
1164
|
end
|
|
615
|
-
return joins(build_join_node(args.first, join_class, alias_name, &block)) if block
|
|
616
1165
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
end
|
|
1166
|
+
def check_lateral_support
|
|
1167
|
+
Dialect.for(klass).check_lateral(klass)
|
|
1168
|
+
end
|
|
621
1169
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
# place in the gem that writes any: the keyword is fixed and the names
|
|
626
|
-
# are quoted by the adapter, so nothing of the caller's is in it.
|
|
627
|
-
def build_cross_join(target_table, alias_name)
|
|
628
|
-
joined = klass.with_connection do |connection|
|
|
629
|
-
name = connection.quote_table_name(target_table.to_s)
|
|
630
|
-
alias_name ? "#{name} #{connection.quote_table_name(alias_name.to_s)}" : name
|
|
1170
|
+
def check_full_outer_support
|
|
1171
|
+
return if Dialect.for(klass).full_outer_join_supported?
|
|
1172
|
+
raise NotImplementedError, "a full outer join has no equivalent on MySQL"
|
|
631
1173
|
end
|
|
632
|
-
Arel::Nodes::StringJoin.new(Arel.sql("CROSS JOIN #{joined}"))
|
|
633
|
-
end
|
|
634
1174
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
1175
|
+
def outer_joins(called, join_class, args, alias_name, &block)
|
|
1176
|
+
if args.first.is_a?(ActiveRecord::Relation)
|
|
1177
|
+
return joins(build_lateral_join(args.first, join_class, alias_name, &block))
|
|
1178
|
+
end
|
|
1179
|
+
return joins(build_join_node(args.first, join_class, alias_name, &block)) if block
|
|
1180
|
+
|
|
1181
|
+
raise ArgumentError,
|
|
1182
|
+
"#{called} takes a table and the block that joins it; an association " \
|
|
1183
|
+
"is what joins and left_outer_joins read"
|
|
1184
|
+
end
|
|
1185
|
+
|
|
1186
|
+
# Arel has a node for every other join and none for this one, and INNER
|
|
1187
|
+
# JOIN with no ON -- which is a cross join on SQLite and MySQL -- is a
|
|
1188
|
+
# syntax error on PostgreSQL. So the SQL is written here, the second
|
|
1189
|
+
# place in the gem that writes any: the keyword is fixed and the names
|
|
1190
|
+
# are quoted by the adapter, so nothing of the caller's is in it.
|
|
1191
|
+
def build_cross_join(target_table, alias_name)
|
|
1192
|
+
joined = klass.with_connection do |connection|
|
|
1193
|
+
name = connection.quote_table_name(target_table.to_s)
|
|
1194
|
+
alias_name ? "#{name} #{connection.quote_table_name(alias_name.to_s)}" : name
|
|
1195
|
+
end
|
|
1196
|
+
Arel::Nodes::StringJoin.new(Arel.sql("CROSS JOIN #{joined}"))
|
|
1197
|
+
end
|
|
1198
|
+
|
|
1199
|
+
def build_join_node(target_table, join_class, alias_name, &block)
|
|
1200
|
+
ast = evaluate_block(&block)
|
|
1201
|
+
arel_table = Arel::Table.new(target_table)
|
|
1202
|
+
arel_table = arel_table.alias(alias_name) if alias_name
|
|
1203
|
+
join_class.new(arel_table, Arel::Nodes::On.new(ast.to_arel(table, klass)))
|
|
1204
|
+
end
|
|
641
1205
|
end
|
|
642
1206
|
|
|
643
1207
|
# The writing statements, which live on Relation rather than in
|
|
644
1208
|
# QueryMethods. What a block adds here is the one thing their arguments
|
|
645
1209
|
# cannot carry: a value worked out from the row rather than given.
|
|
646
1210
|
module Writes
|
|
647
|
-
# `
|
|
648
|
-
#
|
|
649
|
-
#
|
|
650
|
-
#
|
|
651
|
-
#
|
|
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) } }
|
|
652
1219
|
def update_all(updates = nil, &block)
|
|
653
1220
|
return super(updates) unless block
|
|
654
1221
|
if updates
|
|
@@ -658,16 +1225,20 @@ module ActiveRecord
|
|
|
658
1225
|
unless result.is_a?(::Hash)
|
|
659
1226
|
raise ArgumentError, "the block gives update_all a hash of column => value"
|
|
660
1227
|
end
|
|
661
|
-
super(result.transform_values {|value| to_arel_field(value) })
|
|
1228
|
+
super(result.transform_values { |value| to_arel_field(value) })
|
|
662
1229
|
end
|
|
663
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
|
+
#
|
|
664
1239
|
# upsert_all's on_duplicate takes SQL text and nothing else, so this is
|
|
665
1240
|
# the one place the DSL writes the SQL out itself rather than handing
|
|
666
|
-
# Arel a tree.
|
|
667
|
-
#
|
|
668
|
-
# Post.upsert_all(rows, unique_by: :title) {
|
|
669
|
-
# { likes: :likes + excluded(:likes) }
|
|
670
|
-
# }
|
|
1241
|
+
# Arel a tree.
|
|
671
1242
|
def upsert_all(attributes, **options, &block)
|
|
672
1243
|
return super(attributes, **options) unless block
|
|
673
1244
|
if options.key?(:on_duplicate)
|
|
@@ -684,19 +1255,18 @@ module ActiveRecord
|
|
|
684
1255
|
end
|
|
685
1256
|
|
|
686
1257
|
private
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
end
|
|
1258
|
+
# The left of each assignment is the column being written, which is bare
|
|
1259
|
+
# -- the statement is already about one table -- and the right is the
|
|
1260
|
+
# expression, compiled here because a string is what on_duplicate reads.
|
|
1261
|
+
def set_clause(updates)
|
|
1262
|
+
klass.with_connection do |connection|
|
|
1263
|
+
updates.map do |column, value|
|
|
1264
|
+
expression = connection.visitor.compile(
|
|
1265
|
+
to_arel_field(value), Arel::Collectors::SQLString.new)
|
|
1266
|
+
"#{connection.quote_column_name(column)}=#{expression}"
|
|
1267
|
+
end.join(", ")
|
|
1268
|
+
end
|
|
698
1269
|
end
|
|
699
|
-
end
|
|
700
1270
|
end
|
|
701
1271
|
end
|
|
702
1272
|
end
|