activerecord-refined 0.10.2 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/ctes.md +2 -2
- data/docs/expressions.md +37 -20
- data/docs/functions.md +14 -10
- data/docs/index.md +2 -17
- data/docs/windows.md +2 -2
- data/examples/expressions.rb +21 -15
- 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 +39 -2364
- 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/dialect/mysql_compat.rb +16 -0
- data/lib/active_record/refined/dialect/mysqlish_json_functions.rb +45 -0
- data/lib/active_record/refined/dialect/oracle.rb +1 -7
- data/lib/active_record/refined/dialect/postgresql.rb +8 -0
- data/lib/active_record/refined/dialect/sql_server.rb +29 -5
- data/lib/active_record/refined/dialect/sqlite.rb +23 -0
- data/lib/active_record/refined/dialect.rb +132 -66
- data/lib/active_record/refined/query_methods.rb +444 -0
- data/lib/{activerecord-refined → active_record/refined}/version.rb +3 -2
- data/lib/active_record/refined/writes.rb +72 -0
- data/lib/active_record/refined.rb +7 -1270
- data/lib/activerecord-refined.rb +0 -4
- metadata +36 -5
|
@@ -0,0 +1,671 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_record/refined/ast"
|
|
4
|
+
require "active_record/refined/dialect"
|
|
5
|
+
|
|
6
|
+
module ActiveRecord
|
|
7
|
+
module Refined
|
|
8
|
+
# What a block can call: the aggregates, the functions, CASE, and the
|
|
9
|
+
# escape hatches. A block is evaluated with one of these as `self`, so
|
|
10
|
+
# its methods are called bare -- `count(:*)`, `upper(:name)` -- and each
|
|
11
|
+
# gives back an expression that compares, aliases and orders like a
|
|
12
|
+
# column does (see {BlockSyntax}).
|
|
13
|
+
#
|
|
14
|
+
# Where a function is spelled differently from one database to the next,
|
|
15
|
+
# the method names the one meaning and the adapter gets its own
|
|
16
|
+
# spelling; where a database has no equivalent, the method raises
|
|
17
|
+
# `NotImplementedError` as the block is read, rather than leaving the
|
|
18
|
+
# database to reject the SQL.
|
|
19
|
+
#
|
|
20
|
+
# @example
|
|
21
|
+
# Author.select { [upper(:name).as(:author), count(:*).as(:posts)] }
|
|
22
|
+
# Author.having { count(:*) > 1 }
|
|
23
|
+
class BlockContext
|
|
24
|
+
# The model is only consulted to learn which adapter the query is being
|
|
25
|
+
# built for, which is what decides how a scalar function is spelled.
|
|
26
|
+
# @api private
|
|
27
|
+
def initialize(model)
|
|
28
|
+
@model = model
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @!group Aggregates
|
|
32
|
+
|
|
33
|
+
# @!method sum(column, distinct: false)
|
|
34
|
+
# `SUM(column)`, or `SUM(DISTINCT column)`.
|
|
35
|
+
# @return [AST::Aggregate]
|
|
36
|
+
# @!method avg(column, distinct: false)
|
|
37
|
+
# `AVG(column)`, or `AVG(DISTINCT column)`.
|
|
38
|
+
# @return [AST::Aggregate]
|
|
39
|
+
# @!method min(column)
|
|
40
|
+
# `MIN(column)`.
|
|
41
|
+
# @return [AST::Aggregate]
|
|
42
|
+
# @!method max(column)
|
|
43
|
+
# `MAX(column)`.
|
|
44
|
+
# @return [AST::Aggregate]
|
|
45
|
+
# @private
|
|
46
|
+
AGGREGATE_FUNCTIONS = {
|
|
47
|
+
sum: :sum, avg: :average, min: :minimum, max: :maximum,
|
|
48
|
+
}.freeze
|
|
49
|
+
|
|
50
|
+
# count, sum and avg take distinct: true, for the aggregate over each
|
|
51
|
+
# value once; min and max would answer the same with or without it,
|
|
52
|
+
# so they take no such thing.
|
|
53
|
+
AGGREGATE_FUNCTIONS.each do |name, arel_func|
|
|
54
|
+
if AST::Aggregate::DISTINCT_FUNCTIONS.include?(arel_func)
|
|
55
|
+
define_method(name) do |column, distinct: false|
|
|
56
|
+
AST::Aggregate.new(column, arel_func, distinct: distinct)
|
|
57
|
+
end
|
|
58
|
+
else
|
|
59
|
+
define_method(name) { |column| AST::Aggregate.new(column, arel_func) }
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# `COUNT(column)`; `:*` for `COUNT(*)`, `distinct: true` for
|
|
64
|
+
# `COUNT(DISTINCT column)`. Every aggregate takes {AST::Aggregate#filter}
|
|
65
|
+
# for the rows it is taken over, and {AST::Windowing#over} for a window.
|
|
66
|
+
# @param column [Symbol, AST::Node, :*]
|
|
67
|
+
# @return [AST::Aggregate]
|
|
68
|
+
# @example
|
|
69
|
+
# Author.group { :country }.having { count(:*) > 1 }
|
|
70
|
+
# Post.select { count(:author_id, distinct: true) }
|
|
71
|
+
# Author.select { count(:*).filter { :age < 50 }.as(:young) }
|
|
72
|
+
def count(column, distinct: false)
|
|
73
|
+
AST::Aggregate.new(column, :count, distinct: distinct)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# The rows of a group gathered into one JSON array, a value from each:
|
|
77
|
+
# `jsonb_agg` on PostgreSQL, `json_group_array` on SQLite,
|
|
78
|
+
# `JSON_ARRAYAGG` on the MySQL family and Oracle; SQL Server has none.
|
|
79
|
+
# What it gives is JSON, which compares as a dug value does.
|
|
80
|
+
# @return [AST::JsonAggregate]
|
|
81
|
+
# @example
|
|
82
|
+
# Post.group { :author_id }.select { json_arrayagg(:title).as(:titles) }
|
|
83
|
+
def json_arrayagg(value)
|
|
84
|
+
AST::JsonAggregate.new(:arrayagg, [value])
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# The rows of a group gathered into one JSON object, a key and a value
|
|
88
|
+
# from each, named as {#json_arrayagg} is; SQL Server has none.
|
|
89
|
+
# @return [AST::JsonAggregate]
|
|
90
|
+
# @example
|
|
91
|
+
# Post.select { json_objectagg(:title, :meta.dig(:stars)).as(:stars) }
|
|
92
|
+
def json_objectagg(key, value)
|
|
93
|
+
AST::JsonAggregate.new(:objectagg, [key, value])
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# The strings of a group joined into one, a separator between:
|
|
97
|
+
# `STRING_AGG` on PostgreSQL and SQL Server, `group_concat` on SQLite,
|
|
98
|
+
# `GROUP_CONCAT` on MySQL, `LISTAGG` on Oracle. Takes
|
|
99
|
+
# {AST::StringAggregate#order} for the order they are joined in.
|
|
100
|
+
# @param separator [String] the comma GROUP_CONCAT defaults to, unless given
|
|
101
|
+
# @return [AST::StringAggregate]
|
|
102
|
+
# @example
|
|
103
|
+
# Post.group { :author_id }.
|
|
104
|
+
# select { string_agg(:title, ", ").order(:title).as(:titles) }
|
|
105
|
+
def string_agg(value, separator = ",")
|
|
106
|
+
AST::StringAggregate.new(value, separator)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# @!endgroup
|
|
110
|
+
# @!group JSON
|
|
111
|
+
|
|
112
|
+
# A JSON array built in the row from the values given. SQL Server
|
|
113
|
+
# spells the pair its own way and is not carried yet.
|
|
114
|
+
# @return [AST::JsonBuild]
|
|
115
|
+
# @example
|
|
116
|
+
# Post.select { json_array(:title, :likes).as(:pair) }
|
|
117
|
+
def json_array(*values)
|
|
118
|
+
AST::JsonBuild.new(:array, values)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# A JSON object built in the row from a hash whose values are
|
|
122
|
+
# expressions. SQL Server spells the pair its own way and is not
|
|
123
|
+
# carried yet.
|
|
124
|
+
# @param pairs [Hash{Symbol, String => Object}]
|
|
125
|
+
# @return [AST::JsonBuild]
|
|
126
|
+
# @example
|
|
127
|
+
# Post.select { json_object(title: :title, stars: :meta.dig(:stars)).as(:doc) }
|
|
128
|
+
def json_object(pairs = {})
|
|
129
|
+
AST::JsonBuild.new(:object, pairs)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# @!endgroup
|
|
133
|
+
# @!group Scalar functions
|
|
134
|
+
|
|
135
|
+
# @!method abs(x)
|
|
136
|
+
# `ABS(x)`.
|
|
137
|
+
# @return [AST::Function]
|
|
138
|
+
# @!method acos(x)
|
|
139
|
+
# `ACOS(x)`.
|
|
140
|
+
# @return [AST::Function]
|
|
141
|
+
# @!method asin(x)
|
|
142
|
+
# `ASIN(x)`.
|
|
143
|
+
# @return [AST::Function]
|
|
144
|
+
# @!method atan(x)
|
|
145
|
+
# `ATAN(x)`.
|
|
146
|
+
# @return [AST::Function]
|
|
147
|
+
# @!method atan2(y, x)
|
|
148
|
+
# `ATAN2(y, x)`: `ATN2` on SQL Server.
|
|
149
|
+
# @return [AST::Function]
|
|
150
|
+
# @!method ceil(x)
|
|
151
|
+
# `CEIL(x)`: `CEILING` on SQL Server.
|
|
152
|
+
# @return [AST::Function]
|
|
153
|
+
# @!method coalesce(*values)
|
|
154
|
+
# `COALESCE(a, b, ...)`: the first that is not NULL.
|
|
155
|
+
# @return [AST::Function]
|
|
156
|
+
# @!method concat(*strings)
|
|
157
|
+
# `CONCAT(a, b, ...)`. Oracle's takes exactly two.
|
|
158
|
+
# @return [AST::Function]
|
|
159
|
+
# @!method cos(x)
|
|
160
|
+
# `COS(x)`.
|
|
161
|
+
# @return [AST::Function]
|
|
162
|
+
# @!method exp(x)
|
|
163
|
+
# `EXP(x)`.
|
|
164
|
+
# @return [AST::Function]
|
|
165
|
+
# @!method floor(x)
|
|
166
|
+
# `FLOOR(x)`.
|
|
167
|
+
# @return [AST::Function]
|
|
168
|
+
# @!method length(string)
|
|
169
|
+
# `LENGTH(string)`: `LEN` on SQL Server. What it counts is the
|
|
170
|
+
# family's own -- bytes on MySQL, characters elsewhere, and `LEN`
|
|
171
|
+
# leaves trailing spaces out; {#char_length} is the portable count.
|
|
172
|
+
# @return [AST::Function]
|
|
173
|
+
# @!method ln(x)
|
|
174
|
+
# `LN(x)`: `LOG` on SQL Server.
|
|
175
|
+
# @return [AST::Function]
|
|
176
|
+
# @!method log(base, x)
|
|
177
|
+
# `LOG(base, x)`. SQL Server takes the arguments the other way round, and is refused.
|
|
178
|
+
# @return [AST::Function]
|
|
179
|
+
# @!method lower(string)
|
|
180
|
+
# `LOWER(string)`.
|
|
181
|
+
# @return [AST::Function]
|
|
182
|
+
# @!method ltrim(string)
|
|
183
|
+
# `LTRIM(string)`.
|
|
184
|
+
# @return [AST::Function]
|
|
185
|
+
# @!method mod(x, y)
|
|
186
|
+
# `MOD(x, y)`. SQL Server has only the % operator.
|
|
187
|
+
# @return [AST::Function]
|
|
188
|
+
# @!method nullif(x, y)
|
|
189
|
+
# `NULLIF(x, y)`: NULL where the two are equal, x otherwise.
|
|
190
|
+
# @return [AST::Function]
|
|
191
|
+
# @!method power(x, y)
|
|
192
|
+
# `POWER(x, y)`.
|
|
193
|
+
# @return [AST::Function]
|
|
194
|
+
# @!method replace(string, from, to)
|
|
195
|
+
# `REPLACE(string, from, to)`.
|
|
196
|
+
# @return [AST::Function]
|
|
197
|
+
# @!method round(x, places = 0)
|
|
198
|
+
# `ROUND(x, places)`.
|
|
199
|
+
# @return [AST::Function]
|
|
200
|
+
# @!method rtrim(string)
|
|
201
|
+
# `RTRIM(string)`.
|
|
202
|
+
# @return [AST::Function]
|
|
203
|
+
# @!method sign(x)
|
|
204
|
+
# `SIGN(x)`.
|
|
205
|
+
# @return [AST::Function]
|
|
206
|
+
# @!method sin(x)
|
|
207
|
+
# `SIN(x)`.
|
|
208
|
+
# @return [AST::Function]
|
|
209
|
+
# @!method sqrt(x)
|
|
210
|
+
# `SQRT(x)`.
|
|
211
|
+
# @return [AST::Function]
|
|
212
|
+
# @!method substr(string, from, length = nil)
|
|
213
|
+
# `SUBSTR(string, from, length)`: `SUBSTRING` on SQL Server, which insists on the length.
|
|
214
|
+
# @return [AST::Function]
|
|
215
|
+
# @!method tan(x)
|
|
216
|
+
# `TAN(x)`.
|
|
217
|
+
# @return [AST::Function]
|
|
218
|
+
# @!method trim(string)
|
|
219
|
+
# `TRIM(string)`.
|
|
220
|
+
# @return [AST::Function]
|
|
221
|
+
# @!method upper(string)
|
|
222
|
+
# `UPPER(string)`.
|
|
223
|
+
# @return [AST::Function]
|
|
224
|
+
# @!method degrees(x)
|
|
225
|
+
# `DEGREES(x)`. Oracle has none.
|
|
226
|
+
# @return [AST::Function]
|
|
227
|
+
# @!method radians(x)
|
|
228
|
+
# `RADIANS(x)`. Oracle has none.
|
|
229
|
+
# @return [AST::Function]
|
|
230
|
+
# @!method pi
|
|
231
|
+
# `PI()`. Oracle has none.
|
|
232
|
+
# @return [AST::Function]
|
|
233
|
+
# @!method char_length(string)
|
|
234
|
+
# `CHAR_LENGTH(string)`: `LENGTH` on SQLite and Oracle, `LEN` on SQL Server.
|
|
235
|
+
# @return [AST::Function]
|
|
236
|
+
# @!method greatest(*values)
|
|
237
|
+
# `GREATEST(a, b, ...)`: `MAX` on SQLite.
|
|
238
|
+
# @return [AST::Function]
|
|
239
|
+
# @!method least(*values)
|
|
240
|
+
# `LEAST(a, b, ...)`: `MIN` on SQLite.
|
|
241
|
+
# @return [AST::Function]
|
|
242
|
+
# @!method log2(x)
|
|
243
|
+
# `LOG2(x)`. PostgreSQL and Oracle have none -- `log(2, x)` is their spelling -- and SQL Server has neither.
|
|
244
|
+
# @return [AST::Function]
|
|
245
|
+
# @!method log10(x)
|
|
246
|
+
# `LOG10(x)`. Oracle has none.
|
|
247
|
+
# @return [AST::Function]
|
|
248
|
+
# @!method trunc(x, places = 0)
|
|
249
|
+
# `TRUNC(x, places)`: `TRUNCATE` on MySQL, which insists on the places. SQL Server has none.
|
|
250
|
+
# @return [AST::Function]
|
|
251
|
+
# @!method now
|
|
252
|
+
# `NOW()`. SQLite, Oracle and SQL Server have none; {#current_timestamp} reaches all three.
|
|
253
|
+
# @return [AST::Function]
|
|
254
|
+
# @!method bit_and(column)
|
|
255
|
+
# `BIT_AND(column)`, an aggregate. PostgreSQL and MySQL have it.
|
|
256
|
+
# @return [AST::Function]
|
|
257
|
+
# @!method bit_or(column)
|
|
258
|
+
# `BIT_OR(column)`, an aggregate. PostgreSQL and MySQL have it.
|
|
259
|
+
# @return [AST::Function]
|
|
260
|
+
# @!method bit_xor(column)
|
|
261
|
+
# `BIT_XOR(column)`, an aggregate. PostgreSQL and MySQL have it.
|
|
262
|
+
# @return [AST::Function]
|
|
263
|
+
# @!method date_trunc(field, timestamp)
|
|
264
|
+
# `date_trunc('day', timestamp)`. PostgreSQL has it; the others do not.
|
|
265
|
+
# @return [AST::Function]
|
|
266
|
+
# @!method rand
|
|
267
|
+
# `RAND()`, a random number per row: `RANDOM()` on PostgreSQL and SQLite. Oracle and SQL Server have none.
|
|
268
|
+
# @return [AST::Function]
|
|
269
|
+
# @!method format(template, *values)
|
|
270
|
+
# printf-style `FORMAT(template, ...)`. PostgreSQL and SQLite have it; MySQL's FORMAT is a different function, reached through {#fn}.
|
|
271
|
+
# @return [AST::Function]
|
|
272
|
+
#
|
|
273
|
+
# Scalar functions, defined as real methods so that a typo is a
|
|
274
|
+
# NoMethodError and a name Kernel also answers to (format, hash, test)
|
|
275
|
+
# cannot quietly mean something else. Where one is spelled other than as
|
|
276
|
+
# its plain upper-cased name, and where a family has no equivalent, is
|
|
277
|
+
# the dialect's to say; here is only the list of them.
|
|
278
|
+
# @private
|
|
279
|
+
SCALAR_FUNCTIONS = %i[
|
|
280
|
+
abs acos asin atan atan2 ceil coalesce concat cos exp floor length ln
|
|
281
|
+
log lower ltrim mod nullif power replace round rtrim sign sin sqrt
|
|
282
|
+
substr tan trim upper degrees radians pi char_length greatest least
|
|
283
|
+
log2 log10 trunc now bit_and bit_or bit_xor date_trunc rand format
|
|
284
|
+
].freeze
|
|
285
|
+
|
|
286
|
+
SCALAR_FUNCTIONS.each do |name|
|
|
287
|
+
define_method(name) do |*args|
|
|
288
|
+
AST::Function.new(dialect.function_name(name, @model), args)
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# @!endgroup
|
|
293
|
+
# @!group Datetime value functions
|
|
294
|
+
|
|
295
|
+
# @!method current_timestamp(precision = nil)
|
|
296
|
+
# `CURRENT_TIMESTAMP`, the server's clock in the session's zone; the
|
|
297
|
+
# portable spelling of what {#now} means. A precision --
|
|
298
|
+
# `current_timestamp(3)` -- goes into parentheses, which SQLite and
|
|
299
|
+
# SQL Server refuse.
|
|
300
|
+
# @return [AST::DatetimeValueFunction]
|
|
301
|
+
# @example
|
|
302
|
+
# Post.where { :published_at <= current_timestamp }
|
|
303
|
+
# Post.where { :created_at > current_timestamp - 7.days }
|
|
304
|
+
# @!method current_time(precision = nil)
|
|
305
|
+
# `CURRENT_TIME`. SQL Server has none.
|
|
306
|
+
# @return [AST::DatetimeValueFunction]
|
|
307
|
+
# @!method localtime(precision = nil)
|
|
308
|
+
# `LOCALTIME`. SQLite and SQL Server have none.
|
|
309
|
+
# @return [AST::DatetimeValueFunction]
|
|
310
|
+
# @!method localtimestamp(precision = nil)
|
|
311
|
+
# `LOCALTIMESTAMP`. SQLite and SQL Server have none.
|
|
312
|
+
# @return [AST::DatetimeValueFunction]
|
|
313
|
+
#
|
|
314
|
+
# The datetime value functions, as the SQL grammar calls them. These
|
|
315
|
+
# the grammar has bare -- PostgreSQL and SQLite reject them written with
|
|
316
|
+
# parentheses -- and the one thing that does go into parentheses is an
|
|
317
|
+
# optional precision, current_timestamp(3), which current_date never
|
|
318
|
+
# takes and SQLite never accepts. The table reads like
|
|
319
|
+
# SCALAR_FUNCTIONS; current_timestamp is the portable spelling of what
|
|
320
|
+
# now means, reaching SQLite where now does not.
|
|
321
|
+
# @private
|
|
322
|
+
DATETIME_VALUE_FUNCTIONS = %i[
|
|
323
|
+
current_date current_time current_timestamp localtime localtimestamp
|
|
324
|
+
].freeze
|
|
325
|
+
|
|
326
|
+
# `CURRENT_DATE`, today in the session's zone -- UTC where Active
|
|
327
|
+
# Record has set it so. Takes no precision. SQL Server has none.
|
|
328
|
+
# @return [AST::DatetimeValueFunction]
|
|
329
|
+
# @example
|
|
330
|
+
# Task.where { :due_on < current_date }
|
|
331
|
+
def current_date
|
|
332
|
+
AST::DatetimeValueFunction.new(dialect.function_name(:current_date, @model))
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
(DATETIME_VALUE_FUNCTIONS - [:current_date]).each do |name|
|
|
336
|
+
define_method(name) do |precision = nil|
|
|
337
|
+
# Built first so that a precision of the wrong type is an
|
|
338
|
+
# ArgumentError on every adapter, before SQLite gets to say it takes
|
|
339
|
+
# none at all.
|
|
340
|
+
node = AST::DatetimeValueFunction.new(
|
|
341
|
+
dialect.function_name(name, @model), precision)
|
|
342
|
+
if precision && !dialect.datetime_precision_supported?
|
|
343
|
+
raise NotImplementedError,
|
|
344
|
+
"#{name} takes no precision on #{@model.connection_db_config.adapter}"
|
|
345
|
+
end
|
|
346
|
+
node
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
# `EXTRACT(field FROM expr)`: a year, a month, a day of a date. The
|
|
351
|
+
# field is a keyword and has to be a plain name. SQLite and SQL Server
|
|
352
|
+
# have none.
|
|
353
|
+
# @param field [Symbol, String] `:year`, `:month`, `:day`, `:hour`, ...
|
|
354
|
+
# @return [AST::Extract]
|
|
355
|
+
# @example
|
|
356
|
+
# Post.where { extract(:year, :created_at) == 2026 }
|
|
357
|
+
#
|
|
358
|
+
# The field is a keyword, not a value, so it has to be a plain name;
|
|
359
|
+
# the node checks it. SQLite spells all of this as strftime formats,
|
|
360
|
+
# which no renaming carries, so it raises there -- after the node is
|
|
361
|
+
# built, so that a bad field is an ArgumentError on every adapter.
|
|
362
|
+
def extract(field, expr)
|
|
363
|
+
node = AST::Extract.new(field, expr)
|
|
364
|
+
unless dialect.extract_supported?
|
|
365
|
+
raise NotImplementedError,
|
|
366
|
+
"extract has no equivalent on #{@model.connection_db_config.adapter}"
|
|
367
|
+
end
|
|
368
|
+
node
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
# @!endgroup
|
|
372
|
+
# @!group Grouping
|
|
373
|
+
|
|
374
|
+
# `GROUP BY GROUPING SETS ((a), (b), ())`: several groupings in one
|
|
375
|
+
# query, an empty set for the grand total. PostgreSQL has it; the
|
|
376
|
+
# others do not.
|
|
377
|
+
# @param sets [Array<Array<Symbol, AST::Node>>]
|
|
378
|
+
# @return [AST::GroupingSets]
|
|
379
|
+
# @example
|
|
380
|
+
# Sale.group { grouping_sets([:region], [:product], []) }
|
|
381
|
+
#
|
|
382
|
+
# Arel has the nodes and writes them for PostgreSQL alone, so what it
|
|
383
|
+
# would raise elsewhere says nothing; this says it here, as extract
|
|
384
|
+
# does, while the block is being read.
|
|
385
|
+
def grouping_sets(*sets)
|
|
386
|
+
grouping(:grouping_sets, sets)
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
# `GROUP BY ROLLUP (a, b)`: subtotals up the list and a grand total.
|
|
390
|
+
# PostgreSQL has it, and the MySQL family as `WITH ROLLUP` trailing
|
|
391
|
+
# the group list, which the node spells there.
|
|
392
|
+
# @return [AST::GroupingSets]
|
|
393
|
+
# @example
|
|
394
|
+
# Sale.group { rollup(:region, :product) }
|
|
395
|
+
def rollup(*columns)
|
|
396
|
+
grouping(:rollup, columns)
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
# `GROUP BY CUBE (a, b)`: every subtotal there is. PostgreSQL has it;
|
|
400
|
+
# the others do not.
|
|
401
|
+
# @return [AST::GroupingSets]
|
|
402
|
+
# @example
|
|
403
|
+
# Sale.group { cube(:region, :product) }
|
|
404
|
+
def cube(*columns)
|
|
405
|
+
grouping(:cube, columns)
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
# @!endgroup
|
|
409
|
+
# @!group Conversions
|
|
410
|
+
|
|
411
|
+
# `CAST(expr AS type)`. The type is the adapter's own name for it --
|
|
412
|
+
# `decimal(10,2)`, `double precision` -- and has to look like one;
|
|
413
|
+
# whether it exists is the database's to say.
|
|
414
|
+
# @param type [Symbol, String]
|
|
415
|
+
# @return [AST::Cast]
|
|
416
|
+
# @example
|
|
417
|
+
# Post.select { cast(:price, "decimal(10,2)").as(:price) }
|
|
418
|
+
def cast(expr, type)
|
|
419
|
+
AST::Cast.new(expr, type)
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
# @!endgroup
|
|
423
|
+
# @!group Window functions
|
|
424
|
+
|
|
425
|
+
# @!method row_number
|
|
426
|
+
# `ROW_NUMBER()`. Means nothing without {AST::Windowing#over}, and
|
|
427
|
+
# says so.
|
|
428
|
+
# @return [AST::WindowFunction]
|
|
429
|
+
# @example
|
|
430
|
+
# Author.select { row_number.over.partition(:country).order(:age.desc).as(:rank) }
|
|
431
|
+
# @!method rank
|
|
432
|
+
# `RANK()`; needs `over`.
|
|
433
|
+
# @return [AST::WindowFunction]
|
|
434
|
+
# @!method dense_rank
|
|
435
|
+
# `DENSE_RANK()`; needs `over`.
|
|
436
|
+
# @return [AST::WindowFunction]
|
|
437
|
+
# @!method percent_rank
|
|
438
|
+
# `PERCENT_RANK()`; needs `over`.
|
|
439
|
+
# @return [AST::WindowFunction]
|
|
440
|
+
# @!method cume_dist
|
|
441
|
+
# `CUME_DIST()`; needs `over`.
|
|
442
|
+
# @return [AST::WindowFunction]
|
|
443
|
+
# @!method ntile(buckets)
|
|
444
|
+
# `NTILE(buckets)`; needs `over`.
|
|
445
|
+
# @return [AST::WindowFunction]
|
|
446
|
+
# @!method first_value(expr)
|
|
447
|
+
# `FIRST_VALUE(expr)`; needs `over`.
|
|
448
|
+
# @return [AST::WindowFunction]
|
|
449
|
+
# @!method last_value(expr)
|
|
450
|
+
# `LAST_VALUE(expr)`; needs `over`.
|
|
451
|
+
# @return [AST::WindowFunction]
|
|
452
|
+
#
|
|
453
|
+
# The functions that only mean anything with a window. Every adapter
|
|
454
|
+
# that has window functions at all spells these the same -- PostgreSQL,
|
|
455
|
+
# MySQL 8, SQLite 3.25 -- so unlike the scalar functions there is nothing
|
|
456
|
+
# here to translate. Each says so if `over` never arrives.
|
|
457
|
+
%i[row_number rank dense_rank percent_rank cume_dist].each do |name|
|
|
458
|
+
define_method(name) { AST::WindowFunction.new(name.to_s.upcase, []) }
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
%i[ntile first_value last_value].each do |name|
|
|
462
|
+
define_method(name) { |arg| AST::WindowFunction.new(name.to_s.upcase, [arg]) }
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
# `NTH_VALUE(expr, nth)`; needs `over`.
|
|
466
|
+
# @return [AST::WindowFunction]
|
|
467
|
+
def nth_value(expr, nth)
|
|
468
|
+
AST::WindowFunction.new("NTH_VALUE", [expr, nth])
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
# `LAG(expr, offset, default)`: the value `offset` rows before this
|
|
472
|
+
# one; needs `over`.
|
|
473
|
+
# @return [AST::WindowFunction]
|
|
474
|
+
# @example
|
|
475
|
+
# Post.select { (:likes - lag(:likes).over.order(:created_at)).as(:gain) }
|
|
476
|
+
#
|
|
477
|
+
# The offset is written out rather than left to default, so that a
|
|
478
|
+
# default value cannot end up where the offset belongs.
|
|
479
|
+
def lag(expr, offset = 1, default = nil)
|
|
480
|
+
AST::WindowFunction.new("LAG", default.nil? ? [expr, offset] : [expr, offset, default])
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
# `LEAD(expr, offset, default)`: the value `offset` rows after this
|
|
484
|
+
# one; needs `over`.
|
|
485
|
+
# @return [AST::WindowFunction]
|
|
486
|
+
def lead(expr, offset = 1, default = nil)
|
|
487
|
+
AST::WindowFunction.new("LEAD", default.nil? ? [expr, offset] : [expr, offset, default])
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
# @!endgroup
|
|
491
|
+
# @!group Escape hatches
|
|
492
|
+
|
|
493
|
+
# Any function by name: `fn(:date_part, "year", :created_at)`. The name
|
|
494
|
+
# is written as given -- so a case-sensitive one can be spelled exactly
|
|
495
|
+
# -- and has to be a plain name, optionally qualified by a schema;
|
|
496
|
+
# the arguments are quoted as values unless they are columns or
|
|
497
|
+
# expressions.
|
|
498
|
+
# @param name [Symbol, String]
|
|
499
|
+
# @return [AST::Function]
|
|
500
|
+
# @example
|
|
501
|
+
# Post.select { fn(:date_part, "year", :created_at).as(:year) }
|
|
502
|
+
#
|
|
503
|
+
# The name is emitted as written, so a case-sensitive one can be
|
|
504
|
+
# spelled exactly, and for that reason it has to be a plain name,
|
|
505
|
+
# optionally qualified by a schema; anything else is refused rather
|
|
506
|
+
# than written into the SQL.
|
|
507
|
+
def fn(name, *args)
|
|
508
|
+
AST::Function.new(
|
|
509
|
+
AST.check_name(name, AST::FUNCTION_NAME, "function name").to_s, args)
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
# Any binary operator by its spelling: `op("&&", :tags, "{ruby,sql}")`.
|
|
513
|
+
# The operator has to be made of operator characters; the operands are
|
|
514
|
+
# quoted as values unless they are columns or expressions, and
|
|
515
|
+
# parenthesized, since the operator's precedence is not known.
|
|
516
|
+
# @param operator [String]
|
|
517
|
+
# @return [AST::Operation]
|
|
518
|
+
# @example
|
|
519
|
+
# Post.where { op("&&", :tags, "{ruby,sql}") } # PostgreSQL arrays
|
|
520
|
+
def op(operator, left, right)
|
|
521
|
+
AST::Operation.new(operator, left, right)
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
# @!endgroup
|
|
525
|
+
# @!group Bits
|
|
526
|
+
|
|
527
|
+
# `BIT_COUNT(expr)`, the bits set in a number. MySQL and PostgreSQL
|
|
528
|
+
# have it; SQLite, Oracle and SQL Server do not.
|
|
529
|
+
# @return [AST::Function]
|
|
530
|
+
# @example
|
|
531
|
+
# Post.select { bit_count(:flags).as(:set) }
|
|
532
|
+
#
|
|
533
|
+
# MySQL counts the bits of a number; PostgreSQL counts those of a bit
|
|
534
|
+
# string, so the argument is cast, and to bit(64) because that is what
|
|
535
|
+
# makes a negative come back as MySQL has it -- 64 bits of two's
|
|
536
|
+
# complement rather than as many as the column happens to be wide.
|
|
537
|
+
def bit_count(expr)
|
|
538
|
+
dialect.bit_count(expr, @model)
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
# @!endgroup
|
|
542
|
+
# @!group Subqueries
|
|
543
|
+
|
|
544
|
+
# `EXISTS (subquery)`. The subquery is a relation, which may refer to
|
|
545
|
+
# the outer row through a qualified column.
|
|
546
|
+
# @param relation [ActiveRecord::Relation]
|
|
547
|
+
# @return [AST::Exists]
|
|
548
|
+
# @example
|
|
549
|
+
# Author.where { exists?(Post.where { :posts[:author_id] == :authors[:id] }) }
|
|
550
|
+
def exists?(relation)
|
|
551
|
+
AST::Exists.new(relation)
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
# `ANY (subquery)`, on the right of a comparison: true of the rows the
|
|
555
|
+
# comparison holds for any row of the subquery. SQLite has none.
|
|
556
|
+
# @param relation [ActiveRecord::Relation]
|
|
557
|
+
# @return [AST::Quantified]
|
|
558
|
+
# @example
|
|
559
|
+
# Post.where { :likes > any(Post.published.select(:likes)) }
|
|
560
|
+
#
|
|
561
|
+
# ANY and ALL quantify a comparison over a subquery, which is what a
|
|
562
|
+
# scalar subquery cannot do: it has to return the one row. `== any`
|
|
563
|
+
# is IN and `!= all` is NOT IN, so what these add is the four
|
|
564
|
+
# comparisons IN has no spelling for.
|
|
565
|
+
def any(relation)
|
|
566
|
+
quantified("ANY", relation)
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
# `ALL (subquery)`, on the right of a comparison: true of the rows the
|
|
570
|
+
# comparison holds for every row of the subquery. SQLite has none.
|
|
571
|
+
# @param relation [ActiveRecord::Relation]
|
|
572
|
+
# @return [AST::Quantified]
|
|
573
|
+
# @example
|
|
574
|
+
# Post.where { :likes >= all(Post.select(:likes)) }
|
|
575
|
+
def all(relation)
|
|
576
|
+
quantified("ALL", relation)
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
# @!endgroup
|
|
580
|
+
# @!group Escape hatches
|
|
581
|
+
|
|
582
|
+
# SQL as written, the one way a string means SQL inside a block. `?`
|
|
583
|
+
# and `:name` placeholders take quoted values, as `where` takes them.
|
|
584
|
+
# @param statement [String]
|
|
585
|
+
# @return [AST::Sql]
|
|
586
|
+
# @example
|
|
587
|
+
# Post.where { sql("length(title) > ?", 10) }
|
|
588
|
+
# Post.select { sql("count(*) FILTER (WHERE score > 0) AS positive") }
|
|
589
|
+
def sql(statement, *binds)
|
|
590
|
+
AST::Sql.new(statement, binds)
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
# A literal where an expression is expected, quoted like any other
|
|
594
|
+
# value. A number or a string takes `as` for itself -- `0.as(:depth)`
|
|
595
|
+
# -- so this is the spelling for the rest: `true`, `nil`, a date.
|
|
596
|
+
# @return [AST::Value]
|
|
597
|
+
# @example
|
|
598
|
+
# Node.select { [:id, value(0).as(:depth)] }
|
|
599
|
+
# Post.select { [:title, value(nil).as(:score)] }
|
|
600
|
+
def value(literal)
|
|
601
|
+
AST::Value.new(literal)
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
# The row an upsert could not insert, in the block `upsert_all` takes:
|
|
605
|
+
# `"excluded"."column"` on PostgreSQL and SQLite, `VALUES(column)` on
|
|
606
|
+
# MySQL.
|
|
607
|
+
# @param column [Symbol]
|
|
608
|
+
# @return [AST::Node]
|
|
609
|
+
# @example
|
|
610
|
+
# Tally.upsert_all(rows, unique_by: :page) { { hits: :hits + excluded(:hits) } }
|
|
611
|
+
def excluded(column)
|
|
612
|
+
dialect.excluded(column, @model)
|
|
613
|
+
end
|
|
614
|
+
|
|
615
|
+
# @!endgroup
|
|
616
|
+
# @!group CASE
|
|
617
|
+
|
|
618
|
+
# `CASE`, in either shape: with an operand each `when` is compared
|
|
619
|
+
# against, or without one, each `when` carrying its own condition.
|
|
620
|
+
# `case` is a keyword, so this one is reached as `self.case`; the
|
|
621
|
+
# shorthands `:age.when(...)` and {#case_when} need no receiver.
|
|
622
|
+
# @return [AST::Case]
|
|
623
|
+
# @example
|
|
624
|
+
# self.case(:age).when(10).then(1).else(0)
|
|
625
|
+
# self.case.when { :age >= 60 }.then { :age - 60 }
|
|
626
|
+
def case(operand = nil)
|
|
627
|
+
AST::Case.new(operand)
|
|
628
|
+
end
|
|
629
|
+
|
|
630
|
+
# The searched `CASE`, started at its first `when`: each `when` is a
|
|
631
|
+
# condition, as a value or a block, and `then` and `else` give the
|
|
632
|
+
# values.
|
|
633
|
+
# @return [AST::Case::When]
|
|
634
|
+
# @example
|
|
635
|
+
# Author.select { case_when { :age >= 60 }.then("senior").else("adult").as(:band) }
|
|
636
|
+
# Author.select { sum(case_when { :age >= 60 }.then(1).else(0)).as(:seniors) }
|
|
637
|
+
def case_when(value = nil, &block)
|
|
638
|
+
AST::Case.new.when(value, &block)
|
|
639
|
+
end
|
|
640
|
+
|
|
641
|
+
private
|
|
642
|
+
# @!endgroup
|
|
643
|
+
#
|
|
644
|
+
# The group closes here rather than above `private`: a comment on
|
|
645
|
+
# that line belongs to the `private` call, which reads no
|
|
646
|
+
# directives, and the group would run on into the next module.
|
|
647
|
+
#
|
|
648
|
+
# SQLite is the one adapter with no quantifier at all, and what it says
|
|
649
|
+
# when it meets one is a syntax error at the SELECT.
|
|
650
|
+
def quantified(kind, relation)
|
|
651
|
+
unless dialect.quantifiers_supported?
|
|
652
|
+
raise NotImplementedError,
|
|
653
|
+
"#{kind} has no equivalent on #{@model.connection_db_config.adapter}"
|
|
654
|
+
end
|
|
655
|
+
AST::Quantified.new(kind, relation)
|
|
656
|
+
end
|
|
657
|
+
|
|
658
|
+
def grouping(kind, sets)
|
|
659
|
+
node = AST::GroupingSets.new(kind, sets)
|
|
660
|
+
return node if dialect.grouping_supported?(kind)
|
|
661
|
+
|
|
662
|
+
raise NotImplementedError,
|
|
663
|
+
"#{kind} has no equivalent on #{@model.connection_db_config.adapter}"
|
|
664
|
+
end
|
|
665
|
+
|
|
666
|
+
def dialect
|
|
667
|
+
@dialect ||= Dialect.for(@model)
|
|
668
|
+
end
|
|
669
|
+
end
|
|
670
|
+
end
|
|
671
|
+
end
|