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