activerecord-refined 0.8.1 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.yardopts +17 -0
- data/README.md +111 -815
- data/activerecord-refined.gemspec +36 -17
- data/docs/conditions.md +206 -0
- data/docs/ctes.md +65 -0
- data/docs/expressions.md +125 -0
- data/docs/functions.md +219 -0
- data/docs/grouping.md +55 -0
- data/docs/joins.md +73 -0
- data/docs/json.md +230 -0
- data/docs/ordering.md +55 -0
- data/docs/time_zones.md +30 -0
- data/docs/windows.md +41 -0
- data/docs/writing.md +33 -0
- data/examples/aggregations.rb +31 -11
- data/examples/complex_joins.rb +12 -10
- data/examples/ctes.rb +22 -20
- data/examples/expressions.rb +110 -45
- data/examples/json.rb +77 -38
- data/examples/postgresql.rb +64 -53
- data/examples/predicates.rb +35 -33
- data/examples/subqueries.rb +20 -18
- data/examples/windows.rb +23 -21
- data/examples/writes.rb +26 -24
- data/lib/active_record/refined/ast.rb +1117 -373
- data/lib/active_record/refined/dialect/mariadb.rb +25 -0
- data/lib/active_record/refined/dialect/mysql.rb +18 -0
- data/lib/active_record/refined/dialect/mysql_compat.rb +67 -0
- data/lib/active_record/refined/dialect/oracle.rb +110 -0
- data/lib/active_record/refined/dialect/postgresql.rb +120 -0
- data/lib/active_record/refined/dialect/sql_server.rb +115 -0
- data/lib/active_record/refined/dialect/sqlite.rb +57 -0
- data/lib/active_record/refined/dialect.rb +340 -0
- data/lib/active_record/refined.rb +877 -307
- data/lib/activerecord-refined/version.rb +3 -1
- data/lib/activerecord-refined.rb +9 -5
- metadata +186 -15
- data/.github/workflows/push_gem.yml +0 -45
- data/.github/workflows/sandbox.yml +0 -295
- data/.github/workflows/test.yml +0 -90
- data/.gitignore +0 -19
- data/Gemfile +0 -12
- data/Rakefile +0 -25
- data/benchmark/query_building.rb +0 -129
- data/test/test_block_syntax.rb +0 -2493
- data/test/test_helper.rb +0 -221
|
@@ -1,45 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
# JSON.generate, for the document a containment test is given.
|
|
2
|
-
require
|
|
4
|
+
require "json"
|
|
3
5
|
|
|
4
6
|
module ActiveRecord
|
|
5
7
|
module Refined
|
|
6
8
|
module AST
|
|
9
|
+
# @private
|
|
7
10
|
NAME = /[[:alpha:]_][[:alnum:]_$]*/
|
|
11
|
+
# @private
|
|
8
12
|
ALIAS_NAME = /\A#{NAME}\z/
|
|
13
|
+
# @private
|
|
9
14
|
FUNCTION_NAME = /\A#{NAME}(\.#{NAME})?\z/
|
|
15
|
+
# A collation name where the family writes it bare, as all but PostgreSQL
|
|
16
|
+
# do. A plain identifier: a hyphen unquoted would read as the collation
|
|
17
|
+
# minus a number, `x COLLATE nocase-1` as `(x COLLATE nocase) - 1`, valid
|
|
18
|
+
# and wrong. PostgreSQL quotes the name and widens this; see its dialect.
|
|
19
|
+
# @private
|
|
20
|
+
COLLATION_NAME = /\A#{NAME}\z/
|
|
10
21
|
# A SQL type as cast writes it: words, at most parenthesized with
|
|
11
22
|
# lengths -- double precision, decimal(10,2).
|
|
23
|
+
# @private
|
|
12
24
|
TYPE_NAME = /\A[[:alpha:]_][[:alnum:]_ ]*(\(\d+(, ?\d+)?\))?\z/
|
|
13
|
-
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
|
|
18
|
-
#
|
|
19
|
-
# pglite is PostgreSQL itself compiled to WebAssembly, reached through
|
|
20
|
-
# wasmify-rails' adapter; the server it answers for is the same one.
|
|
21
|
-
ADAPTER_FAMILIES = {
|
|
22
|
-
'sqlite3' => :sqlite,
|
|
23
|
-
'postgresql' => :postgresql,
|
|
24
|
-
'postgis' => :postgresql,
|
|
25
|
-
'pglite' => :postgresql,
|
|
26
|
-
'mysql2' => :mysql,
|
|
27
|
-
'trilogy' => :mysql,
|
|
28
|
-
}.freeze
|
|
29
|
-
|
|
30
|
-
def self.adapter_family(model)
|
|
31
|
-
ADAPTER_FAMILIES[model.connection_db_config.adapter] || :unknown
|
|
32
|
-
end
|
|
25
|
+
# The characters PostgreSQL allows an operator to be made of, the
|
|
26
|
+
# widest operator alphabet of the three; op admits nothing else, so a
|
|
27
|
+
# letter, a space or a quote never reaches the SQL as an operator.
|
|
28
|
+
# @private
|
|
29
|
+
OPERATOR = %r{\A[+\-*/<>=~!@\#%^&|`?]+\z}
|
|
33
30
|
|
|
34
31
|
def self.check_name(name, pattern, what)
|
|
35
32
|
return name if pattern.match?(name.to_s)
|
|
36
33
|
raise ArgumentError, "#{name.inspect} is not a plain #{what}"
|
|
37
34
|
end
|
|
38
35
|
|
|
36
|
+
# A Ruby document or boolean written where JSON is wanted. Taken as it
|
|
37
|
+
# is, a document would be the string that spells it and a boolean
|
|
38
|
+
# SQLite's own 1. SQLite's json() marks the literal for the JSON
|
|
39
|
+
# functions; the MySQL family, which has no json(), reads it with
|
|
40
|
+
# JSON_EXTRACT.
|
|
41
|
+
def self.json_argument(value, model)
|
|
42
|
+
Dialect.for(model).json_argument(value, model)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# The conditions a column or an expression can be put in. Every one
|
|
46
|
+
# gives back a condition that combines with `&`, `|` and `!`, and the
|
|
47
|
+
# comparisons quote a Ruby value on the right the way Active Record
|
|
48
|
+
# does, or take a column, an expression or a subquery there.
|
|
49
|
+
#
|
|
50
|
+
# @example
|
|
51
|
+
# Author.where { :age.between?(20, 40) & :name.like?("A%") }
|
|
52
|
+
# Author.where { :id.in?(Post.select(:author_id)) }
|
|
53
|
+
#
|
|
39
54
|
# Predicate builders shared by symbols, qualified columns and
|
|
40
55
|
# expressions. Imported into the Symbol refinement with
|
|
41
56
|
# Refinement#import_methods, so every method must be defined with def.
|
|
42
57
|
module Predications
|
|
58
|
+
# `=`. A value, a column, an expression or a scalar subquery on the right; `nil` is refused, since `= NULL` is never true -- {#null?} is the spelling.
|
|
59
|
+
# @return [AST::Predicate]
|
|
60
|
+
# @example
|
|
61
|
+
# Author.where { :name == "alice" }
|
|
62
|
+
# Author.where { :age == Author.select { max(:age) } }
|
|
63
|
+
#
|
|
43
64
|
# == and != mean SQL = and <>, and = NULL is never true there, so nil
|
|
44
65
|
# is rejected rather than silently rewritten to IS NULL. null? builds
|
|
45
66
|
# its node directly and stays clear of this check.
|
|
@@ -50,6 +71,8 @@ module ActiveRecord
|
|
|
50
71
|
Comparison.new(self, :==, other)
|
|
51
72
|
end
|
|
52
73
|
|
|
74
|
+
# `!=`; `nil` is refused, as with `==`.
|
|
75
|
+
# @return [AST::Predicate]
|
|
53
76
|
def !=(other)
|
|
54
77
|
if other.nil?
|
|
55
78
|
raise ArgumentError, "!= does not take nil; use !null? instead"
|
|
@@ -57,30 +80,50 @@ module ActiveRecord
|
|
|
57
80
|
Comparison.new(self, :!=, other)
|
|
58
81
|
end
|
|
59
82
|
|
|
83
|
+
# `>`.
|
|
84
|
+
# @return [AST::Predicate]
|
|
60
85
|
def >(other)
|
|
61
86
|
Comparison.new(self, :>, other)
|
|
62
87
|
end
|
|
63
88
|
|
|
89
|
+
# `>=`.
|
|
90
|
+
# @return [AST::Predicate]
|
|
64
91
|
def >=(other)
|
|
65
92
|
Comparison.new(self, :>=, other)
|
|
66
93
|
end
|
|
67
94
|
|
|
95
|
+
# `<`.
|
|
96
|
+
# @return [AST::Predicate]
|
|
68
97
|
def <(other)
|
|
69
98
|
Comparison.new(self, :<, other)
|
|
70
99
|
end
|
|
71
100
|
|
|
101
|
+
# `<=`.
|
|
102
|
+
# @return [AST::Predicate]
|
|
72
103
|
def <=(other)
|
|
73
104
|
Comparison.new(self, :<=, other)
|
|
74
105
|
end
|
|
75
106
|
|
|
107
|
+
# A regular expression match: `~` on PostgreSQL, `REGEXP` on MySQL, and what the adapter has elsewhere. A Ruby Regexp's source is the pattern.
|
|
108
|
+
# @param pattern [Regexp, String]
|
|
109
|
+
# @return [AST::Predicate]
|
|
110
|
+
# @example
|
|
111
|
+
# Author.where { :name =~ /^A/ }
|
|
76
112
|
def =~(pattern)
|
|
77
113
|
Match.new(self, pattern)
|
|
78
114
|
end
|
|
79
115
|
|
|
116
|
+
# The negated regular expression match.
|
|
117
|
+
# @return [AST::Predicate]
|
|
80
118
|
def !~(pattern)
|
|
81
119
|
Match.new(self, pattern, negated: true)
|
|
82
120
|
end
|
|
83
121
|
|
|
122
|
+
# `IS NULL`.
|
|
123
|
+
# @return [AST::Predicate]
|
|
124
|
+
# @example
|
|
125
|
+
# Author.where { :country.null? }
|
|
126
|
+
#
|
|
84
127
|
# `!` negates any predicate, so these are here for the four that SQL
|
|
85
128
|
# spells for itself: IS NOT NULL rather than NOT (... IS NULL), and
|
|
86
129
|
# likewise NOT IN and NOT LIKE. They mean the same thing either way,
|
|
@@ -89,10 +132,17 @@ module ActiveRecord
|
|
|
89
132
|
Comparison.new(self, :==, nil)
|
|
90
133
|
end
|
|
91
134
|
|
|
135
|
+
# `IS NOT NULL`.
|
|
136
|
+
# @return [AST::Predicate]
|
|
92
137
|
def not_null?
|
|
93
138
|
Comparison.new(self, :!=, nil)
|
|
94
139
|
end
|
|
95
140
|
|
|
141
|
+
# `IS TRUE`: true of the rows where the boolean is true, false where it is false or NULL -- where `== true` would be NULL.
|
|
142
|
+
# @return [AST::Predicate]
|
|
143
|
+
# @example
|
|
144
|
+
# Post.where { :published.true? }
|
|
145
|
+
#
|
|
96
146
|
# IS TRUE and IS FALSE differ from a comparison against the literal in
|
|
97
147
|
# what they make of NULL: `flag = TRUE` is itself NULL there, and a
|
|
98
148
|
# NULL predicate selects nothing, while these two answer false. So the
|
|
@@ -102,34 +152,62 @@ module ActiveRecord
|
|
|
102
152
|
TruthValue.new(self, true)
|
|
103
153
|
end
|
|
104
154
|
|
|
155
|
+
# `IS NOT TRUE`: keeps the NULL rows that `!(:flag == true)` drops.
|
|
156
|
+
# @return [AST::Predicate]
|
|
105
157
|
def not_true?
|
|
106
158
|
TruthValue.new(self, true, negated: true)
|
|
107
159
|
end
|
|
108
160
|
|
|
161
|
+
# `IS FALSE`.
|
|
162
|
+
# @return [AST::Predicate]
|
|
109
163
|
def false?
|
|
110
164
|
TruthValue.new(self, false)
|
|
111
165
|
end
|
|
112
166
|
|
|
167
|
+
# `IS NOT FALSE`.
|
|
168
|
+
# @return [AST::Predicate]
|
|
113
169
|
def not_false?
|
|
114
170
|
TruthValue.new(self, false, negated: true)
|
|
115
171
|
end
|
|
116
172
|
|
|
173
|
+
# `IN (...)`: an array of values, a range, or a relation as a subquery.
|
|
174
|
+
# @param values [Array, Range, ActiveRecord::Relation]
|
|
175
|
+
# @return [AST::Predicate]
|
|
176
|
+
# @example
|
|
177
|
+
# Author.where { :country.in?(%w[JP US]) }
|
|
178
|
+
# Author.where { :id.in?(Post.select(:author_id)) }
|
|
117
179
|
def in?(values)
|
|
118
180
|
In.new(self, values)
|
|
119
181
|
end
|
|
120
182
|
|
|
183
|
+
# `NOT IN (...)`.
|
|
184
|
+
# @return [AST::Predicate]
|
|
121
185
|
def not_in?(values)
|
|
122
186
|
In.new(self, values, negated: true)
|
|
123
187
|
end
|
|
124
188
|
|
|
189
|
+
# `BETWEEN min AND max`, with either end a value, a column or an expression.
|
|
190
|
+
# @return [AST::Predicate]
|
|
191
|
+
# @example
|
|
192
|
+
# Author.where { :age.between?(20, 40) }
|
|
193
|
+
#
|
|
194
|
+
# Not min..max: an endpoint may be an expression, which Range would
|
|
195
|
+
# refuse to hold, since expressions do not compare among themselves.
|
|
125
196
|
def between?(min, max)
|
|
126
|
-
In.new(self, min
|
|
197
|
+
In.new(self, In::QuotedRange.new(min, max, false))
|
|
127
198
|
end
|
|
128
199
|
|
|
200
|
+
# `NOT BETWEEN min AND max`.
|
|
201
|
+
# @return [AST::Predicate]
|
|
129
202
|
def not_between?(min, max)
|
|
130
|
-
In.new(self, min
|
|
203
|
+
In.new(self, In::QuotedRange.new(min, max, false), negated: true)
|
|
131
204
|
end
|
|
132
205
|
|
|
206
|
+
# `CASE column WHEN value THEN ...`: a CASE with this as the operand, each `when` a value it is compared against, followed by `then` and finally `else`.
|
|
207
|
+
# @return [AST::Case::When]
|
|
208
|
+
# @example
|
|
209
|
+
# Author.select { :country.when("JP").then("Japan").else("elsewhere").as(:where) }
|
|
210
|
+
#
|
|
133
211
|
# CASE with this as the operand, compared against each `when`:
|
|
134
212
|
# `:age.when(10).then(1).else(0)`. The other shape, where each `when`
|
|
135
213
|
# carries its own condition, starts at `case_when`.
|
|
@@ -137,22 +215,38 @@ module ActiveRecord
|
|
|
137
215
|
Case.new(self).when(value, &block)
|
|
138
216
|
end
|
|
139
217
|
|
|
218
|
+
# `LIKE pattern`, the pattern as written: `%` and `_` are its wildcards.
|
|
219
|
+
# @param pattern [String]
|
|
220
|
+
# @return [AST::Predicate]
|
|
221
|
+
# @example
|
|
222
|
+
# Author.where { :name.like?("A%") }
|
|
140
223
|
def like?(pattern)
|
|
141
224
|
Like.new(self, pattern)
|
|
142
225
|
end
|
|
143
226
|
|
|
227
|
+
# `NOT LIKE pattern`.
|
|
228
|
+
# @return [AST::Predicate]
|
|
144
229
|
def not_like?(pattern)
|
|
145
230
|
Like.new(self, pattern, negated: true)
|
|
146
231
|
end
|
|
147
232
|
|
|
233
|
+
# A case-insensitive `LIKE`: `ILIKE` on PostgreSQL, and `LIKE` over both sides lower-cased elsewhere.
|
|
234
|
+
# @return [AST::Predicate]
|
|
148
235
|
def ilike?(pattern)
|
|
149
236
|
Like.new(self, pattern, nil, case_sensitive: false)
|
|
150
237
|
end
|
|
151
238
|
|
|
239
|
+
# The negated case-insensitive `LIKE`.
|
|
240
|
+
# @return [AST::Predicate]
|
|
152
241
|
def not_ilike?(pattern)
|
|
153
242
|
Like.new(self, pattern, nil, case_sensitive: false, negated: true)
|
|
154
243
|
end
|
|
155
244
|
|
|
245
|
+
# Case-insensitive equality: `LOWER(column) = LOWER(value)`.
|
|
246
|
+
# @return [AST::Predicate]
|
|
247
|
+
# @example
|
|
248
|
+
# Author.where { :name.casecmp?("Alice") }
|
|
249
|
+
#
|
|
156
250
|
# Case-insensitive equality, folded on both sides rather than left to
|
|
157
251
|
# the collation, so it means the same thing on every adapter.
|
|
158
252
|
def casecmp?(value)
|
|
@@ -163,34 +257,56 @@ module ActiveRecord
|
|
|
163
257
|
Function.new("LOWER", [value]))
|
|
164
258
|
end
|
|
165
259
|
|
|
260
|
+
# `IS DISTINCT FROM`: `!=` that treats NULL as a value. `IS NOT` on SQLite, `NOT <=>` on MySQL.
|
|
261
|
+
# @return [AST::Predicate]
|
|
262
|
+
#
|
|
166
263
|
# Null-safe comparison: unlike = and <>, these treat NULL as a value,
|
|
167
264
|
# so not_distinct_from? is the one equality that may take nil.
|
|
168
265
|
def distinct_from?(value)
|
|
169
266
|
DistinctFrom.new(self, value, negated: true)
|
|
170
267
|
end
|
|
171
268
|
|
|
269
|
+
# `IS NOT DISTINCT FROM`: `=` that treats NULL as a value, so this is the one equality that takes `nil`.
|
|
270
|
+
# @return [AST::Predicate]
|
|
271
|
+
# @example
|
|
272
|
+
# Author.where { :country.not_distinct_from?(nil) }
|
|
172
273
|
def not_distinct_from?(value)
|
|
173
274
|
DistinctFrom.new(self, value)
|
|
174
275
|
end
|
|
175
276
|
|
|
277
|
+
# `LIKE 'prefix%'`, the prefix escaped so that a `%` or `_` in it is itself; several prefixes are `OR`ed.
|
|
278
|
+
# @return [AST::Predicate]
|
|
279
|
+
# @example
|
|
280
|
+
# Author.where { :name.start_with?("A", "B") }
|
|
176
281
|
def start_with?(*prefixes)
|
|
177
282
|
if prefixes.empty?
|
|
178
283
|
raise ArgumentError, "start_with? needs at least one prefix"
|
|
179
284
|
end
|
|
180
|
-
Like.any(self, prefixes.map {|prefix| "#{Like.escape(prefix)}%" })
|
|
285
|
+
Like.any(self, prefixes.map { |prefix| "#{Like.escape(prefix)}%" })
|
|
181
286
|
end
|
|
182
287
|
|
|
288
|
+
# `LIKE '%suffix'`, escaped as {#start_with?} escapes.
|
|
289
|
+
# @return [AST::Predicate]
|
|
183
290
|
def end_with?(*suffixes)
|
|
184
291
|
if suffixes.empty?
|
|
185
292
|
raise ArgumentError, "end_with? needs at least one suffix"
|
|
186
293
|
end
|
|
187
|
-
Like.any(self, suffixes.map {|suffix| "%#{Like.escape(suffix)}" })
|
|
294
|
+
Like.any(self, suffixes.map { |suffix| "%#{Like.escape(suffix)}" })
|
|
188
295
|
end
|
|
189
296
|
|
|
297
|
+
# `LIKE '%substring%'`, escaped as {#start_with?} escapes.
|
|
298
|
+
# @return [AST::Predicate]
|
|
299
|
+
# @example
|
|
300
|
+
# Post.where { :title.include?("ruby") }
|
|
190
301
|
def include?(substring)
|
|
191
302
|
Like.new(self, "%#{Like.escape(substring)}%", Like::ESCAPE)
|
|
192
303
|
end
|
|
193
304
|
|
|
305
|
+
# Whether a PostgreSQL array column holds the element: `@> ARRAY[element]`.
|
|
306
|
+
# @return [AST::Predicate]
|
|
307
|
+
# @example
|
|
308
|
+
# Post.where { :tags.member?("ruby") }
|
|
309
|
+
#
|
|
194
310
|
# The array comparisons carry the meaning of their Ruby namesakes.
|
|
195
311
|
# member? is Enumerable's element test, so an Array argument is
|
|
196
312
|
# rejected rather than quietly meaning something Array#member? does
|
|
@@ -203,18 +319,32 @@ module ActiveRecord
|
|
|
203
319
|
ArrayPredicate.new(self, :"@>", [element])
|
|
204
320
|
end
|
|
205
321
|
|
|
322
|
+
# Whether an array column holds every element given: `@>`.
|
|
323
|
+
# @return [AST::Predicate]
|
|
206
324
|
def superset?(elements)
|
|
207
325
|
ArrayPredicate.new(self, :"@>", ArrayPredicate.elements(elements, "superset?"))
|
|
208
326
|
end
|
|
209
327
|
|
|
328
|
+
# Whether every element of an array column is among those given: `<@`.
|
|
329
|
+
# @return [AST::Predicate]
|
|
210
330
|
def subset?(elements)
|
|
211
331
|
ArrayPredicate.new(self, :"<@", ArrayPredicate.elements(elements, "subset?"))
|
|
212
332
|
end
|
|
213
333
|
|
|
334
|
+
# Whether an array column and the elements given share any: `&&`.
|
|
335
|
+
# @return [AST::Predicate]
|
|
336
|
+
# @example
|
|
337
|
+
# Post.where { :tags.intersect?(%w[ruby sql]) }
|
|
214
338
|
def intersect?(elements)
|
|
215
339
|
ArrayPredicate.new(self, :"&&", ArrayPredicate.elements(elements, "intersect?"))
|
|
216
340
|
end
|
|
217
341
|
|
|
342
|
+
# The JSON at a path into a JSON column, still JSON -- to be dug further, compared with a Ruby value, or asked {#key?} and the rest. A string or a symbol steps into an object, an integer into an array. `#>` on PostgreSQL, `JSON_EXTRACT` on MySQL, `->` on SQLite.
|
|
343
|
+
# @return [AST::JsonPath]
|
|
344
|
+
# @example
|
|
345
|
+
# Doc.select { :meta.dig(:author).as(:author) }
|
|
346
|
+
# Doc.where { :meta.dig(:author).key?(:name) }
|
|
347
|
+
#
|
|
218
348
|
# Reading inside a JSON document, by the name of what Hash does. A
|
|
219
349
|
# string or symbol steps into an object, an integer into an array, and
|
|
220
350
|
# what comes back is still JSON, the way Hash#dig hands back the
|
|
@@ -222,13 +352,22 @@ module ActiveRecord
|
|
|
222
352
|
# the JSON questions. dig_text gives the value as text instead,
|
|
223
353
|
# which is what a comparison wants.
|
|
224
354
|
def dig(*path)
|
|
225
|
-
JsonPath.new(self, path
|
|
355
|
+
JsonPath.new(self, path)
|
|
226
356
|
end
|
|
227
357
|
|
|
358
|
+
# The value at a path as text, which is what a comparison against a string wants where the JSON type would not do: `#>>` on PostgreSQL, `JSON_UNQUOTE(JSON_EXTRACT(...))` on MySQL, `->>` on SQLite.
|
|
359
|
+
# @return [AST::JsonPath]
|
|
360
|
+
# @example
|
|
361
|
+
# Doc.where { :meta.dig_text(:author, :name) == "alice" }
|
|
228
362
|
def dig_text(*path)
|
|
229
|
-
JsonPath.new(self, path)
|
|
363
|
+
JsonPath.new(self, path, json_value: false)
|
|
230
364
|
end
|
|
231
365
|
|
|
366
|
+
# The document without the keys given, as Hash#except gives it; an expression, for `update_all` to write back.
|
|
367
|
+
# @return [AST::JsonExcept]
|
|
368
|
+
# @example
|
|
369
|
+
# Doc.update_all { { meta: :meta.except(:draft) } }
|
|
370
|
+
#
|
|
232
371
|
# Keys taken out of a JSON document, by the name of what Hash does,
|
|
233
372
|
# and taking keys as Hash#except takes them. Like bury it gives back
|
|
234
373
|
# the document changed rather than writing it anywhere.
|
|
@@ -236,6 +375,11 @@ module ActiveRecord
|
|
|
236
375
|
JsonExcept.new(self, keys)
|
|
237
376
|
end
|
|
238
377
|
|
|
378
|
+
# The document with a value set at a path, as {#dig} reads one; an expression, for `update_all` to write back.
|
|
379
|
+
# @return [AST::JsonSet]
|
|
380
|
+
# @example
|
|
381
|
+
# Doc.update_all { { meta: :meta.bury(:author, :name, "alice") } }
|
|
382
|
+
#
|
|
239
383
|
# What dig reads, bury sets: the last argument is the value and the
|
|
240
384
|
# rest are the path to it. The document comes back changed rather
|
|
241
385
|
# than being written anywhere, which update_all is for.
|
|
@@ -243,40 +387,78 @@ module ActiveRecord
|
|
|
243
387
|
JsonSet.new(self, path, value)
|
|
244
388
|
end
|
|
245
389
|
|
|
390
|
+
# Whether the document contains the Ruby document given, which SQL calls containment: `@>` on PostgreSQL, `JSON_CONTAINS` on MySQL. SQLite and MariaDB have none.
|
|
391
|
+
# @return [AST::Predicate]
|
|
392
|
+
# @example
|
|
393
|
+
# Doc.where { :meta.contains?(author: { name: "alice" }) }
|
|
394
|
+
#
|
|
246
395
|
# Whether the document holds what is given, which SQL calls
|
|
247
396
|
# containment. SQLite has no equivalent.
|
|
248
397
|
def contains?(value)
|
|
249
398
|
JsonContains.new(self, value)
|
|
250
399
|
end
|
|
251
400
|
|
|
401
|
+
# Whether the object has the key, as Hash#key? asks.
|
|
402
|
+
# @return [AST::Predicate]
|
|
403
|
+
# @example
|
|
404
|
+
# Doc.where { :meta.key?(:author) }
|
|
405
|
+
#
|
|
252
406
|
# Whether the key is there at all, as Hash#key? asks. Hash has
|
|
253
407
|
# has_key? too; one name is enough, and this is the one Ruby's own
|
|
254
408
|
# style prefers.
|
|
255
409
|
def key?(key)
|
|
256
410
|
JsonHasKey.new(self, key)
|
|
257
411
|
end
|
|
412
|
+
|
|
413
|
+
# The keys of the object as a JSON array, as Hash#keys gives them. Oracle has none.
|
|
414
|
+
# @return [AST::JsonKeys]
|
|
415
|
+
#
|
|
416
|
+
# The keys of the document, as Hash#keys gives them: a JSON array.
|
|
417
|
+
def keys
|
|
418
|
+
JsonKeys.new(self)
|
|
419
|
+
end
|
|
258
420
|
end
|
|
259
421
|
|
|
422
|
+
# The arithmetic and the bitwise operators on a column or an
|
|
423
|
+
# expression. Ruby puts all of them above the comparisons, so
|
|
424
|
+
# `:price * :quantity > 100` groups the way it reads, and a number on
|
|
425
|
+
# the left -- `20 - :quantity` -- builds the same expression.
|
|
426
|
+
#
|
|
427
|
+
# @example
|
|
428
|
+
# LineItem.where { :price * :quantity > 1000 }
|
|
429
|
+
# LineItem.select { (:flags & 4).as(:featured) }
|
|
430
|
+
#
|
|
260
431
|
# Arithmetic builders shared by symbols, qualified columns and
|
|
261
432
|
# expressions. Imported into the Symbol refinement like Predications,
|
|
262
433
|
# so every method must be defined with def.
|
|
263
434
|
module Arithmetics
|
|
435
|
+
# `+`; with an Active Support duration on the right, a date moved: `:due_on + 3.days`.
|
|
436
|
+
# @return [AST::Arithmetic]
|
|
264
437
|
def +(other)
|
|
265
438
|
Arithmetic.new(self, :+, other)
|
|
266
439
|
end
|
|
267
440
|
|
|
441
|
+
# `-`; with a duration on the right, a date moved back.
|
|
442
|
+
# @return [AST::Arithmetic]
|
|
268
443
|
def -(other)
|
|
269
444
|
Arithmetic.new(self, :-, other)
|
|
270
445
|
end
|
|
271
446
|
|
|
447
|
+
# `*`.
|
|
448
|
+
# @return [AST::Arithmetic]
|
|
272
449
|
def *(other)
|
|
273
450
|
Arithmetic.new(self, :*, other)
|
|
274
451
|
end
|
|
275
452
|
|
|
453
|
+
# `/`.
|
|
454
|
+
# @return [AST::Arithmetic]
|
|
276
455
|
def /(other)
|
|
277
456
|
Arithmetic.new(self, :/, other)
|
|
278
457
|
end
|
|
279
458
|
|
|
459
|
+
# Bitwise AND. `&` between two conditions is AND, which leaves this free to mean the SQL operator.
|
|
460
|
+
# @return [AST::Bitwise]
|
|
461
|
+
#
|
|
280
462
|
# SQL's bitwise operators. & and | are AND and OR between conditions
|
|
281
463
|
# and are defined there, which is what leaves them free to mean here
|
|
282
464
|
# what SQL means by them. Ruby's precedence puts all six above the
|
|
@@ -285,27 +467,90 @@ module ActiveRecord
|
|
|
285
467
|
Bitwise.new(self, :&, other)
|
|
286
468
|
end
|
|
287
469
|
|
|
470
|
+
# Bitwise OR.
|
|
471
|
+
# @return [AST::Bitwise]
|
|
288
472
|
def |(other)
|
|
289
473
|
Bitwise.new(self, :|, other)
|
|
290
474
|
end
|
|
291
475
|
|
|
476
|
+
# Bitwise XOR: `#` on PostgreSQL, `^` on MySQL, and the two operations it is made of on SQLite.
|
|
477
|
+
# @return [AST::Bitwise]
|
|
292
478
|
def ^(other)
|
|
293
479
|
Bitwise.new(self, :^, other)
|
|
294
480
|
end
|
|
295
481
|
|
|
482
|
+
# A shift left.
|
|
483
|
+
# @return [AST::Bitwise]
|
|
296
484
|
def <<(other)
|
|
297
485
|
Bitwise.new(self, :<<, other)
|
|
298
486
|
end
|
|
299
487
|
|
|
488
|
+
# A shift right.
|
|
489
|
+
# @return [AST::Bitwise]
|
|
300
490
|
def >>(other)
|
|
301
491
|
Bitwise.new(self, :>>, other)
|
|
302
492
|
end
|
|
303
493
|
|
|
494
|
+
# Bitwise NOT.
|
|
495
|
+
# @return [AST::BitwiseNot]
|
|
304
496
|
def ~
|
|
305
497
|
BitwiseNot.new(self)
|
|
306
498
|
end
|
|
307
499
|
end
|
|
308
500
|
|
|
501
|
+
# Arithmetic with the number on the left, imported into the numeric
|
|
502
|
+
# refinements: 20 - :quantity builds what :quantity + 20 builds. Only
|
|
503
|
+
# a column or an expression on the right means a query; anything else
|
|
504
|
+
# goes back to the number through super, so 1 + 2 is 3 inside a block
|
|
505
|
+
# too.
|
|
506
|
+
# @private
|
|
507
|
+
module NumericArithmetics
|
|
508
|
+
def +(other)
|
|
509
|
+
return super unless other.is_a?(::Symbol) || other.is_a?(Node)
|
|
510
|
+
Arithmetic.new(self, :+, other)
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
def -(other)
|
|
514
|
+
return super unless other.is_a?(::Symbol) || other.is_a?(Node)
|
|
515
|
+
Arithmetic.new(self, :-, other)
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
def *(other)
|
|
519
|
+
return super unless other.is_a?(::Symbol) || other.is_a?(Node)
|
|
520
|
+
Arithmetic.new(self, :*, other)
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
def /(other)
|
|
524
|
+
return super unless other.is_a?(::Symbol) || other.is_a?(Node)
|
|
525
|
+
Arithmetic.new(self, :/, other)
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
def &(other)
|
|
529
|
+
return super unless other.is_a?(::Symbol) || other.is_a?(Node)
|
|
530
|
+
Bitwise.new(self, :&, other)
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
def |(other)
|
|
534
|
+
return super unless other.is_a?(::Symbol) || other.is_a?(Node)
|
|
535
|
+
Bitwise.new(self, :|, other)
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
def ^(other)
|
|
539
|
+
return super unless other.is_a?(::Symbol) || other.is_a?(Node)
|
|
540
|
+
Bitwise.new(self, :^, other)
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
def <<(other)
|
|
544
|
+
return super unless other.is_a?(::Symbol) || other.is_a?(Node)
|
|
545
|
+
Bitwise.new(self, :<<, other)
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
def >>(other)
|
|
549
|
+
return super unless other.is_a?(::Symbol) || other.is_a?(Node)
|
|
550
|
+
Bitwise.new(self, :>>, other)
|
|
551
|
+
end
|
|
552
|
+
end
|
|
553
|
+
|
|
309
554
|
class Node
|
|
310
555
|
# The model travels with the table because some SQL cannot be written
|
|
311
556
|
# without knowing the adapter, and a node is built before anything
|
|
@@ -316,38 +561,86 @@ module ActiveRecord
|
|
|
316
561
|
raise ScriptError, "subclass must override this method"
|
|
317
562
|
end
|
|
318
563
|
|
|
564
|
+
# The expression under an alias, as {BlockSyntax#as} gives a column
|
|
565
|
+
# one.
|
|
566
|
+
# @return [AST::As]
|
|
319
567
|
def as(alias_name, quote: true)
|
|
320
568
|
As.new(self, alias_name, quote: quote)
|
|
321
569
|
end
|
|
322
570
|
|
|
571
|
+
# An ascending ordering by the expression.
|
|
572
|
+
# @return [AST::Ordering]
|
|
323
573
|
def asc
|
|
324
574
|
Ordering.new(self, :asc)
|
|
325
575
|
end
|
|
326
576
|
|
|
577
|
+
# A descending ordering by the expression.
|
|
578
|
+
# @return [AST::Ordering]
|
|
327
579
|
def desc
|
|
328
580
|
Ordering.new(self, :desc)
|
|
329
581
|
end
|
|
330
582
|
|
|
583
|
+
# The expression under a collation, as {BlockSyntax#collate}.
|
|
584
|
+
# @return [AST::Collate]
|
|
585
|
+
def collate(name)
|
|
586
|
+
Collate.new(self, name)
|
|
587
|
+
end
|
|
588
|
+
|
|
331
589
|
private
|
|
590
|
+
# Resolves an operand denoting a column or an expression. A number
|
|
591
|
+
# rides along for Arel to write out, which it can do for Integer and
|
|
592
|
+
# Float alone: a BigDecimal is quoted, which the adapter spells as
|
|
593
|
+
# the exact decimal, and a Rational, which no decimal spells exactly,
|
|
594
|
+
# is refused.
|
|
595
|
+
def to_arel_operand(operand, table, model)
|
|
596
|
+
case operand
|
|
597
|
+
when Node then operand.to_arel(table, model)
|
|
598
|
+
when :* then Arel.star
|
|
599
|
+
when Symbol then table[operand]
|
|
600
|
+
when ::BigDecimal, ::Rational then quote_number(operand)
|
|
601
|
+
else operand
|
|
602
|
+
end
|
|
603
|
+
end
|
|
332
604
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
605
|
+
# A bare symbol is a column in every position, the value side of a
|
|
606
|
+
# comparison included. The name is checked against the model, since
|
|
607
|
+
# a name it has no column for is almost always an enum value spelled
|
|
608
|
+
# as a symbol -- which, taken as a column, would quietly compare
|
|
609
|
+
# against nothing anyone meant.
|
|
610
|
+
def column_operand(name, table, model)
|
|
611
|
+
unless model.column_names.include?(name.to_s)
|
|
612
|
+
raise ArgumentError,
|
|
613
|
+
"#{name.inspect} is no column of #{model.table_name}; an enum " \
|
|
614
|
+
"value is written as its string, a column of another table " \
|
|
615
|
+
"qualified"
|
|
616
|
+
end
|
|
617
|
+
table[name]
|
|
340
618
|
end
|
|
341
|
-
end
|
|
342
619
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
620
|
+
# A number compares as itself, the way a bound ? does: the typed path
|
|
621
|
+
# would cast 99.5 against an integer column to 99 and quietly move
|
|
622
|
+
# the boundary. Everything else keeps the column's own
|
|
623
|
+
# serialization -- an enum's name, a time's zone, a custom type's
|
|
624
|
+
# scaling.
|
|
625
|
+
def quote_number(value)
|
|
626
|
+
case value
|
|
627
|
+
when ::Rational
|
|
628
|
+
raise ArgumentError,
|
|
629
|
+
"a Rational has no exact SQL spelling; to_d says the decimal meant"
|
|
630
|
+
when ::Integer, ::Float, ::BigDecimal
|
|
631
|
+
Arel::Nodes.build_quoted(value)
|
|
632
|
+
else value
|
|
633
|
+
end
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
# Resolves a function argument: a column or an expression as above,
|
|
637
|
+
# anything else a value to be quoted.
|
|
638
|
+
def to_arel_argument(arg, table, model)
|
|
639
|
+
case arg
|
|
640
|
+
when Node, Symbol, ::Rational then to_arel_operand(arg, table, model)
|
|
641
|
+
else Arel::Nodes.build_quoted(arg)
|
|
642
|
+
end
|
|
349
643
|
end
|
|
350
|
-
end
|
|
351
644
|
end
|
|
352
645
|
|
|
353
646
|
class Predicate < Node
|
|
@@ -366,11 +659,11 @@ module ActiveRecord
|
|
|
366
659
|
|
|
367
660
|
# A literal standing where an expression would: `select { value(0).as(:depth) }`.
|
|
368
661
|
#
|
|
369
|
-
# Values reach the SQL quoted wherever they appear as an operand, but
|
|
370
|
-
# top of a select list is
|
|
371
|
-
#
|
|
372
|
-
#
|
|
373
|
-
#
|
|
662
|
+
# Values reach the SQL quoted wherever they appear as an operand, but a
|
|
663
|
+
# bare Ruby literal at the top of a select list is refused as saying
|
|
664
|
+
# nothing. `value` is the spelling that quotes it there, and it carries
|
|
665
|
+
# the predications with it, so a literal can be compared and combined
|
|
666
|
+
# like anything else.
|
|
374
667
|
class Value < Node
|
|
375
668
|
include Predications
|
|
376
669
|
include Arithmetics
|
|
@@ -386,6 +679,43 @@ module ActiveRecord
|
|
|
386
679
|
end
|
|
387
680
|
end
|
|
388
681
|
|
|
682
|
+
# SQL as written: `sql("length(name) > ?", 10)`. The ? and :name
|
|
683
|
+
# placeholders take quoted values through sanitize_sql_array, which
|
|
684
|
+
# needs the connection, so the binds wait here until the model is
|
|
685
|
+
# known. Without binds the statement passes untouched -- which is
|
|
686
|
+
# what leaves PostgreSQL's ? operators writable, since only the
|
|
687
|
+
# positional-bind rewrite reads ? as a placeholder.
|
|
688
|
+
#
|
|
689
|
+
# As an operand the statement is parenthesized: its precedence is
|
|
690
|
+
# whatever was written inside. The top of a select list gets it bare,
|
|
691
|
+
# through field_arel, where parentheses would refuse an alias written
|
|
692
|
+
# into the string.
|
|
693
|
+
class Sql < Node
|
|
694
|
+
include Predications
|
|
695
|
+
include Arithmetics
|
|
696
|
+
|
|
697
|
+
attr_reader :statement, :binds
|
|
698
|
+
|
|
699
|
+
def initialize(statement, binds)
|
|
700
|
+
unless statement.is_a?(::String)
|
|
701
|
+
raise ArgumentError,
|
|
702
|
+
"sql takes the statement as a string, not #{statement.inspect}"
|
|
703
|
+
end
|
|
704
|
+
@statement = statement
|
|
705
|
+
@binds = binds
|
|
706
|
+
end
|
|
707
|
+
|
|
708
|
+
def to_arel(_table, model)
|
|
709
|
+
Arel::Nodes::Grouping.new(field_arel(model))
|
|
710
|
+
end
|
|
711
|
+
|
|
712
|
+
def field_arel(model)
|
|
713
|
+
return Arel.sql(statement) if binds.empty?
|
|
714
|
+
|
|
715
|
+
Arel.sql(model.sanitize_sql_array([statement, *binds]))
|
|
716
|
+
end
|
|
717
|
+
end
|
|
718
|
+
|
|
389
719
|
# CASE, in both of the shapes SQL has for it. With an operand, each
|
|
390
720
|
# `when` is something to compare it against; without one, each `when` is
|
|
391
721
|
# a condition of its own.
|
|
@@ -409,16 +739,23 @@ module ActiveRecord
|
|
|
409
739
|
@default = default
|
|
410
740
|
end
|
|
411
741
|
|
|
742
|
+
# The next `WHEN`: a value to compare the operand against, or a condition as a value or a block.
|
|
743
|
+
# @return [AST::Case::When]
|
|
412
744
|
def when(value = nil, &block)
|
|
413
745
|
Pending.new(self, Case.argument(:when, value, block))
|
|
414
746
|
end
|
|
415
747
|
|
|
748
|
+
# `THEN`, which belongs after a `when`; here it says so.
|
|
749
|
+
# @raise [ArgumentError]
|
|
750
|
+
#
|
|
416
751
|
# Kernel#then is on every object, so `then` in the wrong place would be
|
|
417
752
|
# answered by it -- with no block, silently, with an Enumerator.
|
|
418
753
|
def then(*)
|
|
419
754
|
raise ArgumentError, "then follows a when, and there is none to follow here"
|
|
420
755
|
end
|
|
421
756
|
|
|
757
|
+
# `ELSE value`, as a value or a block, closing the CASE. Without one the CASE gives NULL where no `when` matched.
|
|
758
|
+
# @return [AST::Case]
|
|
422
759
|
def else(value = nil, &block)
|
|
423
760
|
Case.new(operand, whens, Case.argument(:else, value, block))
|
|
424
761
|
end
|
|
@@ -457,6 +794,8 @@ module ActiveRecord
|
|
|
457
794
|
@condition = condition
|
|
458
795
|
end
|
|
459
796
|
|
|
797
|
+
# `THEN value`, as a value or a block, for the `when` before it.
|
|
798
|
+
# @return [AST::Case]
|
|
460
799
|
def then(value = nil, &block)
|
|
461
800
|
Case.new(@kase.operand,
|
|
462
801
|
@kase.whens + [[@condition, Case.argument(:then, value, block)]],
|
|
@@ -485,24 +824,24 @@ module ActiveRecord
|
|
|
485
824
|
# quoted so that a comma or a brace in a key is part of it. except
|
|
486
825
|
# writes its keys the same way, which are steps of no one path.
|
|
487
826
|
def steps_array(steps = path)
|
|
488
|
-
"{#{steps.map {|step| %("#{escape_step(step)}") }.join(',')}}"
|
|
827
|
+
"{#{steps.map { |step| %("#{escape_step(step)}") }.join(',')}}"
|
|
489
828
|
end
|
|
490
829
|
|
|
491
830
|
# MySQL and SQLite take a path expression instead, where an integer is
|
|
492
831
|
# a subscript and a name that is not plain has to be quoted.
|
|
493
832
|
def dollar_path
|
|
494
|
-
path.inject(+
|
|
833
|
+
path.inject(+"$") { |so_far, step| so_far << dollar_step(step) }
|
|
495
834
|
end
|
|
496
835
|
|
|
497
836
|
def dollar_step(step)
|
|
498
837
|
return "[#{step}]" if step.is_a?(::Integer)
|
|
499
838
|
name = step.to_s
|
|
500
|
-
|
|
839
|
+
"." + (name.match?(/\A[[:alpha:]_][[:alnum:]_]*\z/) ?
|
|
501
840
|
name : %("#{escape_step(step)}"))
|
|
502
841
|
end
|
|
503
842
|
|
|
504
843
|
def escape_step(step)
|
|
505
|
-
step.to_s.gsub(
|
|
844
|
+
step.to_s.gsub("\\", "\\\\").gsub('"', '\\"')
|
|
506
845
|
end
|
|
507
846
|
end
|
|
508
847
|
|
|
@@ -521,11 +860,12 @@ module ActiveRecord
|
|
|
521
860
|
# `dig_text(:flag) == true` is true, an error, and false. cast is what
|
|
522
861
|
# says which type was meant, and then all three agree.
|
|
523
862
|
#
|
|
524
|
-
# dig
|
|
525
|
-
#
|
|
526
|
-
#
|
|
527
|
-
#
|
|
528
|
-
#
|
|
863
|
+
# dig, bury and except give JSON, and a JSON comparison belongs to the
|
|
864
|
+
# JSON types -- jsonb and MySQL's -- where numbers compare as numbers
|
|
865
|
+
# and documents structurally, key order and spelling aside. The Ruby
|
|
866
|
+
# value goes in as a JSON literal, and the adapters without such a
|
|
867
|
+
# type refuse it from JsonLiteral when the SQL is written, which is
|
|
868
|
+
# when the adapter is known.
|
|
529
869
|
#
|
|
530
870
|
# A string against dig_text, and anything the block itself built -- a
|
|
531
871
|
# column, a function, another dug value -- go through untouched.
|
|
@@ -537,15 +877,14 @@ module ActiveRecord
|
|
|
537
877
|
module JsonComparable
|
|
538
878
|
%i[== != < <= > >=].each do |operator|
|
|
539
879
|
define_method(operator) do |other|
|
|
540
|
-
|
|
541
|
-
super(other)
|
|
880
|
+
super(comparison_value(other))
|
|
542
881
|
end
|
|
543
882
|
end
|
|
544
883
|
|
|
545
|
-
def in?(values) = super(
|
|
546
|
-
def not_in?(values) = super(
|
|
547
|
-
def between?(min, max) = super(
|
|
548
|
-
def not_between?(min, max) = super(
|
|
884
|
+
def in?(values) = super(comparison_set(values))
|
|
885
|
+
def not_in?(values) = super(comparison_set(values))
|
|
886
|
+
def between?(min, max) = super(comparison_value(min), comparison_value(max))
|
|
887
|
+
def not_between?(min, max) = super(comparison_value(min), comparison_value(max))
|
|
549
888
|
|
|
550
889
|
%i[+ - * / & | ^ << >>].each do |operator|
|
|
551
890
|
define_method(operator) do |_other|
|
|
@@ -558,38 +897,71 @@ module ActiveRecord
|
|
|
558
897
|
end
|
|
559
898
|
|
|
560
899
|
private
|
|
900
|
+
# nil is left to the comparison itself, which says to use null?, and
|
|
901
|
+
# so is anything the block built rather than wrote as a literal.
|
|
902
|
+
def comparison_value(other)
|
|
903
|
+
return other if other.nil? || other.is_a?(Node) || other.is_a?(::Symbol) ||
|
|
904
|
+
other.is_a?(Arel::Nodes::Node) ||
|
|
905
|
+
other.is_a?(Arel::Attributes::Attribute) ||
|
|
906
|
+
other.is_a?(ActiveRecord::Relation)
|
|
907
|
+
return json_literal(other) if json_value?
|
|
908
|
+
return other if other.is_a?(::String)
|
|
909
|
+
|
|
910
|
+
raise ArgumentError,
|
|
911
|
+
"dig_text gives text, and comparing it with #{other.inspect} means " \
|
|
912
|
+
"something different on every adapter; cast it to the type meant"
|
|
913
|
+
end
|
|
561
914
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
915
|
+
def json_literal(other)
|
|
916
|
+
case other
|
|
917
|
+
when ::String, ::Integer, ::Float, ::BigDecimal, true, false, ::Hash, ::Array
|
|
918
|
+
JsonLiteral.new(other)
|
|
919
|
+
when ::Rational
|
|
920
|
+
raise ArgumentError,
|
|
921
|
+
"a Rational has no exact SQL spelling; to_d says the decimal meant"
|
|
922
|
+
else
|
|
923
|
+
raise ArgumentError,
|
|
924
|
+
"#{json_source} gives JSON, and #{other.inspect} has no JSON " \
|
|
925
|
+
"spelling; dig_text gives the value"
|
|
926
|
+
end
|
|
927
|
+
end
|
|
570
928
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
929
|
+
def comparison_set(values)
|
|
930
|
+
case values
|
|
931
|
+
when ActiveRecord::Relation then values
|
|
932
|
+
when ::Range
|
|
933
|
+
In::QuotedRange.new(comparison_value(values.begin),
|
|
934
|
+
comparison_value(values.end), values.exclude_end?)
|
|
935
|
+
else values.map { |value| comparison_value(value) }
|
|
936
|
+
end
|
|
937
|
+
end
|
|
577
938
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
939
|
+
def arithmetic_refusal(operator)
|
|
940
|
+
json_value? ?
|
|
941
|
+
"#{json_source} gives JSON, and #{operator} on it means something " \
|
|
942
|
+
"different on every adapter; cast dig_text to the type meant" :
|
|
943
|
+
"dig_text gives text, and #{operator} on it means something " \
|
|
944
|
+
"different on every adapter; cast it to the type meant"
|
|
583
945
|
end
|
|
584
|
-
|
|
946
|
+
end
|
|
947
|
+
|
|
948
|
+
# A Ruby value on the JSON side of a comparison, which jsonb and
|
|
949
|
+
# MySQL's JSON type answer alike: numbers compare as numbers and
|
|
950
|
+
# documents structurally. SQLite and MariaDB have only the text of
|
|
951
|
+
# each -- spelling and key order deciding what equality means -- and
|
|
952
|
+
# refuse here. PostgreSQL needs no cast, an untyped literal beside a
|
|
953
|
+
# jsonb operand coercing to jsonb; MySQL is told CAST(... AS JSON),
|
|
954
|
+
# since a bare string beside JSON would be a JSON string, and every
|
|
955
|
+
# string outranks every number in its ordering.
|
|
956
|
+
class JsonLiteral < Node
|
|
957
|
+
attr_reader :value
|
|
958
|
+
|
|
959
|
+
def initialize(value)
|
|
960
|
+
@value = value
|
|
585
961
|
end
|
|
586
962
|
|
|
587
|
-
def
|
|
588
|
-
|
|
589
|
-
"#{json_source} gives JSON, and #{operator} on it means something " \
|
|
590
|
-
"different on every adapter; cast dig_text to the type meant" :
|
|
591
|
-
"dig_text gives text, and #{operator} on it means something " \
|
|
592
|
-
"different on every adapter; cast it to the type meant"
|
|
963
|
+
def to_arel(_table, model)
|
|
964
|
+
Dialect.for(model).json_literal(Arel::Nodes.build_quoted(JSON.generate(value)), model)
|
|
593
965
|
end
|
|
594
966
|
end
|
|
595
967
|
|
|
@@ -600,9 +972,9 @@ module ActiveRecord
|
|
|
600
972
|
# again is where they part company: SQLite parses it back and MySQL
|
|
601
973
|
# takes it as written, where PostgreSQL has no such function for text.
|
|
602
974
|
module JsonDocument
|
|
603
|
-
%i[dig dig_text key? contains? bury except].each do |name|
|
|
975
|
+
%i[dig dig_text key? keys contains? bury except].each do |name|
|
|
604
976
|
define_method(name) do |*args|
|
|
605
|
-
unless
|
|
977
|
+
unless json_value?
|
|
606
978
|
raise ArgumentError,
|
|
607
979
|
"dig_text gives text, and #{name} reads JSON; dig keeps it"
|
|
608
980
|
end
|
|
@@ -611,6 +983,22 @@ module ActiveRecord
|
|
|
611
983
|
end
|
|
612
984
|
end
|
|
613
985
|
|
|
986
|
+
# JSON the query computes rather than reads out of a document: always
|
|
987
|
+
# a JSON value, with no dig_text counterpart for JsonComparable's
|
|
988
|
+
# advice to name. Included after JsonComparable, whose own
|
|
989
|
+
# arithmetic_refusal it overrides.
|
|
990
|
+
module ComputedJson
|
|
991
|
+
def json_value?
|
|
992
|
+
true
|
|
993
|
+
end
|
|
994
|
+
|
|
995
|
+
private
|
|
996
|
+
def arithmetic_refusal(operator)
|
|
997
|
+
"#{json_source} gives JSON, and #{operator} on it means " \
|
|
998
|
+
"something different on every adapter"
|
|
999
|
+
end
|
|
1000
|
+
end
|
|
1001
|
+
|
|
614
1002
|
class JsonPath < Node
|
|
615
1003
|
include Predications
|
|
616
1004
|
include Arithmetics
|
|
@@ -618,40 +1006,28 @@ module ActiveRecord
|
|
|
618
1006
|
include JsonComparable
|
|
619
1007
|
include JsonDocument
|
|
620
1008
|
|
|
621
|
-
attr_reader :operand, :path
|
|
1009
|
+
attr_reader :operand, :path
|
|
622
1010
|
|
|
623
|
-
def initialize(operand, path,
|
|
1011
|
+
def initialize(operand, path, json_value: true)
|
|
624
1012
|
@operand = operand
|
|
625
|
-
@path = check_steps(path,
|
|
626
|
-
@
|
|
1013
|
+
@path = check_steps(path, "dig")
|
|
1014
|
+
@json_value = json_value
|
|
1015
|
+
end
|
|
1016
|
+
|
|
1017
|
+
def json_value?
|
|
1018
|
+
@json_value
|
|
627
1019
|
end
|
|
628
1020
|
|
|
629
1021
|
def to_arel(table, model)
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
Arel::Nodes::InfixOperation.new(
|
|
634
|
-
as_json ? :"#>" : :"#>>", document, Arel::Nodes.build_quoted(steps_array))
|
|
635
|
-
when :mysql
|
|
636
|
-
extracted = Arel::Nodes::NamedFunction.new(
|
|
637
|
-
'JSON_EXTRACT', [document, Arel::Nodes.build_quoted(dollar_path)])
|
|
638
|
-
as_json ? extracted : Arel::Nodes::NamedFunction.new('JSON_UNQUOTE', [extracted])
|
|
639
|
-
else
|
|
640
|
-
extracted = Arel::Nodes::InfixOperation.new(
|
|
641
|
-
as_json ? :"->" : :"->>", document, Arel::Nodes.build_quoted(dollar_path))
|
|
642
|
-
# SQLite's ->> gives back the value with its type, where the other
|
|
643
|
-
# two give text. Cast so that `dig_text(:n) == '5'` means the
|
|
644
|
-
# same thing everywhere, and a number wants a cast everywhere too.
|
|
645
|
-
as_json ? extracted : Arel::Nodes::NamedFunction.new(
|
|
646
|
-
'CAST', [Arel::Nodes::As.new(extracted, Arel::Nodes::SqlLiteral.new('text'))])
|
|
647
|
-
end
|
|
1022
|
+
Dialect.for(model).json_path(
|
|
1023
|
+
to_arel_operand(operand, table, model),
|
|
1024
|
+
dollar_path, steps_array, json_value?, model)
|
|
648
1025
|
end
|
|
649
1026
|
|
|
650
1027
|
private
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
end
|
|
1028
|
+
def json_source
|
|
1029
|
+
"dig"
|
|
1030
|
+
end
|
|
655
1031
|
end
|
|
656
1032
|
|
|
657
1033
|
# Setting a value inside a JSON document, which is what bury does to what
|
|
@@ -666,67 +1042,30 @@ module ActiveRecord
|
|
|
666
1042
|
|
|
667
1043
|
def initialize(operand, path, value)
|
|
668
1044
|
@operand = operand
|
|
669
|
-
@path = check_steps(path,
|
|
1045
|
+
@path = check_steps(path, "bury")
|
|
670
1046
|
@value = value
|
|
671
1047
|
end
|
|
672
1048
|
|
|
673
1049
|
# Always JSON, which is what the comparison guard asks.
|
|
674
|
-
def
|
|
1050
|
+
def json_value?
|
|
675
1051
|
true
|
|
676
1052
|
end
|
|
677
1053
|
|
|
678
1054
|
def to_arel(table, model)
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
[document, Arel::Nodes.build_quoted(steps_array), postgresql_value(table, model)])
|
|
684
|
-
else
|
|
685
|
-
Arel::Nodes::NamedFunction.new(
|
|
686
|
-
'JSON_SET',
|
|
687
|
-
[document, Arel::Nodes.build_quoted(dollar_path), other_value(table, model)])
|
|
688
|
-
end
|
|
1055
|
+
Dialect.for(model).json_set(
|
|
1056
|
+
to_arel_operand(operand, table, model),
|
|
1057
|
+
steps_array, dollar_path, value,
|
|
1058
|
+
(to_arel_operand(value, table, model) if expression?), model)
|
|
689
1059
|
end
|
|
690
1060
|
|
|
691
1061
|
private
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
# value goes in as the JSON that says it -- '"x"' rather than 'x',
|
|
695
|
-
# which is not a document at all.
|
|
696
|
-
def postgresql_value(table, model)
|
|
697
|
-
return Arel::Nodes::NamedFunction.new(
|
|
698
|
-
'to_jsonb', [to_arel_operand(value, table, model)]) if expression?
|
|
699
|
-
Arel::Nodes.build_quoted(JSON.generate(value))
|
|
700
|
-
end
|
|
701
|
-
|
|
702
|
-
# The others take the value as it is, except a whole document or a
|
|
703
|
-
# boolean, which go in as JSON: taken as they are, a document would
|
|
704
|
-
# be the string that spells it, and a boolean SQLite's own 1.
|
|
705
|
-
# SQLite's json() marks the literal for JSON_SET; the MySQL family,
|
|
706
|
-
# which has no json(), reads it with JSON_EXTRACT.
|
|
707
|
-
def other_value(table, model)
|
|
708
|
-
return to_arel_operand(value, table, model) if expression?
|
|
709
|
-
unless value.is_a?(::Hash) || value.is_a?(::Array) ||
|
|
710
|
-
value == true || value == false
|
|
711
|
-
return Arel::Nodes.build_quoted(value)
|
|
712
|
-
end
|
|
713
|
-
|
|
714
|
-
json = Arel::Nodes.build_quoted(JSON.generate(value))
|
|
715
|
-
if AST.adapter_family(model) == :sqlite
|
|
716
|
-
Arel::Nodes::NamedFunction.new('json', [json])
|
|
717
|
-
else
|
|
718
|
-
Arel::Nodes::NamedFunction.new(
|
|
719
|
-
'JSON_EXTRACT', [json, Arel::Nodes.build_quoted('$')])
|
|
1062
|
+
def expression?
|
|
1063
|
+
value.is_a?(Node) || value.is_a?(::Symbol)
|
|
720
1064
|
end
|
|
721
|
-
end
|
|
722
|
-
|
|
723
|
-
def expression?
|
|
724
|
-
value.is_a?(Node) || value.is_a?(::Symbol)
|
|
725
|
-
end
|
|
726
1065
|
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
1066
|
+
def json_source
|
|
1067
|
+
"bury"
|
|
1068
|
+
end
|
|
730
1069
|
end
|
|
731
1070
|
|
|
732
1071
|
# Keys taken out of a JSON document. PostgreSQL subtracts them, the
|
|
@@ -743,52 +1082,33 @@ module ActiveRecord
|
|
|
743
1082
|
@keys = check_keys(keys)
|
|
744
1083
|
end
|
|
745
1084
|
|
|
746
|
-
def
|
|
1085
|
+
def json_value?
|
|
747
1086
|
true
|
|
748
1087
|
end
|
|
749
1088
|
|
|
750
1089
|
def to_arel(table, model)
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
return Arel::Nodes::InfixOperation.new(
|
|
756
|
-
:-, Arel::Nodes::Grouping.new(document), key_array)
|
|
757
|
-
end
|
|
758
|
-
|
|
759
|
-
Arel::Nodes::NamedFunction.new(
|
|
760
|
-
'JSON_REMOVE',
|
|
761
|
-
[document, *keys.map {|key| Arel::Nodes.build_quoted("$#{dollar_step(key)}") }])
|
|
1090
|
+
Dialect.for(model).json_remove(
|
|
1091
|
+
to_arel_operand(operand, table, model),
|
|
1092
|
+
keys.map { |key| "$#{dollar_step(key)}" },
|
|
1093
|
+
steps_array(keys), model)
|
|
762
1094
|
end
|
|
763
1095
|
|
|
764
1096
|
private
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
end
|
|
776
|
-
|
|
777
|
-
# Keys, as Hash#except takes them: an index into an array is not what
|
|
778
|
-
# the name says anywhere, and is bury's business through a path.
|
|
779
|
-
def check_keys(keys)
|
|
780
|
-
raise ArgumentError, 'except needs a key' if keys.empty?
|
|
781
|
-
keys.each do |key|
|
|
782
|
-
next if key.is_a?(::String) || key.is_a?(::Symbol)
|
|
783
|
-
raise ArgumentError,
|
|
784
|
-
"except takes keys of the document, not #{key.inspect}"
|
|
1097
|
+
# Keys, as Hash#except takes them: an index into an array is not what
|
|
1098
|
+
# the name says anywhere, and is bury's business through a path.
|
|
1099
|
+
def check_keys(keys)
|
|
1100
|
+
raise ArgumentError, "except needs a key" if keys.empty?
|
|
1101
|
+
keys.each do |key|
|
|
1102
|
+
next if key.is_a?(::String) || key.is_a?(::Symbol)
|
|
1103
|
+
raise ArgumentError,
|
|
1104
|
+
"except takes keys of the document, not #{key.inspect}"
|
|
1105
|
+
end
|
|
1106
|
+
keys
|
|
785
1107
|
end
|
|
786
|
-
keys
|
|
787
|
-
end
|
|
788
1108
|
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
1109
|
+
def json_source
|
|
1110
|
+
"except"
|
|
1111
|
+
end
|
|
792
1112
|
end
|
|
793
1113
|
|
|
794
1114
|
# JSON containment: whether the document holds what is given.
|
|
@@ -801,23 +1121,17 @@ module ActiveRecord
|
|
|
801
1121
|
end
|
|
802
1122
|
|
|
803
1123
|
def to_arel(table, model)
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
when :postgresql then Arel::Nodes::Contains.new(document, json)
|
|
808
|
-
when :mysql
|
|
809
|
-
Arel::Nodes::NamedFunction.new('JSON_CONTAINS', [document, json])
|
|
810
|
-
else
|
|
811
|
-
# Later than the others, since the adapter is only known here.
|
|
812
|
-
raise NotImplementedError,
|
|
813
|
-
"contains? has no equivalent on #{model.connection_db_config.adapter}"
|
|
814
|
-
end
|
|
1124
|
+
Dialect.for(model).json_contains(
|
|
1125
|
+
to_arel_operand(operand, table, model),
|
|
1126
|
+
Arel::Nodes.build_quoted(JSON.generate(value)), model)
|
|
815
1127
|
end
|
|
816
1128
|
end
|
|
817
1129
|
|
|
818
|
-
# Whether a key is in the document. PostgreSQL
|
|
819
|
-
#
|
|
820
|
-
#
|
|
1130
|
+
# Whether a key is in the document. PostgreSQL's spelling is the ?
|
|
1131
|
+
# operator rather than jsonb_exists, the function it is shorthand for,
|
|
1132
|
+
# because a GIN index matches the operator and never the function. A
|
|
1133
|
+
# ? is a bind placeholder only to sanitize_sql, which none of the SQL
|
|
1134
|
+
# written here passes through.
|
|
821
1135
|
class JsonHasKey < Predicate
|
|
822
1136
|
attr_reader :operand, :key
|
|
823
1137
|
|
|
@@ -827,28 +1141,111 @@ module ActiveRecord
|
|
|
827
1141
|
end
|
|
828
1142
|
|
|
829
1143
|
def to_arel(table, model)
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
1144
|
+
Dialect.for(model).json_has_key(
|
|
1145
|
+
to_arel_operand(operand, table, model),
|
|
1146
|
+
Arel::Nodes.build_quoted(key.to_s),
|
|
1147
|
+
Arel::Nodes.build_quoted("$.#{key}"), model)
|
|
1148
|
+
end
|
|
1149
|
+
end
|
|
1150
|
+
|
|
1151
|
+
# The keys of a JSON document, as Hash#keys gives them: a JSON array.
|
|
1152
|
+
# Only the MySQL family has a function for it; the other two reach the
|
|
1153
|
+
# same array through a subquery over their key-listing functions. The
|
|
1154
|
+
# type guard is what makes all four answer alike: the keys of what is
|
|
1155
|
+
# not an object are NULL rather than SQLite's array indices or
|
|
1156
|
+
# PostgreSQL's error, and the keys of {} are [] rather than
|
|
1157
|
+
# PostgreSQL's NULL, jsonb_agg over no rows.
|
|
1158
|
+
class JsonKeys < Node
|
|
1159
|
+
include Predications
|
|
1160
|
+
include JsonComparable
|
|
1161
|
+
include ComputedJson
|
|
1162
|
+
|
|
1163
|
+
attr_reader :operand
|
|
1164
|
+
|
|
1165
|
+
def initialize(operand)
|
|
1166
|
+
@operand = operand
|
|
1167
|
+
end
|
|
1168
|
+
|
|
1169
|
+
def to_arel(table, model)
|
|
1170
|
+
Dialect.for(model).json_keys(to_arel_operand(operand, table, model), model)
|
|
1171
|
+
end
|
|
1172
|
+
|
|
1173
|
+
private
|
|
1174
|
+
def json_source
|
|
1175
|
+
"keys"
|
|
1176
|
+
end
|
|
1177
|
+
end
|
|
1178
|
+
|
|
1179
|
+
# A JSON document built in the row: json_array from the values given,
|
|
1180
|
+
# json_object from a Ruby hash. SQLite and the MySQL family both say
|
|
1181
|
+
# the standard names; PostgreSQL is asked to build jsonb, whose
|
|
1182
|
+
# documents the other JSON operations here read.
|
|
1183
|
+
class JsonBuild < Node
|
|
1184
|
+
include Predications
|
|
1185
|
+
include JsonComparable
|
|
1186
|
+
include ComputedJson
|
|
1187
|
+
|
|
1188
|
+
attr_reader :kind, :values
|
|
1189
|
+
|
|
1190
|
+
def initialize(kind, values)
|
|
1191
|
+
@kind = kind
|
|
1192
|
+
@values = kind == :object ? check_pairs(values) : values
|
|
1193
|
+
end
|
|
1194
|
+
|
|
1195
|
+
def to_arel(table, model)
|
|
1196
|
+
dialect = Dialect.for(model)
|
|
1197
|
+
if kind == :array
|
|
1198
|
+
dialect.json_build(:array, nil,
|
|
1199
|
+
values.map { |value| build_argument(value, dialect, table, model) }, model)
|
|
839
1200
|
else
|
|
840
|
-
|
|
1201
|
+
dialect.json_build(:object, values.keys.map(&:to_s),
|
|
1202
|
+
values.values.map { |value| build_argument(value, dialect, table, model) }, model)
|
|
841
1203
|
end
|
|
842
1204
|
end
|
|
1205
|
+
|
|
1206
|
+
private
|
|
1207
|
+
# An expression is itself and a bare scalar is quoted; a document or
|
|
1208
|
+
# a boolean the dialect embeds as JSON, as bury takes it.
|
|
1209
|
+
def build_argument(value, dialect, table, model)
|
|
1210
|
+
case value
|
|
1211
|
+
when Node, ::Symbol then to_arel_operand(value, table, model)
|
|
1212
|
+
when ::Hash, ::Array, true, false then dialect.json_build_argument(value, model)
|
|
1213
|
+
when ::Rational then quote_number(value)
|
|
1214
|
+
else Arel::Nodes.build_quoted(value)
|
|
1215
|
+
end
|
|
1216
|
+
end
|
|
1217
|
+
|
|
1218
|
+
# The keys come from Ruby as Hash keys rather than alternating with
|
|
1219
|
+
# the values as SQL has them, which is what keeps a bare symbol
|
|
1220
|
+
# free to mean a column on the value side. Anything but a name is
|
|
1221
|
+
# refused here, before the adapters answer a NULL key three ways.
|
|
1222
|
+
def check_pairs(pairs)
|
|
1223
|
+
unless pairs.is_a?(::Hash)
|
|
1224
|
+
raise ArgumentError,
|
|
1225
|
+
"json_object takes a hash of keys to values, not #{pairs.inspect}"
|
|
1226
|
+
end
|
|
1227
|
+
pairs.each_key do |key|
|
|
1228
|
+
next if key.is_a?(::String) || key.is_a?(::Symbol)
|
|
1229
|
+
raise ArgumentError,
|
|
1230
|
+
"a key of json_object is a string or a symbol, not #{key.inspect}"
|
|
1231
|
+
end
|
|
1232
|
+
pairs
|
|
1233
|
+
end
|
|
1234
|
+
|
|
1235
|
+
def json_source
|
|
1236
|
+
"json_#{kind}"
|
|
1237
|
+
end
|
|
843
1238
|
end
|
|
844
1239
|
|
|
845
1240
|
# GROUP BY GROUPING SETS / ROLLUP / CUBE: several groupings asked for at
|
|
846
1241
|
# once, the totals of each coming back beside the rows. PostgreSQL has
|
|
847
|
-
# all three; the block raises for the
|
|
1242
|
+
# all three and the MySQL family rollup alone; the block raises for the
|
|
1243
|
+
# rest before it gets this far.
|
|
848
1244
|
#
|
|
849
1245
|
# Each set is a list of its own, so grouping_sets takes lists and rollup
|
|
850
1246
|
# and cube take the columns themselves.
|
|
851
1247
|
class GroupingSets < Node
|
|
1248
|
+
# @private
|
|
852
1249
|
KINDS = {
|
|
853
1250
|
grouping_sets: Arel::Nodes::GroupingSet,
|
|
854
1251
|
rollup: Arel::Nodes::RollUp,
|
|
@@ -864,16 +1261,32 @@ module ActiveRecord
|
|
|
864
1261
|
end
|
|
865
1262
|
|
|
866
1263
|
def to_arel(table, model)
|
|
1264
|
+
return with_rollup(table, model) if Dialect.for(model).grouping_by_with_rollup?
|
|
1265
|
+
|
|
867
1266
|
KINDS.fetch(kind).new(
|
|
868
1267
|
if kind == :grouping_sets
|
|
869
1268
|
sets.map do |set|
|
|
870
1269
|
Arel::Nodes::GroupingElement.new(
|
|
871
|
-
Array(set).map {|column| to_arel_operand(column, table, model) })
|
|
1270
|
+
Array(set).map { |column| to_arel_operand(column, table, model) })
|
|
872
1271
|
end
|
|
873
1272
|
else
|
|
874
|
-
sets.map {|column| to_arel_operand(column, table, model) }
|
|
1273
|
+
sets.map { |column| to_arel_operand(column, table, model) }
|
|
875
1274
|
end)
|
|
876
1275
|
end
|
|
1276
|
+
|
|
1277
|
+
private
|
|
1278
|
+
# The MySQL family spells rollup WITH ROLLUP, trailing the whole
|
|
1279
|
+
# group list rather than wrapping a list of its own -- which is also
|
|
1280
|
+
# why a rollup cannot stand beside other group entries there. The
|
|
1281
|
+
# columns are compiled by the connection's own visitor, so their
|
|
1282
|
+
# quoting is the adapter's.
|
|
1283
|
+
def with_rollup(table, model)
|
|
1284
|
+
columns = sets.map { |column| to_arel_operand(column, table, model) }
|
|
1285
|
+
sql = model.with_connection do |connection|
|
|
1286
|
+
columns.map { |column| connection.visitor.compile(column) }.join(", ")
|
|
1287
|
+
end
|
|
1288
|
+
Arel.sql("#{sql} WITH ROLLUP")
|
|
1289
|
+
end
|
|
877
1290
|
end
|
|
878
1291
|
|
|
879
1292
|
class Column < Node
|
|
@@ -895,10 +1308,25 @@ module ActiveRecord
|
|
|
895
1308
|
# Arithmetic on columns and expressions. Ruby's precedence puts these
|
|
896
1309
|
# above the comparison operators, so :price * :quantity > 100 groups the
|
|
897
1310
|
# way it reads.
|
|
1311
|
+
#
|
|
1312
|
+
# A Duration on the right moves a date: `:due_on + 3.days`. No two
|
|
1313
|
+
# families spell the move alike, so the dialect writes it, a part of the
|
|
1314
|
+
# duration at a time.
|
|
898
1315
|
class Arithmetic < Node
|
|
899
1316
|
include Predications
|
|
900
1317
|
include Arithmetics
|
|
901
1318
|
|
|
1319
|
+
# Active Support's parts, as the units the SQL takes. A week is seven
|
|
1320
|
+
# days: SQLite and Oracle have no week.
|
|
1321
|
+
# @private
|
|
1322
|
+
UNITS = {
|
|
1323
|
+
years: :year, months: :month, weeks: :day, days: :day,
|
|
1324
|
+
hours: :hour, minutes: :minute, seconds: :second,
|
|
1325
|
+
}.freeze
|
|
1326
|
+
|
|
1327
|
+
# @private
|
|
1328
|
+
DATE_UNITS = %i[year month day].freeze
|
|
1329
|
+
|
|
902
1330
|
attr_reader :left, :operator, :right
|
|
903
1331
|
|
|
904
1332
|
def initialize(left, operator, right)
|
|
@@ -908,9 +1336,54 @@ module ActiveRecord
|
|
|
908
1336
|
end
|
|
909
1337
|
|
|
910
1338
|
def to_arel(table, model)
|
|
911
|
-
to_arel_operand(left, table, model)
|
|
912
|
-
|
|
1339
|
+
arel_left = to_arel_operand(left, table, model)
|
|
1340
|
+
return move_date(arel_left, model) if right.is_a?(::ActiveSupport::Duration)
|
|
1341
|
+
# The operator dispatches Arel's Math, which a bare number carries
|
|
1342
|
+
# none of; quoted, it is a node with the same methods.
|
|
1343
|
+
arel_left = Arel::Nodes.build_quoted(arel_left) if arel_left.is_a?(::Numeric)
|
|
1344
|
+
arel_left.public_send(operator, to_arel_operand(right, table, model))
|
|
1345
|
+
end
|
|
1346
|
+
|
|
1347
|
+
# SQLite has no date type, and its datetime() gives whatever it is
|
|
1348
|
+
# handed a time of day, so a date column moved by a day would come
|
|
1349
|
+
# back a midnight and sort past the same day written bare. Its
|
|
1350
|
+
# dialect has date() for what is a date to begin with, and this is
|
|
1351
|
+
# what says so: a column the model declares a date, CURRENT_DATE, or
|
|
1352
|
+
# one of those already moved by a date's units. The other families
|
|
1353
|
+
# keep the type themselves and never ask.
|
|
1354
|
+
def self.date_operand?(operand, model)
|
|
1355
|
+
case operand
|
|
1356
|
+
when ::Symbol then model.type_for_attribute(operand).type == :date
|
|
1357
|
+
when DatetimeValueFunction then operand.name == "CURRENT_DATE"
|
|
1358
|
+
when Arithmetic
|
|
1359
|
+
operand.right.is_a?(::ActiveSupport::Duration) &&
|
|
1360
|
+
date_operand?(operand.left, model) &&
|
|
1361
|
+
operand.right.parts.keys.all? { |part| DATE_UNITS.include?(UNITS[part]) }
|
|
1362
|
+
else false
|
|
1363
|
+
end
|
|
913
1364
|
end
|
|
1365
|
+
|
|
1366
|
+
private
|
|
1367
|
+
# Each amount is written into the SQL as a number, and a fraction
|
|
1368
|
+
# of a unit is not one every family takes, so it has to be a whole
|
|
1369
|
+
# one.
|
|
1370
|
+
def move_date(date, model)
|
|
1371
|
+
unless operator == :+ || operator == :-
|
|
1372
|
+
raise ArgumentError,
|
|
1373
|
+
"a duration is added to a date or subtracted from it, not #{operator}"
|
|
1374
|
+
end
|
|
1375
|
+
dialect = Dialect.for(model)
|
|
1376
|
+
date_only = Arithmetic.date_operand?(left, model)
|
|
1377
|
+
right.parts.reduce(date) do |arel, (part, amount)|
|
|
1378
|
+
unless amount.is_a?(::Integer)
|
|
1379
|
+
raise ArgumentError, "#{amount.inspect} #{part} is not a whole number of them"
|
|
1380
|
+
end
|
|
1381
|
+
unit = UNITS.fetch(part)
|
|
1382
|
+
amount *= 7 if part == :weeks
|
|
1383
|
+
dialect.add_interval(arel, amount, unit, operator == :-,
|
|
1384
|
+
date_only && DATE_UNITS.include?(unit))
|
|
1385
|
+
end
|
|
1386
|
+
end
|
|
914
1387
|
end
|
|
915
1388
|
|
|
916
1389
|
# What the bitwise operators refuse. Both refusals are there because
|
|
@@ -920,23 +1393,22 @@ module ActiveRecord
|
|
|
920
1393
|
# PostgreSQL has no such operator and would say so.
|
|
921
1394
|
module BitwiseOperands
|
|
922
1395
|
private
|
|
1396
|
+
def check_operand(operand, operator)
|
|
1397
|
+
return operand unless operand.is_a?(Predicate)
|
|
1398
|
+
raise ArgumentError,
|
|
1399
|
+
"a condition cannot be an operand of #{operator}; " \
|
|
1400
|
+
"& and | between conditions are AND and OR"
|
|
1401
|
+
end
|
|
923
1402
|
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
def check_not_boolean(operand, operator, model)
|
|
934
|
-
return unless operand.is_a?(::Symbol)
|
|
935
|
-
return unless model.type_for_attribute(operand).type == :boolean
|
|
936
|
-
raise ArgumentError,
|
|
937
|
-
"#{operand.inspect} is a boolean column, which #{operator} does " \
|
|
938
|
-
"not take; #{operand.inspect}.true? is the condition"
|
|
939
|
-
end
|
|
1403
|
+
# Only the unqualified column can be checked, since that is the one
|
|
1404
|
+
# the model is known to have.
|
|
1405
|
+
def check_not_boolean(operand, operator, model)
|
|
1406
|
+
return unless operand.is_a?(::Symbol)
|
|
1407
|
+
return unless model.type_for_attribute(operand).type == :boolean
|
|
1408
|
+
raise ArgumentError,
|
|
1409
|
+
"#{operand.inspect} is a boolean column, which #{operator} does " \
|
|
1410
|
+
"not take; #{operand.inspect}.true? is the condition"
|
|
1411
|
+
end
|
|
940
1412
|
end
|
|
941
1413
|
|
|
942
1414
|
# SQL's bitwise operators. Each parenthesises itself, which is what
|
|
@@ -947,6 +1419,7 @@ module ActiveRecord
|
|
|
947
1419
|
include Arithmetics
|
|
948
1420
|
include BitwiseOperands
|
|
949
1421
|
|
|
1422
|
+
# @private
|
|
950
1423
|
NODES = {
|
|
951
1424
|
:& => Arel::Nodes::BitwiseAnd,
|
|
952
1425
|
:| => Arel::Nodes::BitwiseOr,
|
|
@@ -976,22 +1449,14 @@ module ActiveRecord
|
|
|
976
1449
|
end
|
|
977
1450
|
|
|
978
1451
|
private
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
case AST.adapter_family(model)
|
|
987
|
-
when :postgresql then Arel::Nodes::InfixOperation.new('#', left, right)
|
|
988
|
-
when :mysql then Arel::Nodes::BitwiseXor.new(left, right)
|
|
989
|
-
else
|
|
990
|
-
Arel::Nodes::Subtraction.new(
|
|
991
|
-
Arel::Nodes::Grouping.new(Arel::Nodes::BitwiseOr.new(left, right)),
|
|
992
|
-
Arel::Nodes::Grouping.new(Arel::Nodes::BitwiseAnd.new(left, right)))
|
|
1452
|
+
# Arel has a node for XOR, but it writes ^ on every adapter, and ^ is
|
|
1453
|
+
# exponentiation to PostgreSQL -- a wrong answer rather than an error.
|
|
1454
|
+
# PostgreSQL's own spelling, #, is where a comment starts on MySQL, so
|
|
1455
|
+
# it cannot be the portable one either. SQLite has no XOR at all;
|
|
1456
|
+
# (a | b) - (a & b) is it, at the cost of naming each operand twice.
|
|
1457
|
+
def xor(left, right, model)
|
|
1458
|
+
Dialect.for(model).bitwise_xor(left, right)
|
|
993
1459
|
end
|
|
994
|
-
end
|
|
995
1460
|
end
|
|
996
1461
|
|
|
997
1462
|
# ~, which every adapter has. MySQL answers with the unsigned 64-bit
|
|
@@ -1018,6 +1483,12 @@ module ActiveRecord
|
|
|
1018
1483
|
# OVER, on the two things that can carry a window: an aggregate, and a
|
|
1019
1484
|
# function.
|
|
1020
1485
|
module Windowing
|
|
1486
|
+
# `OVER ()`, an empty window to be filled by {Over#partition},
|
|
1487
|
+
# {Over#order}, {Over#rows} and {Over#range}.
|
|
1488
|
+
# @return [AST::Over]
|
|
1489
|
+
# @example
|
|
1490
|
+
# Author.select { avg(:age).over.partition(:country).as(:country_average) }
|
|
1491
|
+
# Post.select { sum(:likes).over.order(:created_at).rows(..0).as(:running) }
|
|
1021
1492
|
def over
|
|
1022
1493
|
Over.new(self)
|
|
1023
1494
|
end
|
|
@@ -1039,30 +1510,44 @@ module ActiveRecord
|
|
|
1039
1510
|
@frame = frame
|
|
1040
1511
|
end
|
|
1041
1512
|
|
|
1513
|
+
# `PARTITION BY`, the columns or expressions given.
|
|
1514
|
+
# @return [AST::Over]
|
|
1042
1515
|
def partition(*exprs)
|
|
1043
1516
|
raise ArgumentError, "partition needs an expression" if exprs.empty?
|
|
1044
1517
|
Over.new(function, partitions + exprs, orders, frame)
|
|
1045
1518
|
end
|
|
1046
1519
|
|
|
1520
|
+
# `ORDER BY` within the window: columns, or orderings such as `:age.desc`.
|
|
1521
|
+
# @return [AST::Over]
|
|
1047
1522
|
def order(*exprs)
|
|
1048
1523
|
raise ArgumentError, "order needs an expression" if exprs.empty?
|
|
1049
1524
|
Over.new(function, partitions, orders + exprs, frame)
|
|
1050
1525
|
end
|
|
1051
1526
|
|
|
1527
|
+
# `ROWS BETWEEN`, as a range of rows counted from the current one: negative before it, positive after, `0` the row itself, an open end unbounded. `rows(..0)` is a running total, `rows(-1..1)` the row and its neighbours.
|
|
1528
|
+
# @param bounds [Range]
|
|
1529
|
+
# @return [AST::Over]
|
|
1052
1530
|
def rows(bounds)
|
|
1053
1531
|
Over.new(function, partitions, orders, framing(:rows, bounds))
|
|
1054
1532
|
end
|
|
1055
1533
|
|
|
1534
|
+
# `RANGE BETWEEN`, with the bounds as {#rows} takes them.
|
|
1535
|
+
# @param bounds [Range]
|
|
1536
|
+
# @return [AST::Over]
|
|
1056
1537
|
def range(bounds)
|
|
1057
1538
|
Over.new(function, partitions, orders, framing(:range, bounds))
|
|
1058
1539
|
end
|
|
1059
1540
|
|
|
1060
1541
|
def to_arel(table, model)
|
|
1061
1542
|
window = Arel::Nodes::Window.new
|
|
1062
|
-
partitions.each {|expr| window.partition(to_arel_operand(expr, table, model)) }
|
|
1063
|
-
orders.each {|expr| window.order(to_arel_operand(expr, table, model)) }
|
|
1543
|
+
partitions.each { |expr| window.partition(to_arel_operand(expr, table, model)) }
|
|
1544
|
+
orders.each { |expr| window.order(to_arel_operand(expr, table, model)) }
|
|
1064
1545
|
frame_arel(window) if frame
|
|
1065
1546
|
|
|
1547
|
+
# Not every aggregate can ride a window everywhere; the node itself
|
|
1548
|
+
# says where, once the adapter is known.
|
|
1549
|
+
function.check_window(model) if function.respond_to?(:check_window)
|
|
1550
|
+
|
|
1066
1551
|
# A window-only function refuses to build on its own; here is where
|
|
1067
1552
|
# it is asked for the call itself.
|
|
1068
1553
|
arel_function =
|
|
@@ -1072,43 +1557,42 @@ module ActiveRecord
|
|
|
1072
1557
|
end
|
|
1073
1558
|
|
|
1074
1559
|
private
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1560
|
+
# The frame is a range of rows counted from the current one: negative
|
|
1561
|
+
# before it, positive after, 0 the row itself, and an open end for
|
|
1562
|
+
# unbounded. `rows(..0)` is what a running total wants.
|
|
1563
|
+
def framing(kind, bounds)
|
|
1564
|
+
raise ArgumentError, "a window has one frame" if frame
|
|
1565
|
+
unless bounds.is_a?(::Range)
|
|
1566
|
+
raise ArgumentError, "#{kind} takes a range of rows, as in rows(..0)"
|
|
1567
|
+
end
|
|
1568
|
+
if bounds.exclude_end?
|
|
1569
|
+
raise ArgumentError, "a frame ends on a row rather than before one; use .."
|
|
1570
|
+
end
|
|
1571
|
+
[bounds.begin, bounds.end].each do |bound|
|
|
1572
|
+
next if bound.nil? || bound.is_a?(::Integer)
|
|
1573
|
+
raise ArgumentError,
|
|
1574
|
+
"a frame bound is a number of rows, or nothing for unbounded"
|
|
1575
|
+
end
|
|
1576
|
+
[kind, bounds.begin, bounds.end]
|
|
1091
1577
|
end
|
|
1092
|
-
[kind, bounds.begin, bounds.end]
|
|
1093
|
-
end
|
|
1094
1578
|
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1579
|
+
# Arel wants the keyword itself on the left of the BETWEEN, which is
|
|
1580
|
+
# what window.rows with no argument hands back.
|
|
1581
|
+
def frame_arel(window)
|
|
1582
|
+
kind, from, to = frame
|
|
1583
|
+
window.frame(
|
|
1584
|
+
Arel::Nodes::Between.new(
|
|
1585
|
+
window.public_send(kind),
|
|
1586
|
+
Arel::Nodes::And.new([bound(from, Arel::Nodes::Preceding.new),
|
|
1587
|
+
bound(to, Arel::Nodes::Following.new)])))
|
|
1588
|
+
end
|
|
1105
1589
|
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1590
|
+
def bound(rows, unbounded)
|
|
1591
|
+
return unbounded if rows.nil?
|
|
1592
|
+
return Arel::Nodes::CurrentRow.new if rows.zero?
|
|
1593
|
+
rows.negative? ? Arel::Nodes::Preceding.new(-rows)
|
|
1594
|
+
: Arel::Nodes::Following.new(rows)
|
|
1595
|
+
end
|
|
1112
1596
|
end
|
|
1113
1597
|
|
|
1114
1598
|
class Aggregate < Node
|
|
@@ -1116,11 +1600,17 @@ module ActiveRecord
|
|
|
1116
1600
|
include Arithmetics
|
|
1117
1601
|
include Windowing
|
|
1118
1602
|
|
|
1603
|
+
# The aggregates DISTINCT changes: over each value once, count counts
|
|
1604
|
+
# fewer and sum and avg reckon less. min and max give the same
|
|
1605
|
+
# either way, so a DISTINCT there is refused as saying nothing.
|
|
1606
|
+
# @private
|
|
1607
|
+
DISTINCT_FUNCTIONS = %i[count sum average].freeze
|
|
1608
|
+
|
|
1119
1609
|
attr_reader :operand, :function, :distinct, :condition
|
|
1120
1610
|
|
|
1121
1611
|
def initialize(operand, function, distinct: false, condition: nil)
|
|
1122
|
-
if distinct && function
|
|
1123
|
-
raise ArgumentError, "#{function} does not take distinct"
|
|
1612
|
+
if distinct && !DISTINCT_FUNCTIONS.include?(function)
|
|
1613
|
+
raise ArgumentError, "#{function} does not take distinct; it would give the same"
|
|
1124
1614
|
end
|
|
1125
1615
|
if distinct && operand == :*
|
|
1126
1616
|
raise ArgumentError, "count(:*) does not take distinct; name a column"
|
|
@@ -1131,8 +1621,12 @@ module ActiveRecord
|
|
|
1131
1621
|
@condition = condition
|
|
1132
1622
|
end
|
|
1133
1623
|
|
|
1134
|
-
# FILTER (WHERE
|
|
1135
|
-
# condition holds for
|
|
1624
|
+
# `FILTER (WHERE condition)`: the aggregate taken over the rows the
|
|
1625
|
+
# condition holds for, as a value or a block. Where there is no
|
|
1626
|
+
# FILTER clause -- MySQL, SQL Server -- the CASE that means the same.
|
|
1627
|
+
# @return [AST::Aggregate]
|
|
1628
|
+
# @example
|
|
1629
|
+
# Author.select { [count(:*).as(:all), count(:*).filter { :age < 50 }.as(:young)] }
|
|
1136
1630
|
def filter(condition = nil, &block)
|
|
1137
1631
|
Aggregate.new(operand, function, distinct: distinct,
|
|
1138
1632
|
condition: Case.argument(:filter, condition, block))
|
|
@@ -1141,11 +1635,11 @@ module ActiveRecord
|
|
|
1141
1635
|
def to_arel(table, model)
|
|
1142
1636
|
return aggregate(operand, table, model) unless condition
|
|
1143
1637
|
|
|
1144
|
-
#
|
|
1145
|
-
#
|
|
1146
|
-
# the
|
|
1147
|
-
# keep, and counts a 1 instead.
|
|
1148
|
-
|
|
1638
|
+
# A family without a FILTER clause gets the CASE that means the same.
|
|
1639
|
+
# An aggregate passes over a NULL, so the case that yields nothing for
|
|
1640
|
+
# the rows the condition misses is the same aggregate over the same
|
|
1641
|
+
# rows -- count(*) has no operand to keep, and counts a 1 instead.
|
|
1642
|
+
unless Dialect.for(model).filter_supported?
|
|
1149
1643
|
kept = Case.new.when(condition).then(operand == :* ? 1 : operand)
|
|
1150
1644
|
return aggregate(kept, table, model)
|
|
1151
1645
|
end
|
|
@@ -1154,15 +1648,136 @@ module ActiveRecord
|
|
|
1154
1648
|
end
|
|
1155
1649
|
|
|
1156
1650
|
private
|
|
1651
|
+
def aggregate(over, table, model)
|
|
1652
|
+
arel_operand = to_arel_operand(over, table, model)
|
|
1653
|
+
return arel_operand.count(distinct) if function == :count
|
|
1654
|
+
call = arel_operand.public_send(function)
|
|
1655
|
+
call.distinct = distinct
|
|
1656
|
+
call
|
|
1657
|
+
end
|
|
1658
|
+
end
|
|
1157
1659
|
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1660
|
+
# Rows gathered into one JSON document: json_arrayagg collects a value
|
|
1661
|
+
# from each row into an array, json_objectagg a key and a value into an
|
|
1662
|
+
# object. Every adapter has the pair under a name of its own; what
|
|
1663
|
+
# PostgreSQL gets is the jsonb one, whose documents the other JSON
|
|
1664
|
+
# operations here read.
|
|
1665
|
+
class JsonAggregate < Node
|
|
1666
|
+
include Predications
|
|
1667
|
+
include JsonComparable
|
|
1668
|
+
include ComputedJson
|
|
1669
|
+
include Windowing
|
|
1670
|
+
|
|
1671
|
+
attr_reader :kind, :operands, :condition
|
|
1672
|
+
|
|
1673
|
+
def initialize(kind, operands, condition: nil)
|
|
1674
|
+
@kind = kind
|
|
1675
|
+
@operands = operands
|
|
1676
|
+
@condition = condition
|
|
1677
|
+
end
|
|
1678
|
+
|
|
1679
|
+
# `FILTER (WHERE condition)`, as {Aggregate#filter}; refused on the
|
|
1680
|
+
# MySQL family, where the CASE that stands in would leave a JSON null
|
|
1681
|
+
# for every row it drops.
|
|
1682
|
+
# @return [AST::JsonAggregate]
|
|
1683
|
+
def filter(condition = nil, &block)
|
|
1684
|
+
JsonAggregate.new(kind, operands,
|
|
1685
|
+
condition: Case.argument(:filter, condition, block))
|
|
1686
|
+
end
|
|
1687
|
+
|
|
1688
|
+
# Over asks here before writing a window, since a family may take
|
|
1689
|
+
# every other aggregate as one but not these two.
|
|
1690
|
+
def check_window(model)
|
|
1691
|
+
Dialect.for(model).check_json_aggregate_window(json_source, model)
|
|
1692
|
+
end
|
|
1693
|
+
|
|
1694
|
+
def to_arel(table, model)
|
|
1695
|
+
dialect = Dialect.for(model)
|
|
1696
|
+
call = Arel::Nodes::NamedFunction.new(
|
|
1697
|
+
dialect.json_aggregate_name(kind),
|
|
1698
|
+
operands.map { |operand| to_arel_argument(operand, table, model) })
|
|
1699
|
+
return call unless condition
|
|
1700
|
+
|
|
1701
|
+
# The CASE that stands in for FILTER elsewhere hands the aggregate a
|
|
1702
|
+
# NULL for every row the condition misses, and these two keep a NULL
|
|
1703
|
+
# -- as JSON null -- rather than passing over it, so it is refused.
|
|
1704
|
+
unless dialect.json_aggregate_filter_supported?
|
|
1705
|
+
raise NotImplementedError,
|
|
1706
|
+
"#{json_source}.filter has no equivalent on " \
|
|
1707
|
+
"#{model.connection_db_config.adapter}; a CASE would leave a " \
|
|
1708
|
+
"null in the document for every row it drops"
|
|
1709
|
+
end
|
|
1710
|
+
call.filter(condition.to_arel(table, model))
|
|
1711
|
+
end
|
|
1712
|
+
|
|
1713
|
+
private
|
|
1714
|
+
def json_source
|
|
1715
|
+
"json_#{kind}"
|
|
1716
|
+
end
|
|
1717
|
+
end
|
|
1718
|
+
|
|
1719
|
+
# The strings of a group joined into one, a separator between:
|
|
1720
|
+
# `string_agg(:title, ", ")`, with `.order` for the order they are
|
|
1721
|
+
# joined in. Every family has it under a name of its own with the
|
|
1722
|
+
# ORDER BY in a place of its own, and Arel has no node for an ORDER BY
|
|
1723
|
+
# inside a call, so the dialect writes the call. A NULL is passed
|
|
1724
|
+
# over as by any aggregate, so the CASE that stands in for FILTER
|
|
1725
|
+
# means the same here and is not refused as the JSON aggregates' is.
|
|
1726
|
+
class StringAggregate < Node
|
|
1727
|
+
include Predications
|
|
1728
|
+
include Windowing
|
|
1729
|
+
|
|
1730
|
+
attr_reader :operand, :separator, :orders, :condition
|
|
1731
|
+
|
|
1732
|
+
def initialize(operand, separator, orders: [], condition: nil)
|
|
1733
|
+
unless separator.is_a?(::String)
|
|
1734
|
+
raise ArgumentError, "#{separator.inspect} is not a String separator"
|
|
1164
1735
|
end
|
|
1736
|
+
@operand = operand
|
|
1737
|
+
@separator = separator
|
|
1738
|
+
@orders = orders
|
|
1739
|
+
@condition = condition
|
|
1740
|
+
end
|
|
1741
|
+
|
|
1742
|
+
# The order the strings are joined in: columns, or orderings such as
|
|
1743
|
+
# `:title.desc`.
|
|
1744
|
+
# @return [AST::StringAggregate]
|
|
1745
|
+
def order(*exprs)
|
|
1746
|
+
raise ArgumentError, "order needs an expression" if exprs.empty?
|
|
1747
|
+
StringAggregate.new(operand, separator, orders: orders + exprs, condition: condition)
|
|
1748
|
+
end
|
|
1749
|
+
|
|
1750
|
+
# `FILTER (WHERE condition)`, as {Aggregate#filter}.
|
|
1751
|
+
# @return [AST::StringAggregate]
|
|
1752
|
+
def filter(condition = nil, &block)
|
|
1753
|
+
StringAggregate.new(operand, separator, orders: orders,
|
|
1754
|
+
condition: Case.argument(:filter, condition, block))
|
|
1755
|
+
end
|
|
1756
|
+
|
|
1757
|
+
def check_window(model)
|
|
1758
|
+
Dialect.for(model).check_string_aggregate_window(model)
|
|
1759
|
+
end
|
|
1760
|
+
|
|
1761
|
+
def to_arel(table, model)
|
|
1762
|
+
dialect = Dialect.for(model)
|
|
1763
|
+
kept = condition && !dialect.filter_supported? ?
|
|
1764
|
+
Case.new.when(condition).then(operand) : operand
|
|
1765
|
+
call = dialect.string_agg(
|
|
1766
|
+
to_arel_argument(kept, table, model), separator,
|
|
1767
|
+
orders.map { |expr| to_arel_operand(expr, table, model) },
|
|
1768
|
+
string_operand?(model), model)
|
|
1769
|
+
return call unless condition && dialect.filter_supported?
|
|
1770
|
+
Arel::Nodes::Filter.new(call, condition.to_arel(table, model))
|
|
1165
1771
|
end
|
|
1772
|
+
|
|
1773
|
+
private
|
|
1774
|
+
# Whether the operand is a column the model declares a string.
|
|
1775
|
+
# PostgreSQL asks, its STRING_AGG taking text and nothing else; the
|
|
1776
|
+
# others convert for themselves.
|
|
1777
|
+
def string_operand?(model)
|
|
1778
|
+
operand.is_a?(::Symbol) &&
|
|
1779
|
+
%i[string text].include?(model.type_for_attribute(operand).type)
|
|
1780
|
+
end
|
|
1166
1781
|
end
|
|
1167
1782
|
|
|
1168
1783
|
# A column alias, quoted by the adapter, so that the name asked for is
|
|
@@ -1191,11 +1806,33 @@ module ActiveRecord
|
|
|
1191
1806
|
end
|
|
1192
1807
|
|
|
1193
1808
|
private
|
|
1809
|
+
def alias_sql(model)
|
|
1810
|
+
name = alias_name.to_s
|
|
1811
|
+
return name unless quote
|
|
1812
|
+
model.with_connection { |connection| connection.quote_column_name(name) }
|
|
1813
|
+
end
|
|
1814
|
+
end
|
|
1194
1815
|
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1816
|
+
# A collation named for a comparison or an ordering: `:name.collate(:ci)`.
|
|
1817
|
+
# It stands as an expression -- compared, ordered by, selected -- and
|
|
1818
|
+
# gives back one of its own, so the collation carries through.
|
|
1819
|
+
#
|
|
1820
|
+
# The name follows COLLATE as a bare identifier with no Arel node of its
|
|
1821
|
+
# own. What names are safe turns on whether the family quotes it -- only
|
|
1822
|
+
# PostgreSQL does -- so the dialect checks the name as it builds the
|
|
1823
|
+
# clause, rather than this node holding one rule for all of them.
|
|
1824
|
+
class Collate < Node
|
|
1825
|
+
include Predications
|
|
1826
|
+
|
|
1827
|
+
attr_reader :operand, :name
|
|
1828
|
+
|
|
1829
|
+
def initialize(operand, name)
|
|
1830
|
+
@name = name.to_s
|
|
1831
|
+
@operand = operand
|
|
1832
|
+
end
|
|
1833
|
+
|
|
1834
|
+
def to_arel(table, model)
|
|
1835
|
+
Dialect.for(model).collate(to_arel_operand(operand, table, model), name, model)
|
|
1199
1836
|
end
|
|
1200
1837
|
end
|
|
1201
1838
|
|
|
@@ -1208,12 +1845,18 @@ module ActiveRecord
|
|
|
1208
1845
|
@nulls = nulls
|
|
1209
1846
|
end
|
|
1210
1847
|
|
|
1848
|
+
# `NULLS FIRST`; portable, since Arel emulates it where MySQL has
|
|
1849
|
+
# none.
|
|
1850
|
+
# @return [AST::Ordering]
|
|
1851
|
+
#
|
|
1211
1852
|
# MySQL has no NULLS FIRST/LAST, but Arel emulates it there with a
|
|
1212
1853
|
# leading IS NULL ordering, so these are portable.
|
|
1213
1854
|
def nulls_first
|
|
1214
1855
|
Ordering.new(operand, direction, :nulls_first)
|
|
1215
1856
|
end
|
|
1216
1857
|
|
|
1858
|
+
# `NULLS LAST`.
|
|
1859
|
+
# @return [AST::Ordering]
|
|
1217
1860
|
def nulls_last
|
|
1218
1861
|
Ordering.new(operand, direction, :nulls_last)
|
|
1219
1862
|
end
|
|
@@ -1237,11 +1880,54 @@ module ActiveRecord
|
|
|
1237
1880
|
end
|
|
1238
1881
|
|
|
1239
1882
|
def to_arel(table, model)
|
|
1240
|
-
arel_args = args.map {|arg| to_arel_argument(arg, table, model) }
|
|
1883
|
+
arel_args = args.map { |arg| to_arel_argument(arg, table, model) }
|
|
1241
1884
|
Arel::Nodes::NamedFunction.new(name, arel_args)
|
|
1242
1885
|
end
|
|
1243
1886
|
end
|
|
1244
1887
|
|
|
1888
|
+
# Escape hatch for operators without a spelling of their own, the way
|
|
1889
|
+
# fn is for functions. The operator is emitted as written -- whether
|
|
1890
|
+
# the adapter has it is the caller's assertion, as fn's names are --
|
|
1891
|
+
# and the values ride as quoted literals, so on PostgreSQL an untyped
|
|
1892
|
+
# one takes the type of the operand beside it.
|
|
1893
|
+
class Operation < Node
|
|
1894
|
+
include Predications
|
|
1895
|
+
include Arithmetics
|
|
1896
|
+
|
|
1897
|
+
attr_reader :operator, :left, :right
|
|
1898
|
+
|
|
1899
|
+
def initialize(operator, left, right)
|
|
1900
|
+
@operator = AST.check_name(operator, OPERATOR, "operator").to_s
|
|
1901
|
+
@left = check_side(left)
|
|
1902
|
+
@right = check_side(right)
|
|
1903
|
+
end
|
|
1904
|
+
|
|
1905
|
+
def to_arel(table, model)
|
|
1906
|
+
Arel::Nodes::Grouping.new(
|
|
1907
|
+
Arel::Nodes::InfixOperation.new(
|
|
1908
|
+
operator, side(left, table, model), side(right, table, model)))
|
|
1909
|
+
end
|
|
1910
|
+
|
|
1911
|
+
private
|
|
1912
|
+
# An expression operand is parenthesized: an unknown operator's
|
|
1913
|
+
# precedence is unknown too, and PostgreSQL reads its named
|
|
1914
|
+
# operators from the left, so a bare infix on the right would take
|
|
1915
|
+
# the new operator's left side into its own.
|
|
1916
|
+
def side(operand, table, model)
|
|
1917
|
+
arel = to_arel_argument(operand, table, model)
|
|
1918
|
+
operand.is_a?(Node) ? Arel::Nodes::Grouping.new(arel) : arel
|
|
1919
|
+
end
|
|
1920
|
+
|
|
1921
|
+
def check_side(operand)
|
|
1922
|
+
if operand.is_a?(::Hash) || operand.is_a?(::Array) || operand.is_a?(::Set)
|
|
1923
|
+
raise ArgumentError,
|
|
1924
|
+
"#{operand.inspect} has no one SQL spelling; a string says it " \
|
|
1925
|
+
"in the adapter's own, to_json for a document"
|
|
1926
|
+
end
|
|
1927
|
+
operand
|
|
1928
|
+
end
|
|
1929
|
+
end
|
|
1930
|
+
|
|
1245
1931
|
# ROW_NUMBER and its kind: functions that say nothing without a window.
|
|
1246
1932
|
# On its own this refuses rather than reaching the database as an error
|
|
1247
1933
|
# there; over asks it for call_arel instead.
|
|
@@ -1326,6 +2012,7 @@ module ActiveRecord
|
|
|
1326
2012
|
# or an Array compares against a PostgreSQL range or array column, the way
|
|
1327
2013
|
# Active Record's own force_equality? types do.
|
|
1328
2014
|
class Comparison < Predicate
|
|
2015
|
+
# @private
|
|
1329
2016
|
OPERATOR_MAP = {
|
|
1330
2017
|
:== => :eq, :!= => :not_eq,
|
|
1331
2018
|
:> => :gt, :>= => :gteq, :< => :lt, :<= => :lteq
|
|
@@ -1344,24 +2031,24 @@ module ActiveRecord
|
|
|
1344
2031
|
arel_value =
|
|
1345
2032
|
case value
|
|
1346
2033
|
when Node then value.to_arel(table, model)
|
|
2034
|
+
when ::Symbol then column_operand(value, table, model)
|
|
1347
2035
|
when ActiveRecord::Relation then scalar_subquery(value)
|
|
1348
|
-
else value
|
|
2036
|
+
else quote_number(value)
|
|
1349
2037
|
end
|
|
1350
2038
|
arel_column.public_send(OPERATOR_MAP.fetch(operator), arel_value)
|
|
1351
2039
|
end
|
|
1352
2040
|
|
|
1353
2041
|
private
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
2042
|
+
# A relation compared against a column has to yield a single value, so
|
|
2043
|
+
# unlike In there is no sensible default select list to fall back on.
|
|
2044
|
+
def scalar_subquery(relation)
|
|
2045
|
+
if relation.select_values.empty?
|
|
2046
|
+
raise ArgumentError,
|
|
2047
|
+
"#{operator} needs a subquery selecting one value; add a select"
|
|
2048
|
+
end
|
|
2049
|
+
relation = relation.send(:apply_join_dependency) if relation.eager_loading?
|
|
2050
|
+
relation.arel
|
|
1361
2051
|
end
|
|
1362
|
-
relation = relation.send(:apply_join_dependency) if relation.eager_loading?
|
|
1363
|
-
relation.arel
|
|
1364
|
-
end
|
|
1365
2052
|
end
|
|
1366
2053
|
|
|
1367
2054
|
# IS TRUE, IS FALSE and their negations, which every adapter spells the
|
|
@@ -1376,9 +2063,8 @@ module ActiveRecord
|
|
|
1376
2063
|
end
|
|
1377
2064
|
|
|
1378
2065
|
def to_arel(table, model)
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
to_arel_operand(operand, table, model), literal)
|
|
2066
|
+
Dialect.for(model).truth_value(
|
|
2067
|
+
to_arel_operand(operand, table, model), value, negated, model)
|
|
1382
2068
|
end
|
|
1383
2069
|
end
|
|
1384
2070
|
|
|
@@ -1388,19 +2074,18 @@ module ActiveRecord
|
|
|
1388
2074
|
# selects the model's primary key.
|
|
1389
2075
|
module SetSubquery
|
|
1390
2076
|
private
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
2077
|
+
def set_subquery(relation, spelling)
|
|
2078
|
+
relation = relation.send(:apply_join_dependency) if relation.eager_loading?
|
|
2079
|
+
if relation.select_values.empty?
|
|
2080
|
+
model = relation.model
|
|
2081
|
+
if model.composite_primary_key?
|
|
2082
|
+
raise ArgumentError,
|
|
2083
|
+
"Cannot map composite primary key #{model.primary_key} to #{spelling}"
|
|
2084
|
+
end
|
|
2085
|
+
relation = relation.select(relation.table[model.primary_key])
|
|
1399
2086
|
end
|
|
1400
|
-
relation
|
|
2087
|
+
relation.arel
|
|
1401
2088
|
end
|
|
1402
|
-
relation.arel
|
|
1403
|
-
end
|
|
1404
2089
|
end
|
|
1405
2090
|
|
|
1406
2091
|
# IN for a list of values, BETWEEN for a range, IN (SELECT ...) for a
|
|
@@ -1408,6 +2093,12 @@ module ActiveRecord
|
|
|
1408
2093
|
class In < Predicate
|
|
1409
2094
|
include SetSubquery
|
|
1410
2095
|
|
|
2096
|
+
# Range holds its endpoints to Comparable, which a quoted node is
|
|
2097
|
+
# not, so this quacks the three methods Arel's between reads.
|
|
2098
|
+
QuotedRange = Struct.new(:begin, :end, :exclude_end) do
|
|
2099
|
+
def exclude_end? = exclude_end
|
|
2100
|
+
end
|
|
2101
|
+
|
|
1411
2102
|
attr_reader :operand, :values, :negated
|
|
1412
2103
|
|
|
1413
2104
|
def initialize(operand, values, negated: false)
|
|
@@ -1418,13 +2109,67 @@ module ActiveRecord
|
|
|
1418
2109
|
|
|
1419
2110
|
def to_arel(table, model)
|
|
1420
2111
|
arel_operand = to_arel_operand(operand, table, model)
|
|
1421
|
-
|
|
1422
|
-
|
|
2112
|
+
case values
|
|
2113
|
+
when Range, QuotedRange
|
|
2114
|
+
lower = quote_value(values.begin, table, model)
|
|
2115
|
+
upper = quote_value(values.end, table, model)
|
|
2116
|
+
if json_between?(model) && lower && upper && !negated
|
|
2117
|
+
return arel_operand.gteq(lower).and(
|
|
2118
|
+
values.exclude_end? ? arel_operand.lt(upper) : arel_operand.lteq(upper))
|
|
2119
|
+
end
|
|
2120
|
+
range = QuotedRange.new(lower, upper, values.exclude_end?)
|
|
2121
|
+
arel_operand.public_send(negated ? :not_between : :between, range)
|
|
2122
|
+
when ActiveRecord::Relation
|
|
2123
|
+
arel_operand.public_send(negated ? :not_in : :in, set_subquery(values, "IN"))
|
|
1423
2124
|
else
|
|
1424
|
-
arg = values
|
|
2125
|
+
arg = values
|
|
2126
|
+
if arg.is_a?(::Array)
|
|
2127
|
+
arg = arg.map { |value| quote_value(value, table, model) }
|
|
2128
|
+
return json_list(arel_operand, arg) if json_list?(model)
|
|
2129
|
+
end
|
|
1425
2130
|
arel_operand.public_send(negated ? :not_in : :in, arg)
|
|
1426
2131
|
end
|
|
1427
2132
|
end
|
|
2133
|
+
|
|
2134
|
+
private
|
|
2135
|
+
# An element that is already an expression resolves, a symbol is a
|
|
2136
|
+
# column here as everywhere, a number is quoted as itself, and the
|
|
2137
|
+
# rest ride for Arel to cast by the column.
|
|
2138
|
+
def quote_value(value, table, model)
|
|
2139
|
+
case value
|
|
2140
|
+
when Node then value.to_arel(table, model)
|
|
2141
|
+
when ::Symbol then column_operand(value, table, model)
|
|
2142
|
+
else quote_number(value)
|
|
2143
|
+
end
|
|
2144
|
+
end
|
|
2145
|
+
|
|
2146
|
+
# MySQL leaves IN and BETWEEN out of its JSON comparisons -- they
|
|
2147
|
+
# fall back to another comparison entirely -- so on it a JSON set is
|
|
2148
|
+
# spelled as the comparisons it means: the closed range as its two
|
|
2149
|
+
# bounds, the list as one equality per element. That names the dug
|
|
2150
|
+
# value once per element, the price SQLite's XOR pays per operand;
|
|
2151
|
+
# a negated range needs nothing, Arel writing it as two comparisons
|
|
2152
|
+
# everywhere. MariaDB never gets this far: the endpoints refuse as
|
|
2153
|
+
# they resolve.
|
|
2154
|
+
def json_between?(model)
|
|
2155
|
+
(values.begin.is_a?(JsonLiteral) || values.end.is_a?(JsonLiteral)) &&
|
|
2156
|
+
Dialect.for(model).json_list_by_element?
|
|
2157
|
+
end
|
|
2158
|
+
|
|
2159
|
+
def json_list?(model)
|
|
2160
|
+
values.any? { |value| value.is_a?(JsonLiteral) } &&
|
|
2161
|
+
Dialect.for(model).json_list_by_element?
|
|
2162
|
+
end
|
|
2163
|
+
|
|
2164
|
+
def json_list(arel_operand, elements)
|
|
2165
|
+
comparisons = elements.map do |element|
|
|
2166
|
+
negated ? arel_operand.not_eq(element) : arel_operand.eq(element)
|
|
2167
|
+
end
|
|
2168
|
+
joined = comparisons.inject do |so_far, piece|
|
|
2169
|
+
negated ? so_far.and(piece) : so_far.or(piece)
|
|
2170
|
+
end
|
|
2171
|
+
negated ? Arel::Nodes::Grouping.new(joined) : joined
|
|
2172
|
+
end
|
|
1428
2173
|
end
|
|
1429
2174
|
|
|
1430
2175
|
# ANY and ALL, which stand on the right of a comparison and say how many
|
|
@@ -1472,7 +2217,8 @@ module ActiveRecord
|
|
|
1472
2217
|
end
|
|
1473
2218
|
|
|
1474
2219
|
class Like < Predicate
|
|
1475
|
-
|
|
2220
|
+
# @private
|
|
2221
|
+
ESCAPE = "\\"
|
|
1476
2222
|
|
|
1477
2223
|
# Escapes % and _ so that they match literally. The pattern built from
|
|
1478
2224
|
# the result must be used with ESCAPE, since SQLite has no default
|
|
@@ -1484,8 +2230,8 @@ module ActiveRecord
|
|
|
1484
2230
|
# ORs one LIKE per pattern, for the shortcuts that accept several
|
|
1485
2231
|
# literals the way String#start_with? does.
|
|
1486
2232
|
def self.any(operand, patterns)
|
|
1487
|
-
patterns.map {|pattern| new(operand, pattern, ESCAPE) }.
|
|
1488
|
-
inject {|left, right| Or.new(left, right) }
|
|
2233
|
+
patterns.map { |pattern| new(operand, pattern, ESCAPE) }.
|
|
2234
|
+
inject { |left, right| Or.new(left, right) }
|
|
1489
2235
|
end
|
|
1490
2236
|
|
|
1491
2237
|
attr_reader :operand, :pattern, :escape, :case_sensitive, :negated
|
|
@@ -1569,21 +2315,20 @@ module ActiveRecord
|
|
|
1569
2315
|
end
|
|
1570
2316
|
|
|
1571
2317
|
private
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
2318
|
+
# PostgreSQL array input syntax: elements joined by commas inside
|
|
2319
|
+
# braces, and an element is double-quoted whenever it is empty, spells
|
|
2320
|
+
# NULL, or contains a character the parser treats specially.
|
|
2321
|
+
def array_literal
|
|
2322
|
+
encoded = elements.map do |value|
|
|
2323
|
+
s = value.to_s
|
|
2324
|
+
if s.empty? || s.casecmp?("null") || s.match?(/[\s{},"\\]/)
|
|
2325
|
+
"\"#{s.gsub(/["\\]/) { |c| "\\#{c}" }}\""
|
|
2326
|
+
else
|
|
2327
|
+
s
|
|
2328
|
+
end
|
|
1583
2329
|
end
|
|
2330
|
+
"{#{encoded.join(',')}}"
|
|
1584
2331
|
end
|
|
1585
|
-
"{#{encoded.join(',')}}"
|
|
1586
|
-
end
|
|
1587
2332
|
end
|
|
1588
2333
|
|
|
1589
2334
|
# Regular expression match: REGEXP on MySQL, ~ on PostgreSQL. SQLite has
|
|
@@ -1607,19 +2352,18 @@ module ActiveRecord
|
|
|
1607
2352
|
end
|
|
1608
2353
|
|
|
1609
2354
|
private
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
2355
|
+
# A Regexp literal reads naturally with =~, but only its source crosses
|
|
2356
|
+
# over; the database has its own dialect and no notion of Ruby's flags.
|
|
2357
|
+
# Dropping a flag would silently change what the query matches, so
|
|
2358
|
+
# anything beyond a plain literal is refused rather than ignored.
|
|
2359
|
+
def regexp_source(regexp)
|
|
2360
|
+
unless regexp.options.zero?
|
|
2361
|
+
raise ArgumentError,
|
|
2362
|
+
"#{regexp.inspect} has options that SQL cannot express; " \
|
|
2363
|
+
"pass the pattern as a string instead"
|
|
2364
|
+
end
|
|
2365
|
+
regexp.source
|
|
1620
2366
|
end
|
|
1621
|
-
regexp.source
|
|
1622
|
-
end
|
|
1623
2367
|
end
|
|
1624
2368
|
|
|
1625
2369
|
class And < Predicate
|