activerecord-refined 0.9.0 → 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 +93 -965
- data/activerecord-refined.gemspec +12 -7
- data/docs/conditions.md +206 -0
- data/docs/ctes.md +65 -0
- data/docs/expressions.md +125 -0
- data/docs/functions.md +219 -0
- data/docs/grouping.md +55 -0
- data/docs/joins.md +73 -0
- data/docs/json.md +230 -0
- data/docs/ordering.md +55 -0
- data/docs/time_zones.md +30 -0
- data/docs/windows.md +41 -0
- data/docs/writing.md +33 -0
- data/examples/aggregations.rb +18 -0
- data/examples/expressions.rb +35 -5
- data/lib/active_record/refined/ast.rb +461 -246
- data/lib/active_record/refined/dialect/mariadb.rb +25 -0
- data/lib/active_record/refined/dialect/mysql.rb +18 -0
- data/lib/active_record/refined/dialect/mysql_compat.rb +67 -0
- data/lib/active_record/refined/dialect/oracle.rb +110 -0
- data/lib/active_record/refined/dialect/postgresql.rb +120 -0
- data/lib/active_record/refined/dialect/sql_server.rb +115 -0
- data/lib/active_record/refined/dialect/sqlite.rb +57 -0
- data/lib/active_record/refined/dialect.rb +340 -0
- data/lib/active_record/refined.rb +682 -192
- data/lib/activerecord-refined/version.rb +1 -1
- data/lib/activerecord-refined.rb +1 -0
- metadata +58 -16
- data/.github/workflows/push_gem.yml +0 -45
- data/.github/workflows/sandbox.yml +0 -295
- data/.github/workflows/test.yml +0 -104
- data/.gitignore +0 -19
- data/.rubocop.yml +0 -393
- data/Gemfile +0 -14
- data/Rakefile +0 -53
- data/benchmark/query_building.rb +0 -129
- data/test/test_block_syntax.rb +0 -2999
- data/test/test_helper.rb +0 -238
|
@@ -6,37 +6,28 @@ require "json"
|
|
|
6
6
|
module ActiveRecord
|
|
7
7
|
module Refined
|
|
8
8
|
module AST
|
|
9
|
+
# @private
|
|
9
10
|
NAME = /[[:alpha:]_][[:alnum:]_$]*/
|
|
11
|
+
# @private
|
|
10
12
|
ALIAS_NAME = /\A#{NAME}\z/
|
|
13
|
+
# @private
|
|
11
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/
|
|
12
21
|
# A SQL type as cast writes it: words, at most parenthesized with
|
|
13
22
|
# lengths -- double precision, decimal(10,2).
|
|
23
|
+
# @private
|
|
14
24
|
TYPE_NAME = /\A[[:alpha:]_][[:alnum:]_ ]*(\(\d+(, ?\d+)?\))?\z/
|
|
15
25
|
# The characters PostgreSQL allows an operator to be made of, the
|
|
16
26
|
# widest operator alphabet of the three; op admits nothing else, so a
|
|
17
27
|
# letter, a space or a quote never reaches the SQL as an operator.
|
|
28
|
+
# @private
|
|
18
29
|
OPERATOR = %r{\A[+\-*/<>=~!@\#%^&|`?]+\z}
|
|
19
30
|
|
|
20
|
-
# Which family of spellings an adapter belongs to. MariaDB answers to
|
|
21
|
-
# the mysql2 adapter and is counted with MySQL, though the two part
|
|
22
|
-
# company over JSON. An adapter nobody has classified keeps the
|
|
23
|
-
# standard spellings and is left to say for itself what it cannot do.
|
|
24
|
-
#
|
|
25
|
-
# pglite is PostgreSQL itself compiled to WebAssembly, reached through
|
|
26
|
-
# wasmify-rails' adapter; the server it answers for is the same one.
|
|
27
|
-
ADAPTER_FAMILIES = {
|
|
28
|
-
"sqlite3" => :sqlite,
|
|
29
|
-
"postgresql" => :postgresql,
|
|
30
|
-
"postgis" => :postgresql,
|
|
31
|
-
"pglite" => :postgresql,
|
|
32
|
-
"mysql2" => :mysql,
|
|
33
|
-
"trilogy" => :mysql,
|
|
34
|
-
}.freeze
|
|
35
|
-
|
|
36
|
-
def self.adapter_family(model)
|
|
37
|
-
ADAPTER_FAMILIES[model.connection_db_config.adapter] || :unknown
|
|
38
|
-
end
|
|
39
|
-
|
|
40
31
|
def self.check_name(name, pattern, what)
|
|
41
32
|
return name if pattern.match?(name.to_s)
|
|
42
33
|
raise ArgumentError, "#{name.inspect} is not a plain #{what}"
|
|
@@ -48,19 +39,28 @@ module ActiveRecord
|
|
|
48
39
|
# functions; the MySQL family, which has no json(), reads it with
|
|
49
40
|
# JSON_EXTRACT.
|
|
50
41
|
def self.json_argument(value, model)
|
|
51
|
-
|
|
52
|
-
if adapter_family(model) == :sqlite
|
|
53
|
-
Arel::Nodes::NamedFunction.new("json", [json])
|
|
54
|
-
else
|
|
55
|
-
Arel::Nodes::NamedFunction.new(
|
|
56
|
-
"JSON_EXTRACT", [json, Arel::Nodes.build_quoted("$")])
|
|
57
|
-
end
|
|
42
|
+
Dialect.for(model).json_argument(value, model)
|
|
58
43
|
end
|
|
59
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
|
+
#
|
|
60
54
|
# Predicate builders shared by symbols, qualified columns and
|
|
61
55
|
# expressions. Imported into the Symbol refinement with
|
|
62
56
|
# Refinement#import_methods, so every method must be defined with def.
|
|
63
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
|
+
#
|
|
64
64
|
# == and != mean SQL = and <>, and = NULL is never true there, so nil
|
|
65
65
|
# is rejected rather than silently rewritten to IS NULL. null? builds
|
|
66
66
|
# its node directly and stays clear of this check.
|
|
@@ -71,6 +71,8 @@ module ActiveRecord
|
|
|
71
71
|
Comparison.new(self, :==, other)
|
|
72
72
|
end
|
|
73
73
|
|
|
74
|
+
# `!=`; `nil` is refused, as with `==`.
|
|
75
|
+
# @return [AST::Predicate]
|
|
74
76
|
def !=(other)
|
|
75
77
|
if other.nil?
|
|
76
78
|
raise ArgumentError, "!= does not take nil; use !null? instead"
|
|
@@ -78,30 +80,50 @@ module ActiveRecord
|
|
|
78
80
|
Comparison.new(self, :!=, other)
|
|
79
81
|
end
|
|
80
82
|
|
|
83
|
+
# `>`.
|
|
84
|
+
# @return [AST::Predicate]
|
|
81
85
|
def >(other)
|
|
82
86
|
Comparison.new(self, :>, other)
|
|
83
87
|
end
|
|
84
88
|
|
|
89
|
+
# `>=`.
|
|
90
|
+
# @return [AST::Predicate]
|
|
85
91
|
def >=(other)
|
|
86
92
|
Comparison.new(self, :>=, other)
|
|
87
93
|
end
|
|
88
94
|
|
|
95
|
+
# `<`.
|
|
96
|
+
# @return [AST::Predicate]
|
|
89
97
|
def <(other)
|
|
90
98
|
Comparison.new(self, :<, other)
|
|
91
99
|
end
|
|
92
100
|
|
|
101
|
+
# `<=`.
|
|
102
|
+
# @return [AST::Predicate]
|
|
93
103
|
def <=(other)
|
|
94
104
|
Comparison.new(self, :<=, other)
|
|
95
105
|
end
|
|
96
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/ }
|
|
97
112
|
def =~(pattern)
|
|
98
113
|
Match.new(self, pattern)
|
|
99
114
|
end
|
|
100
115
|
|
|
116
|
+
# The negated regular expression match.
|
|
117
|
+
# @return [AST::Predicate]
|
|
101
118
|
def !~(pattern)
|
|
102
119
|
Match.new(self, pattern, negated: true)
|
|
103
120
|
end
|
|
104
121
|
|
|
122
|
+
# `IS NULL`.
|
|
123
|
+
# @return [AST::Predicate]
|
|
124
|
+
# @example
|
|
125
|
+
# Author.where { :country.null? }
|
|
126
|
+
#
|
|
105
127
|
# `!` negates any predicate, so these are here for the four that SQL
|
|
106
128
|
# spells for itself: IS NOT NULL rather than NOT (... IS NULL), and
|
|
107
129
|
# likewise NOT IN and NOT LIKE. They mean the same thing either way,
|
|
@@ -110,10 +132,17 @@ module ActiveRecord
|
|
|
110
132
|
Comparison.new(self, :==, nil)
|
|
111
133
|
end
|
|
112
134
|
|
|
135
|
+
# `IS NOT NULL`.
|
|
136
|
+
# @return [AST::Predicate]
|
|
113
137
|
def not_null?
|
|
114
138
|
Comparison.new(self, :!=, nil)
|
|
115
139
|
end
|
|
116
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
|
+
#
|
|
117
146
|
# IS TRUE and IS FALSE differ from a comparison against the literal in
|
|
118
147
|
# what they make of NULL: `flag = TRUE` is itself NULL there, and a
|
|
119
148
|
# NULL predicate selects nothing, while these two answer false. So the
|
|
@@ -123,36 +152,62 @@ module ActiveRecord
|
|
|
123
152
|
TruthValue.new(self, true)
|
|
124
153
|
end
|
|
125
154
|
|
|
155
|
+
# `IS NOT TRUE`: keeps the NULL rows that `!(:flag == true)` drops.
|
|
156
|
+
# @return [AST::Predicate]
|
|
126
157
|
def not_true?
|
|
127
158
|
TruthValue.new(self, true, negated: true)
|
|
128
159
|
end
|
|
129
160
|
|
|
161
|
+
# `IS FALSE`.
|
|
162
|
+
# @return [AST::Predicate]
|
|
130
163
|
def false?
|
|
131
164
|
TruthValue.new(self, false)
|
|
132
165
|
end
|
|
133
166
|
|
|
167
|
+
# `IS NOT FALSE`.
|
|
168
|
+
# @return [AST::Predicate]
|
|
134
169
|
def not_false?
|
|
135
170
|
TruthValue.new(self, false, negated: true)
|
|
136
171
|
end
|
|
137
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)) }
|
|
138
179
|
def in?(values)
|
|
139
180
|
In.new(self, values)
|
|
140
181
|
end
|
|
141
182
|
|
|
183
|
+
# `NOT IN (...)`.
|
|
184
|
+
# @return [AST::Predicate]
|
|
142
185
|
def not_in?(values)
|
|
143
186
|
In.new(self, values, negated: true)
|
|
144
187
|
end
|
|
145
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
|
+
#
|
|
146
194
|
# Not min..max: an endpoint may be an expression, which Range would
|
|
147
195
|
# refuse to hold, since expressions do not compare among themselves.
|
|
148
196
|
def between?(min, max)
|
|
149
197
|
In.new(self, In::QuotedRange.new(min, max, false))
|
|
150
198
|
end
|
|
151
199
|
|
|
200
|
+
# `NOT BETWEEN min AND max`.
|
|
201
|
+
# @return [AST::Predicate]
|
|
152
202
|
def not_between?(min, max)
|
|
153
203
|
In.new(self, In::QuotedRange.new(min, max, false), negated: true)
|
|
154
204
|
end
|
|
155
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
|
+
#
|
|
156
211
|
# CASE with this as the operand, compared against each `when`:
|
|
157
212
|
# `:age.when(10).then(1).else(0)`. The other shape, where each `when`
|
|
158
213
|
# carries its own condition, starts at `case_when`.
|
|
@@ -160,22 +215,38 @@ module ActiveRecord
|
|
|
160
215
|
Case.new(self).when(value, &block)
|
|
161
216
|
end
|
|
162
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%") }
|
|
163
223
|
def like?(pattern)
|
|
164
224
|
Like.new(self, pattern)
|
|
165
225
|
end
|
|
166
226
|
|
|
227
|
+
# `NOT LIKE pattern`.
|
|
228
|
+
# @return [AST::Predicate]
|
|
167
229
|
def not_like?(pattern)
|
|
168
230
|
Like.new(self, pattern, negated: true)
|
|
169
231
|
end
|
|
170
232
|
|
|
233
|
+
# A case-insensitive `LIKE`: `ILIKE` on PostgreSQL, and `LIKE` over both sides lower-cased elsewhere.
|
|
234
|
+
# @return [AST::Predicate]
|
|
171
235
|
def ilike?(pattern)
|
|
172
236
|
Like.new(self, pattern, nil, case_sensitive: false)
|
|
173
237
|
end
|
|
174
238
|
|
|
239
|
+
# The negated case-insensitive `LIKE`.
|
|
240
|
+
# @return [AST::Predicate]
|
|
175
241
|
def not_ilike?(pattern)
|
|
176
242
|
Like.new(self, pattern, nil, case_sensitive: false, negated: true)
|
|
177
243
|
end
|
|
178
244
|
|
|
245
|
+
# Case-insensitive equality: `LOWER(column) = LOWER(value)`.
|
|
246
|
+
# @return [AST::Predicate]
|
|
247
|
+
# @example
|
|
248
|
+
# Author.where { :name.casecmp?("Alice") }
|
|
249
|
+
#
|
|
179
250
|
# Case-insensitive equality, folded on both sides rather than left to
|
|
180
251
|
# the collation, so it means the same thing on every adapter.
|
|
181
252
|
def casecmp?(value)
|
|
@@ -186,16 +257,27 @@ module ActiveRecord
|
|
|
186
257
|
Function.new("LOWER", [value]))
|
|
187
258
|
end
|
|
188
259
|
|
|
260
|
+
# `IS DISTINCT FROM`: `!=` that treats NULL as a value. `IS NOT` on SQLite, `NOT <=>` on MySQL.
|
|
261
|
+
# @return [AST::Predicate]
|
|
262
|
+
#
|
|
189
263
|
# Null-safe comparison: unlike = and <>, these treat NULL as a value,
|
|
190
264
|
# so not_distinct_from? is the one equality that may take nil.
|
|
191
265
|
def distinct_from?(value)
|
|
192
266
|
DistinctFrom.new(self, value, negated: true)
|
|
193
267
|
end
|
|
194
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) }
|
|
195
273
|
def not_distinct_from?(value)
|
|
196
274
|
DistinctFrom.new(self, value)
|
|
197
275
|
end
|
|
198
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") }
|
|
199
281
|
def start_with?(*prefixes)
|
|
200
282
|
if prefixes.empty?
|
|
201
283
|
raise ArgumentError, "start_with? needs at least one prefix"
|
|
@@ -203,6 +285,8 @@ module ActiveRecord
|
|
|
203
285
|
Like.any(self, prefixes.map { |prefix| "#{Like.escape(prefix)}%" })
|
|
204
286
|
end
|
|
205
287
|
|
|
288
|
+
# `LIKE '%suffix'`, escaped as {#start_with?} escapes.
|
|
289
|
+
# @return [AST::Predicate]
|
|
206
290
|
def end_with?(*suffixes)
|
|
207
291
|
if suffixes.empty?
|
|
208
292
|
raise ArgumentError, "end_with? needs at least one suffix"
|
|
@@ -210,10 +294,19 @@ module ActiveRecord
|
|
|
210
294
|
Like.any(self, suffixes.map { |suffix| "%#{Like.escape(suffix)}" })
|
|
211
295
|
end
|
|
212
296
|
|
|
297
|
+
# `LIKE '%substring%'`, escaped as {#start_with?} escapes.
|
|
298
|
+
# @return [AST::Predicate]
|
|
299
|
+
# @example
|
|
300
|
+
# Post.where { :title.include?("ruby") }
|
|
213
301
|
def include?(substring)
|
|
214
302
|
Like.new(self, "%#{Like.escape(substring)}%", Like::ESCAPE)
|
|
215
303
|
end
|
|
216
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
|
+
#
|
|
217
310
|
# The array comparisons carry the meaning of their Ruby namesakes.
|
|
218
311
|
# member? is Enumerable's element test, so an Array argument is
|
|
219
312
|
# rejected rather than quietly meaning something Array#member? does
|
|
@@ -226,18 +319,32 @@ module ActiveRecord
|
|
|
226
319
|
ArrayPredicate.new(self, :"@>", [element])
|
|
227
320
|
end
|
|
228
321
|
|
|
322
|
+
# Whether an array column holds every element given: `@>`.
|
|
323
|
+
# @return [AST::Predicate]
|
|
229
324
|
def superset?(elements)
|
|
230
325
|
ArrayPredicate.new(self, :"@>", ArrayPredicate.elements(elements, "superset?"))
|
|
231
326
|
end
|
|
232
327
|
|
|
328
|
+
# Whether every element of an array column is among those given: `<@`.
|
|
329
|
+
# @return [AST::Predicate]
|
|
233
330
|
def subset?(elements)
|
|
234
331
|
ArrayPredicate.new(self, :"<@", ArrayPredicate.elements(elements, "subset?"))
|
|
235
332
|
end
|
|
236
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]) }
|
|
237
338
|
def intersect?(elements)
|
|
238
339
|
ArrayPredicate.new(self, :"&&", ArrayPredicate.elements(elements, "intersect?"))
|
|
239
340
|
end
|
|
240
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
|
+
#
|
|
241
348
|
# Reading inside a JSON document, by the name of what Hash does. A
|
|
242
349
|
# string or symbol steps into an object, an integer into an array, and
|
|
243
350
|
# what comes back is still JSON, the way Hash#dig hands back the
|
|
@@ -248,10 +355,19 @@ module ActiveRecord
|
|
|
248
355
|
JsonPath.new(self, path)
|
|
249
356
|
end
|
|
250
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" }
|
|
251
362
|
def dig_text(*path)
|
|
252
363
|
JsonPath.new(self, path, json_value: false)
|
|
253
364
|
end
|
|
254
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
|
+
#
|
|
255
371
|
# Keys taken out of a JSON document, by the name of what Hash does,
|
|
256
372
|
# and taking keys as Hash#except takes them. Like bury it gives back
|
|
257
373
|
# the document changed rather than writing it anywhere.
|
|
@@ -259,6 +375,11 @@ module ActiveRecord
|
|
|
259
375
|
JsonExcept.new(self, keys)
|
|
260
376
|
end
|
|
261
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
|
+
#
|
|
262
383
|
# What dig reads, bury sets: the last argument is the value and the
|
|
263
384
|
# rest are the path to it. The document comes back changed rather
|
|
264
385
|
# than being written anywhere, which update_all is for.
|
|
@@ -266,12 +387,22 @@ module ActiveRecord
|
|
|
266
387
|
JsonSet.new(self, path, value)
|
|
267
388
|
end
|
|
268
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
|
+
#
|
|
269
395
|
# Whether the document holds what is given, which SQL calls
|
|
270
396
|
# containment. SQLite has no equivalent.
|
|
271
397
|
def contains?(value)
|
|
272
398
|
JsonContains.new(self, value)
|
|
273
399
|
end
|
|
274
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
|
+
#
|
|
275
406
|
# Whether the key is there at all, as Hash#key? asks. Hash has
|
|
276
407
|
# has_key? too; one name is enough, and this is the one Ruby's own
|
|
277
408
|
# style prefers.
|
|
@@ -279,32 +410,55 @@ module ActiveRecord
|
|
|
279
410
|
JsonHasKey.new(self, key)
|
|
280
411
|
end
|
|
281
412
|
|
|
413
|
+
# The keys of the object as a JSON array, as Hash#keys gives them. Oracle has none.
|
|
414
|
+
# @return [AST::JsonKeys]
|
|
415
|
+
#
|
|
282
416
|
# The keys of the document, as Hash#keys gives them: a JSON array.
|
|
283
417
|
def keys
|
|
284
418
|
JsonKeys.new(self)
|
|
285
419
|
end
|
|
286
420
|
end
|
|
287
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
|
+
#
|
|
288
431
|
# Arithmetic builders shared by symbols, qualified columns and
|
|
289
432
|
# expressions. Imported into the Symbol refinement like Predications,
|
|
290
433
|
# so every method must be defined with def.
|
|
291
434
|
module Arithmetics
|
|
435
|
+
# `+`; with an Active Support duration on the right, a date moved: `:due_on + 3.days`.
|
|
436
|
+
# @return [AST::Arithmetic]
|
|
292
437
|
def +(other)
|
|
293
438
|
Arithmetic.new(self, :+, other)
|
|
294
439
|
end
|
|
295
440
|
|
|
441
|
+
# `-`; with a duration on the right, a date moved back.
|
|
442
|
+
# @return [AST::Arithmetic]
|
|
296
443
|
def -(other)
|
|
297
444
|
Arithmetic.new(self, :-, other)
|
|
298
445
|
end
|
|
299
446
|
|
|
447
|
+
# `*`.
|
|
448
|
+
# @return [AST::Arithmetic]
|
|
300
449
|
def *(other)
|
|
301
450
|
Arithmetic.new(self, :*, other)
|
|
302
451
|
end
|
|
303
452
|
|
|
453
|
+
# `/`.
|
|
454
|
+
# @return [AST::Arithmetic]
|
|
304
455
|
def /(other)
|
|
305
456
|
Arithmetic.new(self, :/, other)
|
|
306
457
|
end
|
|
307
458
|
|
|
459
|
+
# Bitwise AND. `&` between two conditions is AND, which leaves this free to mean the SQL operator.
|
|
460
|
+
# @return [AST::Bitwise]
|
|
461
|
+
#
|
|
308
462
|
# SQL's bitwise operators. & and | are AND and OR between conditions
|
|
309
463
|
# and are defined there, which is what leaves them free to mean here
|
|
310
464
|
# what SQL means by them. Ruby's precedence puts all six above the
|
|
@@ -313,22 +467,32 @@ module ActiveRecord
|
|
|
313
467
|
Bitwise.new(self, :&, other)
|
|
314
468
|
end
|
|
315
469
|
|
|
470
|
+
# Bitwise OR.
|
|
471
|
+
# @return [AST::Bitwise]
|
|
316
472
|
def |(other)
|
|
317
473
|
Bitwise.new(self, :|, other)
|
|
318
474
|
end
|
|
319
475
|
|
|
476
|
+
# Bitwise XOR: `#` on PostgreSQL, `^` on MySQL, and the two operations it is made of on SQLite.
|
|
477
|
+
# @return [AST::Bitwise]
|
|
320
478
|
def ^(other)
|
|
321
479
|
Bitwise.new(self, :^, other)
|
|
322
480
|
end
|
|
323
481
|
|
|
482
|
+
# A shift left.
|
|
483
|
+
# @return [AST::Bitwise]
|
|
324
484
|
def <<(other)
|
|
325
485
|
Bitwise.new(self, :<<, other)
|
|
326
486
|
end
|
|
327
487
|
|
|
488
|
+
# A shift right.
|
|
489
|
+
# @return [AST::Bitwise]
|
|
328
490
|
def >>(other)
|
|
329
491
|
Bitwise.new(self, :>>, other)
|
|
330
492
|
end
|
|
331
493
|
|
|
494
|
+
# Bitwise NOT.
|
|
495
|
+
# @return [AST::BitwiseNot]
|
|
332
496
|
def ~
|
|
333
497
|
BitwiseNot.new(self)
|
|
334
498
|
end
|
|
@@ -339,6 +503,7 @@ module ActiveRecord
|
|
|
339
503
|
# a column or an expression on the right means a query; anything else
|
|
340
504
|
# goes back to the number through super, so 1 + 2 is 3 inside a block
|
|
341
505
|
# too.
|
|
506
|
+
# @private
|
|
342
507
|
module NumericArithmetics
|
|
343
508
|
def +(other)
|
|
344
509
|
return super unless other.is_a?(::Symbol) || other.is_a?(Node)
|
|
@@ -396,18 +561,31 @@ module ActiveRecord
|
|
|
396
561
|
raise ScriptError, "subclass must override this method"
|
|
397
562
|
end
|
|
398
563
|
|
|
564
|
+
# The expression under an alias, as {BlockSyntax#as} gives a column
|
|
565
|
+
# one.
|
|
566
|
+
# @return [AST::As]
|
|
399
567
|
def as(alias_name, quote: true)
|
|
400
568
|
As.new(self, alias_name, quote: quote)
|
|
401
569
|
end
|
|
402
570
|
|
|
571
|
+
# An ascending ordering by the expression.
|
|
572
|
+
# @return [AST::Ordering]
|
|
403
573
|
def asc
|
|
404
574
|
Ordering.new(self, :asc)
|
|
405
575
|
end
|
|
406
576
|
|
|
577
|
+
# A descending ordering by the expression.
|
|
578
|
+
# @return [AST::Ordering]
|
|
407
579
|
def desc
|
|
408
580
|
Ordering.new(self, :desc)
|
|
409
581
|
end
|
|
410
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
|
+
|
|
411
589
|
private
|
|
412
590
|
# Resolves an operand denoting a column or an expression. A number
|
|
413
591
|
# rides along for Arel to write out, which it can do for Integer and
|
|
@@ -561,16 +739,23 @@ module ActiveRecord
|
|
|
561
739
|
@default = default
|
|
562
740
|
end
|
|
563
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]
|
|
564
744
|
def when(value = nil, &block)
|
|
565
745
|
Pending.new(self, Case.argument(:when, value, block))
|
|
566
746
|
end
|
|
567
747
|
|
|
748
|
+
# `THEN`, which belongs after a `when`; here it says so.
|
|
749
|
+
# @raise [ArgumentError]
|
|
750
|
+
#
|
|
568
751
|
# Kernel#then is on every object, so `then` in the wrong place would be
|
|
569
752
|
# answered by it -- with no block, silently, with an Enumerator.
|
|
570
753
|
def then(*)
|
|
571
754
|
raise ArgumentError, "then follows a when, and there is none to follow here"
|
|
572
755
|
end
|
|
573
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]
|
|
574
759
|
def else(value = nil, &block)
|
|
575
760
|
Case.new(operand, whens, Case.argument(:else, value, block))
|
|
576
761
|
end
|
|
@@ -609,6 +794,8 @@ module ActiveRecord
|
|
|
609
794
|
@condition = condition
|
|
610
795
|
end
|
|
611
796
|
|
|
797
|
+
# `THEN value`, as a value or a block, for the `when` before it.
|
|
798
|
+
# @return [AST::Case]
|
|
612
799
|
def then(value = nil, &block)
|
|
613
800
|
Case.new(@kase.operand,
|
|
614
801
|
@kase.whens + [[@condition, Case.argument(:then, value, block)]],
|
|
@@ -774,27 +961,8 @@ module ActiveRecord
|
|
|
774
961
|
end
|
|
775
962
|
|
|
776
963
|
def to_arel(_table, model)
|
|
777
|
-
|
|
778
|
-
case AST.adapter_family(model)
|
|
779
|
-
when :postgresql then json
|
|
780
|
-
when :mysql then mysql_literal(json, model)
|
|
781
|
-
else refuse(model.connection_db_config.adapter)
|
|
782
|
-
end
|
|
964
|
+
Dialect.for(model).json_literal(Arel::Nodes.build_quoted(JSON.generate(value)), model)
|
|
783
965
|
end
|
|
784
|
-
|
|
785
|
-
private
|
|
786
|
-
# MariaDB answers to the same adapter and has no JSON type at all.
|
|
787
|
-
def mysql_literal(json, model)
|
|
788
|
-
refuse("MariaDB") if model.with_connection { |c| c.mariadb? }
|
|
789
|
-
Arel::Nodes::NamedFunction.new(
|
|
790
|
-
"CAST", [Arel::Nodes::As.new(json, Arel::Nodes::SqlLiteral.new("JSON"))])
|
|
791
|
-
end
|
|
792
|
-
|
|
793
|
-
def refuse(database)
|
|
794
|
-
raise NotImplementedError,
|
|
795
|
-
"a JSON comparison has no equivalent on #{database}; " \
|
|
796
|
-
"dig_text gives the value"
|
|
797
|
-
end
|
|
798
966
|
end
|
|
799
967
|
|
|
800
968
|
# The JSON operations read a document, and what dig gives is one:
|
|
@@ -851,24 +1019,9 @@ module ActiveRecord
|
|
|
851
1019
|
end
|
|
852
1020
|
|
|
853
1021
|
def to_arel(table, model)
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
Arel::Nodes::InfixOperation.new(
|
|
858
|
-
json_value? ? :"#>" : :"#>>", document, Arel::Nodes.build_quoted(steps_array))
|
|
859
|
-
when :mysql
|
|
860
|
-
extracted = Arel::Nodes::NamedFunction.new(
|
|
861
|
-
"JSON_EXTRACT", [document, Arel::Nodes.build_quoted(dollar_path)])
|
|
862
|
-
json_value? ? extracted : Arel::Nodes::NamedFunction.new("JSON_UNQUOTE", [extracted])
|
|
863
|
-
else
|
|
864
|
-
extracted = Arel::Nodes::InfixOperation.new(
|
|
865
|
-
json_value? ? :"->" : :"->>", document, Arel::Nodes.build_quoted(dollar_path))
|
|
866
|
-
# SQLite's ->> gives back the value with its type, where the other
|
|
867
|
-
# two give text. Cast so that `dig_text(:n) == '5'` means the
|
|
868
|
-
# same thing everywhere, and a number wants a cast everywhere too.
|
|
869
|
-
json_value? ? extracted : Arel::Nodes::NamedFunction.new(
|
|
870
|
-
"CAST", [Arel::Nodes::As.new(extracted, Arel::Nodes::SqlLiteral.new("text"))])
|
|
871
|
-
end
|
|
1022
|
+
Dialect.for(model).json_path(
|
|
1023
|
+
to_arel_operand(operand, table, model),
|
|
1024
|
+
dollar_path, steps_array, json_value?, model)
|
|
872
1025
|
end
|
|
873
1026
|
|
|
874
1027
|
private
|
|
@@ -899,40 +1052,13 @@ module ActiveRecord
|
|
|
899
1052
|
end
|
|
900
1053
|
|
|
901
1054
|
def to_arel(table, model)
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
[document, Arel::Nodes.build_quoted(steps_array), postgresql_value(table, model)])
|
|
907
|
-
else
|
|
908
|
-
Arel::Nodes::NamedFunction.new(
|
|
909
|
-
"JSON_SET",
|
|
910
|
-
[document, Arel::Nodes.build_quoted(dollar_path), other_value(table, model)])
|
|
911
|
-
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)
|
|
912
1059
|
end
|
|
913
1060
|
|
|
914
1061
|
private
|
|
915
|
-
# jsonb_set takes jsonb, so an expression is turned into it and a Ruby
|
|
916
|
-
# value goes in as the JSON that says it -- '"x"' rather than 'x',
|
|
917
|
-
# which is not a document at all.
|
|
918
|
-
def postgresql_value(table, model)
|
|
919
|
-
return Arel::Nodes::NamedFunction.new(
|
|
920
|
-
"to_jsonb", [to_arel_operand(value, table, model)]) if expression?
|
|
921
|
-
Arel::Nodes.build_quoted(JSON.generate(value))
|
|
922
|
-
end
|
|
923
|
-
|
|
924
|
-
# The others take the value as it is, except a whole document or a
|
|
925
|
-
# boolean, which go in as JSON through json_argument.
|
|
926
|
-
def other_value(table, model)
|
|
927
|
-
return to_arel_operand(value, table, model) if expression?
|
|
928
|
-
unless value.is_a?(::Hash) || value.is_a?(::Array) ||
|
|
929
|
-
value == true || value == false
|
|
930
|
-
return Arel::Nodes.build_quoted(value)
|
|
931
|
-
end
|
|
932
|
-
|
|
933
|
-
AST.json_argument(value, model)
|
|
934
|
-
end
|
|
935
|
-
|
|
936
1062
|
def expression?
|
|
937
1063
|
value.is_a?(Node) || value.is_a?(::Symbol)
|
|
938
1064
|
end
|
|
@@ -961,31 +1087,13 @@ module ActiveRecord
|
|
|
961
1087
|
end
|
|
962
1088
|
|
|
963
1089
|
def to_arel(table, model)
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
return Arel::Nodes::InfixOperation.new(
|
|
969
|
-
:-, Arel::Nodes::Grouping.new(document), key_array)
|
|
970
|
-
end
|
|
971
|
-
|
|
972
|
-
Arel::Nodes::NamedFunction.new(
|
|
973
|
-
"JSON_REMOVE",
|
|
974
|
-
[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)
|
|
975
1094
|
end
|
|
976
1095
|
|
|
977
1096
|
private
|
|
978
|
-
# jsonb has three subtractions -- a key, an array of keys, an element
|
|
979
|
-
# by index -- and an array literal written without a type is read as
|
|
980
|
-
# the first of them: `meta - '{draft}'` takes out the key spelled
|
|
981
|
-
# {draft}, which is nothing, and says nothing about it.
|
|
982
|
-
def key_array
|
|
983
|
-
Arel::Nodes::NamedFunction.new(
|
|
984
|
-
"CAST",
|
|
985
|
-
[Arel::Nodes::As.new(Arel::Nodes.build_quoted(steps_array(keys)),
|
|
986
|
-
Arel::Nodes::SqlLiteral.new("text[]"))])
|
|
987
|
-
end
|
|
988
|
-
|
|
989
1097
|
# Keys, as Hash#except takes them: an index into an array is not what
|
|
990
1098
|
# the name says anywhere, and is bury's business through a path.
|
|
991
1099
|
def check_keys(keys)
|
|
@@ -1013,17 +1121,9 @@ module ActiveRecord
|
|
|
1013
1121
|
end
|
|
1014
1122
|
|
|
1015
1123
|
def to_arel(table, model)
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
when :postgresql then Arel::Nodes::Contains.new(document, json)
|
|
1020
|
-
when :mysql
|
|
1021
|
-
Arel::Nodes::NamedFunction.new("JSON_CONTAINS", [document, json])
|
|
1022
|
-
else
|
|
1023
|
-
# Later than the others, since the adapter is only known here.
|
|
1024
|
-
raise NotImplementedError,
|
|
1025
|
-
"contains? has no equivalent on #{model.connection_db_config.adapter}"
|
|
1026
|
-
end
|
|
1124
|
+
Dialect.for(model).json_contains(
|
|
1125
|
+
to_arel_operand(operand, table, model),
|
|
1126
|
+
Arel::Nodes.build_quoted(JSON.generate(value)), model)
|
|
1027
1127
|
end
|
|
1028
1128
|
end
|
|
1029
1129
|
|
|
@@ -1041,18 +1141,10 @@ module ActiveRecord
|
|
|
1041
1141
|
end
|
|
1042
1142
|
|
|
1043
1143
|
def to_arel(table, model)
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
when :postgresql
|
|
1049
|
-
Arel::Nodes::InfixOperation.new(:"?", document, name)
|
|
1050
|
-
when :mysql
|
|
1051
|
-
Arel::Nodes::NamedFunction.new(
|
|
1052
|
-
"JSON_CONTAINS_PATH", [document, Arel::Nodes.build_quoted("one"), path])
|
|
1053
|
-
else
|
|
1054
|
-
Arel::Nodes::NamedFunction.new("json_type", [document, path]).not_eq(nil)
|
|
1055
|
-
end
|
|
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)
|
|
1056
1148
|
end
|
|
1057
1149
|
end
|
|
1058
1150
|
|
|
@@ -1075,30 +1167,10 @@ module ActiveRecord
|
|
|
1075
1167
|
end
|
|
1076
1168
|
|
|
1077
1169
|
def to_arel(table, model)
|
|
1078
|
-
|
|
1079
|
-
case AST.adapter_family(model)
|
|
1080
|
-
when :sqlite
|
|
1081
|
-
sql = compile(document, model)
|
|
1082
|
-
Arel.sql("CASE WHEN json_type(#{sql}) = 'object' " \
|
|
1083
|
-
"THEN (SELECT json_group_array(key) FROM json_each(#{sql})) END")
|
|
1084
|
-
when :postgresql
|
|
1085
|
-
sql = compile(document, model)
|
|
1086
|
-
Arel.sql("CASE WHEN jsonb_typeof(#{sql}) = 'object' " \
|
|
1087
|
-
"THEN COALESCE((SELECT jsonb_agg(k) FROM jsonb_object_keys(#{sql}) k), " \
|
|
1088
|
-
"CAST('[]' AS jsonb)) END")
|
|
1089
|
-
else
|
|
1090
|
-
Arel::Nodes::NamedFunction.new("JSON_KEYS", [document])
|
|
1091
|
-
end
|
|
1170
|
+
Dialect.for(model).json_keys(to_arel_operand(operand, table, model), model)
|
|
1092
1171
|
end
|
|
1093
1172
|
|
|
1094
1173
|
private
|
|
1095
|
-
# The document appears more than once, the way SQLite's XOR names
|
|
1096
|
-
# its operands twice; the connection's own visitor compiles it, so
|
|
1097
|
-
# its quoting is the adapter's.
|
|
1098
|
-
def compile(document, model)
|
|
1099
|
-
model.with_connection { |connection| connection.visitor.compile(document) }
|
|
1100
|
-
end
|
|
1101
|
-
|
|
1102
1174
|
def json_source
|
|
1103
1175
|
"keys"
|
|
1104
1176
|
end
|
|
@@ -1113,11 +1185,6 @@ module ActiveRecord
|
|
|
1113
1185
|
include JsonComparable
|
|
1114
1186
|
include ComputedJson
|
|
1115
1187
|
|
|
1116
|
-
NAMES = {
|
|
1117
|
-
array: { postgresql: "jsonb_build_array" },
|
|
1118
|
-
object: { postgresql: "jsonb_build_object" },
|
|
1119
|
-
}.freeze
|
|
1120
|
-
|
|
1121
1188
|
attr_reader :kind, :values
|
|
1122
1189
|
|
|
1123
1190
|
def initialize(kind, values)
|
|
@@ -1126,39 +1193,23 @@ module ActiveRecord
|
|
|
1126
1193
|
end
|
|
1127
1194
|
|
|
1128
1195
|
def to_arel(table, model)
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
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)
|
|
1200
|
+
else
|
|
1201
|
+
dialect.json_build(:object, values.keys.map(&:to_s),
|
|
1202
|
+
values.values.map { |value| build_argument(value, dialect, table, model) }, model)
|
|
1203
|
+
end
|
|
1132
1204
|
end
|
|
1133
1205
|
|
|
1134
1206
|
private
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
else
|
|
1139
|
-
values.flat_map do |key, value|
|
|
1140
|
-
[Arel::Nodes.build_quoted(key.to_s),
|
|
1141
|
-
build_argument(value, table, model)]
|
|
1142
|
-
end
|
|
1143
|
-
end
|
|
1144
|
-
end
|
|
1145
|
-
|
|
1146
|
-
# An expression is itself and a document or a boolean goes in as
|
|
1147
|
-
# JSON, as bury takes them. PostgreSQL builds from typed
|
|
1148
|
-
# arguments, so its JSON literal is cast -- left untyped it would
|
|
1149
|
-
# be text, and land as a string.
|
|
1150
|
-
def build_argument(value, table, model)
|
|
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)
|
|
1151
1210
|
case value
|
|
1152
1211
|
when Node, ::Symbol then to_arel_operand(value, table, model)
|
|
1153
|
-
when ::Hash, ::Array, true, false
|
|
1154
|
-
if AST.adapter_family(model) == :postgresql
|
|
1155
|
-
Arel::Nodes::NamedFunction.new(
|
|
1156
|
-
"CAST", [Arel::Nodes::As.new(
|
|
1157
|
-
Arel::Nodes.build_quoted(JSON.generate(value)),
|
|
1158
|
-
Arel::Nodes::SqlLiteral.new("jsonb"))])
|
|
1159
|
-
else
|
|
1160
|
-
AST.json_argument(value, model)
|
|
1161
|
-
end
|
|
1212
|
+
when ::Hash, ::Array, true, false then dialect.json_build_argument(value, model)
|
|
1162
1213
|
when ::Rational then quote_number(value)
|
|
1163
1214
|
else Arel::Nodes.build_quoted(value)
|
|
1164
1215
|
end
|
|
@@ -1194,6 +1245,7 @@ module ActiveRecord
|
|
|
1194
1245
|
# Each set is a list of its own, so grouping_sets takes lists and rollup
|
|
1195
1246
|
# and cube take the columns themselves.
|
|
1196
1247
|
class GroupingSets < Node
|
|
1248
|
+
# @private
|
|
1197
1249
|
KINDS = {
|
|
1198
1250
|
grouping_sets: Arel::Nodes::GroupingSet,
|
|
1199
1251
|
rollup: Arel::Nodes::RollUp,
|
|
@@ -1209,7 +1261,7 @@ module ActiveRecord
|
|
|
1209
1261
|
end
|
|
1210
1262
|
|
|
1211
1263
|
def to_arel(table, model)
|
|
1212
|
-
return with_rollup(table, model) if
|
|
1264
|
+
return with_rollup(table, model) if Dialect.for(model).grouping_by_with_rollup?
|
|
1213
1265
|
|
|
1214
1266
|
KINDS.fetch(kind).new(
|
|
1215
1267
|
if kind == :grouping_sets
|
|
@@ -1256,10 +1308,25 @@ module ActiveRecord
|
|
|
1256
1308
|
# Arithmetic on columns and expressions. Ruby's precedence puts these
|
|
1257
1309
|
# above the comparison operators, so :price * :quantity > 100 groups the
|
|
1258
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.
|
|
1259
1315
|
class Arithmetic < Node
|
|
1260
1316
|
include Predications
|
|
1261
1317
|
include Arithmetics
|
|
1262
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
|
+
|
|
1263
1330
|
attr_reader :left, :operator, :right
|
|
1264
1331
|
|
|
1265
1332
|
def initialize(left, operator, right)
|
|
@@ -1270,11 +1337,53 @@ module ActiveRecord
|
|
|
1270
1337
|
|
|
1271
1338
|
def to_arel(table, model)
|
|
1272
1339
|
arel_left = to_arel_operand(left, table, model)
|
|
1340
|
+
return move_date(arel_left, model) if right.is_a?(::ActiveSupport::Duration)
|
|
1273
1341
|
# The operator dispatches Arel's Math, which a bare number carries
|
|
1274
1342
|
# none of; quoted, it is a node with the same methods.
|
|
1275
1343
|
arel_left = Arel::Nodes.build_quoted(arel_left) if arel_left.is_a?(::Numeric)
|
|
1276
1344
|
arel_left.public_send(operator, to_arel_operand(right, table, model))
|
|
1277
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
|
|
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
|
|
1278
1387
|
end
|
|
1279
1388
|
|
|
1280
1389
|
# What the bitwise operators refuse. Both refusals are there because
|
|
@@ -1310,6 +1419,7 @@ module ActiveRecord
|
|
|
1310
1419
|
include Arithmetics
|
|
1311
1420
|
include BitwiseOperands
|
|
1312
1421
|
|
|
1422
|
+
# @private
|
|
1313
1423
|
NODES = {
|
|
1314
1424
|
:& => Arel::Nodes::BitwiseAnd,
|
|
1315
1425
|
:| => Arel::Nodes::BitwiseOr,
|
|
@@ -1345,14 +1455,7 @@ module ActiveRecord
|
|
|
1345
1455
|
# it cannot be the portable one either. SQLite has no XOR at all;
|
|
1346
1456
|
# (a | b) - (a & b) is it, at the cost of naming each operand twice.
|
|
1347
1457
|
def xor(left, right, model)
|
|
1348
|
-
|
|
1349
|
-
when :postgresql then Arel::Nodes::InfixOperation.new("#", left, right)
|
|
1350
|
-
when :mysql then Arel::Nodes::BitwiseXor.new(left, right)
|
|
1351
|
-
else
|
|
1352
|
-
Arel::Nodes::Subtraction.new(
|
|
1353
|
-
Arel::Nodes::Grouping.new(Arel::Nodes::BitwiseOr.new(left, right)),
|
|
1354
|
-
Arel::Nodes::Grouping.new(Arel::Nodes::BitwiseAnd.new(left, right)))
|
|
1355
|
-
end
|
|
1458
|
+
Dialect.for(model).bitwise_xor(left, right)
|
|
1356
1459
|
end
|
|
1357
1460
|
end
|
|
1358
1461
|
|
|
@@ -1380,6 +1483,12 @@ module ActiveRecord
|
|
|
1380
1483
|
# OVER, on the two things that can carry a window: an aggregate, and a
|
|
1381
1484
|
# function.
|
|
1382
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) }
|
|
1383
1492
|
def over
|
|
1384
1493
|
Over.new(self)
|
|
1385
1494
|
end
|
|
@@ -1401,20 +1510,30 @@ module ActiveRecord
|
|
|
1401
1510
|
@frame = frame
|
|
1402
1511
|
end
|
|
1403
1512
|
|
|
1513
|
+
# `PARTITION BY`, the columns or expressions given.
|
|
1514
|
+
# @return [AST::Over]
|
|
1404
1515
|
def partition(*exprs)
|
|
1405
1516
|
raise ArgumentError, "partition needs an expression" if exprs.empty?
|
|
1406
1517
|
Over.new(function, partitions + exprs, orders, frame)
|
|
1407
1518
|
end
|
|
1408
1519
|
|
|
1520
|
+
# `ORDER BY` within the window: columns, or orderings such as `:age.desc`.
|
|
1521
|
+
# @return [AST::Over]
|
|
1409
1522
|
def order(*exprs)
|
|
1410
1523
|
raise ArgumentError, "order needs an expression" if exprs.empty?
|
|
1411
1524
|
Over.new(function, partitions, orders + exprs, frame)
|
|
1412
1525
|
end
|
|
1413
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]
|
|
1414
1530
|
def rows(bounds)
|
|
1415
1531
|
Over.new(function, partitions, orders, framing(:rows, bounds))
|
|
1416
1532
|
end
|
|
1417
1533
|
|
|
1534
|
+
# `RANGE BETWEEN`, with the bounds as {#rows} takes them.
|
|
1535
|
+
# @param bounds [Range]
|
|
1536
|
+
# @return [AST::Over]
|
|
1418
1537
|
def range(bounds)
|
|
1419
1538
|
Over.new(function, partitions, orders, framing(:range, bounds))
|
|
1420
1539
|
end
|
|
@@ -1425,9 +1544,9 @@ module ActiveRecord
|
|
|
1425
1544
|
orders.each { |expr| window.order(to_arel_operand(expr, table, model)) }
|
|
1426
1545
|
frame_arel(window) if frame
|
|
1427
1546
|
|
|
1428
|
-
#
|
|
1429
|
-
#
|
|
1430
|
-
function.check_window(model) if function.
|
|
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)
|
|
1431
1550
|
|
|
1432
1551
|
# A window-only function refuses to build on its own; here is where
|
|
1433
1552
|
# it is asked for the call itself.
|
|
@@ -1481,11 +1600,17 @@ module ActiveRecord
|
|
|
1481
1600
|
include Arithmetics
|
|
1482
1601
|
include Windowing
|
|
1483
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
|
+
|
|
1484
1609
|
attr_reader :operand, :function, :distinct, :condition
|
|
1485
1610
|
|
|
1486
1611
|
def initialize(operand, function, distinct: false, condition: nil)
|
|
1487
|
-
if distinct && function
|
|
1488
|
-
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"
|
|
1489
1614
|
end
|
|
1490
1615
|
if distinct && operand == :*
|
|
1491
1616
|
raise ArgumentError, "count(:*) does not take distinct; name a column"
|
|
@@ -1496,8 +1621,12 @@ module ActiveRecord
|
|
|
1496
1621
|
@condition = condition
|
|
1497
1622
|
end
|
|
1498
1623
|
|
|
1499
|
-
# FILTER (WHERE
|
|
1500
|
-
# 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)] }
|
|
1501
1630
|
def filter(condition = nil, &block)
|
|
1502
1631
|
Aggregate.new(operand, function, distinct: distinct,
|
|
1503
1632
|
condition: Case.argument(:filter, condition, block))
|
|
@@ -1506,11 +1635,11 @@ module ActiveRecord
|
|
|
1506
1635
|
def to_arel(table, model)
|
|
1507
1636
|
return aggregate(operand, table, model) unless condition
|
|
1508
1637
|
|
|
1509
|
-
#
|
|
1510
|
-
#
|
|
1511
|
-
# the
|
|
1512
|
-
# keep, and counts a 1 instead.
|
|
1513
|
-
|
|
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?
|
|
1514
1643
|
kept = Case.new.when(condition).then(operand == :* ? 1 : operand)
|
|
1515
1644
|
return aggregate(kept, table, model)
|
|
1516
1645
|
end
|
|
@@ -1521,11 +1650,10 @@ module ActiveRecord
|
|
|
1521
1650
|
private
|
|
1522
1651
|
def aggregate(over, table, model)
|
|
1523
1652
|
arel_operand = to_arel_operand(over, table, model)
|
|
1524
|
-
if function == :count
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
end
|
|
1653
|
+
return arel_operand.count(distinct) if function == :count
|
|
1654
|
+
call = arel_operand.public_send(function)
|
|
1655
|
+
call.distinct = distinct
|
|
1656
|
+
call
|
|
1529
1657
|
end
|
|
1530
1658
|
end
|
|
1531
1659
|
|
|
@@ -1540,14 +1668,6 @@ module ActiveRecord
|
|
|
1540
1668
|
include ComputedJson
|
|
1541
1669
|
include Windowing
|
|
1542
1670
|
|
|
1543
|
-
# The standard names, which are also the DSL's own and MySQL's, serve
|
|
1544
|
-
# any adapter the table does not list.
|
|
1545
|
-
NAMES = {
|
|
1546
|
-
arrayagg: { sqlite: "json_group_array", postgresql: "jsonb_agg" },
|
|
1547
|
-
objectagg: { sqlite: "json_group_object",
|
|
1548
|
-
postgresql: "jsonb_object_agg" },
|
|
1549
|
-
}.freeze
|
|
1550
|
-
|
|
1551
1671
|
attr_reader :kind, :operands, :condition
|
|
1552
1672
|
|
|
1553
1673
|
def initialize(kind, operands, condition: nil)
|
|
@@ -1556,31 +1676,32 @@ module ActiveRecord
|
|
|
1556
1676
|
@condition = condition
|
|
1557
1677
|
end
|
|
1558
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]
|
|
1559
1683
|
def filter(condition = nil, &block)
|
|
1560
1684
|
JsonAggregate.new(kind, operands,
|
|
1561
1685
|
condition: Case.argument(:filter, condition, block))
|
|
1562
1686
|
end
|
|
1563
1687
|
|
|
1564
|
-
#
|
|
1565
|
-
#
|
|
1688
|
+
# Over asks here before writing a window, since a family may take
|
|
1689
|
+
# every other aggregate as one but not these two.
|
|
1566
1690
|
def check_window(model)
|
|
1567
|
-
|
|
1568
|
-
return unless model.with_connection { |connection| connection.mariadb? }
|
|
1569
|
-
raise NotImplementedError,
|
|
1570
|
-
"#{json_source} over a window has no equivalent on MariaDB"
|
|
1691
|
+
Dialect.for(model).check_json_aggregate_window(json_source, model)
|
|
1571
1692
|
end
|
|
1572
1693
|
|
|
1573
1694
|
def to_arel(table, model)
|
|
1574
|
-
|
|
1695
|
+
dialect = Dialect.for(model)
|
|
1575
1696
|
call = Arel::Nodes::NamedFunction.new(
|
|
1576
|
-
|
|
1697
|
+
dialect.json_aggregate_name(kind),
|
|
1577
1698
|
operands.map { |operand| to_arel_argument(operand, table, model) })
|
|
1578
1699
|
return call unless condition
|
|
1579
1700
|
|
|
1580
|
-
# The CASE that stands in for FILTER elsewhere hands the aggregate
|
|
1581
|
-
#
|
|
1582
|
-
#
|
|
1583
|
-
|
|
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?
|
|
1584
1705
|
raise NotImplementedError,
|
|
1585
1706
|
"#{json_source}.filter has no equivalent on " \
|
|
1586
1707
|
"#{model.connection_db_config.adapter}; a CASE would leave a " \
|
|
@@ -1595,6 +1716,70 @@ module ActiveRecord
|
|
|
1595
1716
|
end
|
|
1596
1717
|
end
|
|
1597
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"
|
|
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))
|
|
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
|
|
1781
|
+
end
|
|
1782
|
+
|
|
1598
1783
|
# A column alias, quoted by the adapter, so that the name asked for is
|
|
1599
1784
|
# the name that comes back: unquoted, PostgreSQL folds a capital away
|
|
1600
1785
|
# and the other two keep it, which is one block meaning two things.
|
|
@@ -1628,6 +1813,29 @@ module ActiveRecord
|
|
|
1628
1813
|
end
|
|
1629
1814
|
end
|
|
1630
1815
|
|
|
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)
|
|
1836
|
+
end
|
|
1837
|
+
end
|
|
1838
|
+
|
|
1631
1839
|
class Ordering < Node
|
|
1632
1840
|
attr_reader :operand, :direction, :nulls
|
|
1633
1841
|
|
|
@@ -1637,12 +1845,18 @@ module ActiveRecord
|
|
|
1637
1845
|
@nulls = nulls
|
|
1638
1846
|
end
|
|
1639
1847
|
|
|
1848
|
+
# `NULLS FIRST`; portable, since Arel emulates it where MySQL has
|
|
1849
|
+
# none.
|
|
1850
|
+
# @return [AST::Ordering]
|
|
1851
|
+
#
|
|
1640
1852
|
# MySQL has no NULLS FIRST/LAST, but Arel emulates it there with a
|
|
1641
1853
|
# leading IS NULL ordering, so these are portable.
|
|
1642
1854
|
def nulls_first
|
|
1643
1855
|
Ordering.new(operand, direction, :nulls_first)
|
|
1644
1856
|
end
|
|
1645
1857
|
|
|
1858
|
+
# `NULLS LAST`.
|
|
1859
|
+
# @return [AST::Ordering]
|
|
1646
1860
|
def nulls_last
|
|
1647
1861
|
Ordering.new(operand, direction, :nulls_last)
|
|
1648
1862
|
end
|
|
@@ -1798,6 +2012,7 @@ module ActiveRecord
|
|
|
1798
2012
|
# or an Array compares against a PostgreSQL range or array column, the way
|
|
1799
2013
|
# Active Record's own force_equality? types do.
|
|
1800
2014
|
class Comparison < Predicate
|
|
2015
|
+
# @private
|
|
1801
2016
|
OPERATOR_MAP = {
|
|
1802
2017
|
:== => :eq, :!= => :not_eq,
|
|
1803
2018
|
:> => :gt, :>= => :gteq, :< => :lt, :<= => :lteq
|
|
@@ -1848,9 +2063,8 @@ module ActiveRecord
|
|
|
1848
2063
|
end
|
|
1849
2064
|
|
|
1850
2065
|
def to_arel(table, model)
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
to_arel_operand(operand, table, model), literal)
|
|
2066
|
+
Dialect.for(model).truth_value(
|
|
2067
|
+
to_arel_operand(operand, table, model), value, negated, model)
|
|
1854
2068
|
end
|
|
1855
2069
|
end
|
|
1856
2070
|
|
|
@@ -1939,12 +2153,12 @@ module ActiveRecord
|
|
|
1939
2153
|
# they resolve.
|
|
1940
2154
|
def json_between?(model)
|
|
1941
2155
|
(values.begin.is_a?(JsonLiteral) || values.end.is_a?(JsonLiteral)) &&
|
|
1942
|
-
|
|
2156
|
+
Dialect.for(model).json_list_by_element?
|
|
1943
2157
|
end
|
|
1944
2158
|
|
|
1945
2159
|
def json_list?(model)
|
|
1946
2160
|
values.any? { |value| value.is_a?(JsonLiteral) } &&
|
|
1947
|
-
|
|
2161
|
+
Dialect.for(model).json_list_by_element?
|
|
1948
2162
|
end
|
|
1949
2163
|
|
|
1950
2164
|
def json_list(arel_operand, elements)
|
|
@@ -2003,6 +2217,7 @@ module ActiveRecord
|
|
|
2003
2217
|
end
|
|
2004
2218
|
|
|
2005
2219
|
class Like < Predicate
|
|
2220
|
+
# @private
|
|
2006
2221
|
ESCAPE = "\\"
|
|
2007
2222
|
|
|
2008
2223
|
# Escapes % and _ so that they match literally. The pattern built from
|