activerecord-refined 0.5.0 → 0.6.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/.github/workflows/sandbox.yml +162 -26
- data/.github/workflows/test.yml +15 -2
- data/README.md +486 -52
- data/activerecord-refined.gemspec +3 -2
- data/examples/ctes.rb +41 -10
- data/examples/expressions.rb +71 -5
- data/examples/json.rb +94 -0
- data/examples/postgresql.rb +89 -6
- data/examples/predicates.rb +32 -5
- data/examples/windows.rb +96 -0
- data/examples/writes.rb +84 -0
- data/lib/active_record/refined/ast.rb +831 -94
- data/lib/active_record/refined.rb +351 -26
- data/lib/activerecord-refined/version.rb +1 -1
- data/lib/activerecord-refined.rb +7 -0
- data/test/test_block_syntax.rb +1100 -51
- data/test/test_helper.rb +82 -0
- metadata +4 -1
|
@@ -1,11 +1,9 @@
|
|
|
1
|
+
# JSON.generate, for the document a containment test is given.
|
|
2
|
+
require 'json'
|
|
3
|
+
|
|
1
4
|
module ActiveRecord
|
|
2
5
|
module Refined
|
|
3
6
|
module AST
|
|
4
|
-
# Column aliases and function names are written into the SQL as given,
|
|
5
|
-
# where a value would have been quoted, so anything that is not a plain
|
|
6
|
-
# name is refused. Quoting them instead would need the model's adapter,
|
|
7
|
-
# which does not reach this far, and quoting with the wrong one would be
|
|
8
|
-
# a bug of its own -- MySQL does not read "x" as an identifier.
|
|
9
7
|
NAME = /[[:alpha:]_][[:alnum:]_$]*/
|
|
10
8
|
ALIAS_NAME = /\A#{NAME}\z/
|
|
11
9
|
FUNCTION_NAME = /\A#{NAME}(\.#{NAME})?\z/
|
|
@@ -13,6 +11,22 @@ module ActiveRecord
|
|
|
13
11
|
# lengths -- double precision, decimal(10,2).
|
|
14
12
|
TYPE_NAME = /\A[[:alpha:]_][[:alnum:]_ ]*(\(\d+(, ?\d+)?\))?\z/
|
|
15
13
|
|
|
14
|
+
# Which family of spellings an adapter belongs to. MariaDB answers to
|
|
15
|
+
# the mysql2 adapter and is counted with MySQL, though the two part
|
|
16
|
+
# company over JSON. An adapter nobody has classified keeps the
|
|
17
|
+
# standard spellings and is left to say for itself what it cannot do.
|
|
18
|
+
ADAPTER_FAMILIES = {
|
|
19
|
+
'sqlite3' => :sqlite,
|
|
20
|
+
'postgresql' => :postgresql,
|
|
21
|
+
'postgis' => :postgresql,
|
|
22
|
+
'mysql2' => :mysql,
|
|
23
|
+
'trilogy' => :mysql,
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
def self.adapter_family(model)
|
|
27
|
+
ADAPTER_FAMILIES[model.connection_db_config.adapter] || :unknown
|
|
28
|
+
end
|
|
29
|
+
|
|
16
30
|
def self.check_name(name, pattern, what)
|
|
17
31
|
return name if pattern.match?(name.to_s)
|
|
18
32
|
raise ArgumentError, "#{name.inspect} is not a plain #{what}"
|
|
@@ -63,26 +77,78 @@ module ActiveRecord
|
|
|
63
77
|
Match.new(self, pattern, negated: true)
|
|
64
78
|
end
|
|
65
79
|
|
|
80
|
+
# `!` negates any predicate, so these are here for the four that SQL
|
|
81
|
+
# spells for itself: IS NOT NULL rather than NOT (... IS NULL), and
|
|
82
|
+
# likewise NOT IN and NOT LIKE. They mean the same thing either way,
|
|
83
|
+
# including when the column is NULL; what they save is the reading.
|
|
66
84
|
def null?
|
|
67
85
|
Comparison.new(self, :==, nil)
|
|
68
86
|
end
|
|
69
87
|
|
|
88
|
+
def not_null?
|
|
89
|
+
Comparison.new(self, :!=, nil)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# IS TRUE and IS FALSE differ from a comparison against the literal in
|
|
93
|
+
# what they make of NULL: `flag = TRUE` is itself NULL there, and a
|
|
94
|
+
# NULL predicate selects nothing, while these two answer false. So the
|
|
95
|
+
# difference shows in the negations: `not_true?` keeps the NULL rows
|
|
96
|
+
# that `!(:flag == true)` drops.
|
|
97
|
+
def true?
|
|
98
|
+
TruthValue.new(self, true)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def not_true?
|
|
102
|
+
TruthValue.new(self, true, negated: true)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def false?
|
|
106
|
+
TruthValue.new(self, false)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def not_false?
|
|
110
|
+
TruthValue.new(self, false, negated: true)
|
|
111
|
+
end
|
|
112
|
+
|
|
70
113
|
def in?(values)
|
|
71
114
|
In.new(self, values)
|
|
72
115
|
end
|
|
73
116
|
|
|
117
|
+
def not_in?(values)
|
|
118
|
+
In.new(self, values, negated: true)
|
|
119
|
+
end
|
|
120
|
+
|
|
74
121
|
def between?(min, max)
|
|
75
122
|
In.new(self, min..max)
|
|
76
123
|
end
|
|
77
124
|
|
|
125
|
+
def not_between?(min, max)
|
|
126
|
+
In.new(self, min..max, negated: true)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# CASE with this as the operand, compared against each `when`:
|
|
130
|
+
# `:age.when(10).then(1).else(0)`. The other shape, where each `when`
|
|
131
|
+
# carries its own condition, starts at `case_when`.
|
|
132
|
+
def when(value = nil, &block)
|
|
133
|
+
Case.new(self).when(value, &block)
|
|
134
|
+
end
|
|
135
|
+
|
|
78
136
|
def like?(pattern)
|
|
79
137
|
Like.new(self, pattern)
|
|
80
138
|
end
|
|
81
139
|
|
|
140
|
+
def not_like?(pattern)
|
|
141
|
+
Like.new(self, pattern, negated: true)
|
|
142
|
+
end
|
|
143
|
+
|
|
82
144
|
def ilike?(pattern)
|
|
83
145
|
Like.new(self, pattern, nil, case_sensitive: false)
|
|
84
146
|
end
|
|
85
147
|
|
|
148
|
+
def not_ilike?(pattern)
|
|
149
|
+
Like.new(self, pattern, nil, case_sensitive: false, negated: true)
|
|
150
|
+
end
|
|
151
|
+
|
|
86
152
|
# Case-insensitive equality, folded on both sides rather than left to
|
|
87
153
|
# the collation, so it means the same thing on every adapter.
|
|
88
154
|
def casecmp?(value)
|
|
@@ -144,6 +210,39 @@ module ActiveRecord
|
|
|
144
210
|
def intersect?(elements)
|
|
145
211
|
ArrayPredicate.new(self, :"&&", ArrayPredicate.elements(elements, "intersect?"))
|
|
146
212
|
end
|
|
213
|
+
|
|
214
|
+
# Reading inside a JSON document, by the name of what Hash does. A
|
|
215
|
+
# string or symbol steps into an object, an integer into an array, and
|
|
216
|
+
# what comes back is the value rather than the JSON, since that is what
|
|
217
|
+
# a comparison wants. dig_json keeps it JSON, for a document to be dug
|
|
218
|
+
# into further or compared whole.
|
|
219
|
+
def dig(*path)
|
|
220
|
+
JsonPath.new(self, path)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def dig_json(*path)
|
|
224
|
+
JsonPath.new(self, path, as_json: true)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# What dig reads, bury sets: the last argument is the value and the
|
|
228
|
+
# rest are the path to it. The document comes back changed rather
|
|
229
|
+
# than being written anywhere, which update_all is for.
|
|
230
|
+
def bury(*path, value)
|
|
231
|
+
JsonSet.new(self, path, value)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Whether the document holds what is given, which SQL calls
|
|
235
|
+
# containment. SQLite has no equivalent.
|
|
236
|
+
def contains?(value)
|
|
237
|
+
JsonContains.new(self, value)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Whether the key is there at all, as Hash#key? asks. Hash has
|
|
241
|
+
# has_key? too; one name is enough, and this is the one Ruby's own
|
|
242
|
+
# style prefers.
|
|
243
|
+
def key?(key)
|
|
244
|
+
JsonHasKey.new(self, key)
|
|
245
|
+
end
|
|
147
246
|
end
|
|
148
247
|
|
|
149
248
|
# Arithmetic builders shared by symbols, qualified columns and
|
|
@@ -165,41 +264,48 @@ module ActiveRecord
|
|
|
165
264
|
def /(other)
|
|
166
265
|
Arithmetic.new(self, :/, other)
|
|
167
266
|
end
|
|
168
|
-
end
|
|
169
267
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
268
|
+
# SQL's bitwise operators. & and | are AND and OR between conditions
|
|
269
|
+
# and are defined there, which is what leaves them free to mean here
|
|
270
|
+
# what SQL means by them. Ruby's precedence puts all six above the
|
|
271
|
+
# comparisons, so `:flags & 4 > 0` groups the way it reads.
|
|
272
|
+
def &(other)
|
|
273
|
+
Bitwise.new(self, :&, other)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def |(other)
|
|
277
|
+
Bitwise.new(self, :|, other)
|
|
177
278
|
end
|
|
178
279
|
|
|
179
|
-
def
|
|
180
|
-
|
|
280
|
+
def ^(other)
|
|
281
|
+
Bitwise.new(self, :^, other)
|
|
181
282
|
end
|
|
182
283
|
|
|
183
|
-
def
|
|
184
|
-
|
|
284
|
+
def <<(other)
|
|
285
|
+
Bitwise.new(self, :<<, other)
|
|
185
286
|
end
|
|
186
287
|
|
|
187
|
-
def
|
|
188
|
-
|
|
288
|
+
def >>(other)
|
|
289
|
+
Bitwise.new(self, :>>, other)
|
|
189
290
|
end
|
|
190
291
|
|
|
191
|
-
def
|
|
192
|
-
|
|
292
|
+
def ~
|
|
293
|
+
BitwiseNot.new(self)
|
|
193
294
|
end
|
|
194
295
|
end
|
|
195
296
|
|
|
196
297
|
class Node
|
|
197
|
-
|
|
298
|
+
# The model travels with the table because some SQL cannot be written
|
|
299
|
+
# without knowing the adapter, and a node is built before anything
|
|
300
|
+
# knows which one it will be rendered for -- a symbol becomes a node
|
|
301
|
+
# inside a refinement, where there is no model to ask. Most nodes
|
|
302
|
+
# never look at it and only pass it on.
|
|
303
|
+
def to_arel(table, model)
|
|
198
304
|
raise ScriptError, "subclass must override this method"
|
|
199
305
|
end
|
|
200
306
|
|
|
201
|
-
def as(alias_name)
|
|
202
|
-
As.new(self, alias_name)
|
|
307
|
+
def as(alias_name, quote: true)
|
|
308
|
+
As.new(self, alias_name, quote: quote)
|
|
203
309
|
end
|
|
204
310
|
|
|
205
311
|
def asc
|
|
@@ -213,9 +319,9 @@ module ActiveRecord
|
|
|
213
319
|
private
|
|
214
320
|
|
|
215
321
|
# Resolves an operand denoting a column or an expression.
|
|
216
|
-
def to_arel_operand(operand, table)
|
|
322
|
+
def to_arel_operand(operand, table, model)
|
|
217
323
|
case operand
|
|
218
|
-
when Node then operand.to_arel(table)
|
|
324
|
+
when Node then operand.to_arel(table, model)
|
|
219
325
|
when :* then Arel.star
|
|
220
326
|
when Symbol then table[operand]
|
|
221
327
|
else operand
|
|
@@ -224,9 +330,9 @@ module ActiveRecord
|
|
|
224
330
|
|
|
225
331
|
# Resolves a function argument: a column or an expression as above,
|
|
226
332
|
# anything else a value to be quoted.
|
|
227
|
-
def to_arel_argument(arg, table)
|
|
333
|
+
def to_arel_argument(arg, table, model)
|
|
228
334
|
case arg
|
|
229
|
-
when Node, Symbol then to_arel_operand(arg, table)
|
|
335
|
+
when Node, Symbol then to_arel_operand(arg, table, model)
|
|
230
336
|
else Arel::Nodes.build_quoted(arg)
|
|
231
337
|
end
|
|
232
338
|
end
|
|
@@ -246,10 +352,333 @@ module ActiveRecord
|
|
|
246
352
|
end
|
|
247
353
|
end
|
|
248
354
|
|
|
355
|
+
# A literal standing where an expression would: `select { value(0).as(:depth) }`.
|
|
356
|
+
#
|
|
357
|
+
# Values reach the SQL quoted wherever they appear as an operand, but the
|
|
358
|
+
# top of a select list is ActiveRecord's, and a bare string there is SQL
|
|
359
|
+
# rather than a string. Saying `value` is how you ask for the other
|
|
360
|
+
# meaning, and it carries the predications with it, so a literal can be
|
|
361
|
+
# compared and combined like anything else.
|
|
362
|
+
class Value < Node
|
|
363
|
+
include Predications
|
|
364
|
+
include Arithmetics
|
|
365
|
+
|
|
366
|
+
attr_reader :value
|
|
367
|
+
|
|
368
|
+
def initialize(value)
|
|
369
|
+
@value = value
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def to_arel(_table, _model)
|
|
373
|
+
Arel::Nodes.build_quoted(value)
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
# CASE, in both of the shapes SQL has for it. With an operand, each
|
|
378
|
+
# `when` is something to compare it against; without one, each `when` is
|
|
379
|
+
# a condition of its own.
|
|
380
|
+
#
|
|
381
|
+
# Every method returns a new node rather than adding to this one, so a
|
|
382
|
+
# case kept in a variable can be branched from more than once.
|
|
383
|
+
class Case < Node
|
|
384
|
+
include Predications
|
|
385
|
+
include Arithmetics
|
|
386
|
+
|
|
387
|
+
# Having no ELSE is not the same as an ELSE of nil, and nil is what an
|
|
388
|
+
# omitted argument looks like, so the absence needs a value of its own.
|
|
389
|
+
NOTHING = Object.new.freeze
|
|
390
|
+
private_constant :NOTHING
|
|
391
|
+
|
|
392
|
+
attr_reader :operand, :whens, :default
|
|
393
|
+
|
|
394
|
+
def initialize(operand = nil, whens = [], default = NOTHING)
|
|
395
|
+
@operand = operand
|
|
396
|
+
@whens = whens
|
|
397
|
+
@default = default
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
def when(value = nil, &block)
|
|
401
|
+
Pending.new(self, Case.argument(:when, value, block))
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
# Kernel#then is on every object, so `then` in the wrong place would be
|
|
405
|
+
# answered by it -- with no block, silently, with an Enumerator.
|
|
406
|
+
def then(*)
|
|
407
|
+
raise ArgumentError, "then follows a when, and there is none to follow here"
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
def else(value = nil, &block)
|
|
411
|
+
Case.new(operand, whens, Case.argument(:else, value, block))
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
def to_arel(table, model)
|
|
415
|
+
raise ArgumentError, "case needs a when before it means anything" if whens.empty?
|
|
416
|
+
|
|
417
|
+
node = operand ? Arel::Nodes::Case.new(to_arel_operand(operand, table, model))
|
|
418
|
+
: Arel::Nodes::Case.new
|
|
419
|
+
whens.each do |condition, result|
|
|
420
|
+
node.when(to_arel_argument(condition, table, model)).
|
|
421
|
+
then(to_arel_argument(result, table, model))
|
|
422
|
+
end
|
|
423
|
+
node.else(to_arel_argument(default, table, model)) unless default.equal?(NOTHING)
|
|
424
|
+
node
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
# A value or a block, and exactly one of them: the block is what makes
|
|
428
|
+
# `when { :age >= 60 }` read like the blocks around it, and the value is
|
|
429
|
+
# what makes `when(10)` possible at all.
|
|
430
|
+
def self.argument(name, value, block)
|
|
431
|
+
if block
|
|
432
|
+
raise ArgumentError, "#{name} takes a value or a block, not both" unless value.nil?
|
|
433
|
+
return block.call
|
|
434
|
+
end
|
|
435
|
+
raise ArgumentError, "#{name} needs a value or a block" if value.nil?
|
|
436
|
+
value
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
# What a `when` is until its `then` arrives. A Node so that using it
|
|
440
|
+
# as one says what is missing rather than reaching ActiveRecord as
|
|
441
|
+
# something it cannot read.
|
|
442
|
+
class Pending < Node
|
|
443
|
+
def initialize(kase, condition)
|
|
444
|
+
@kase = kase
|
|
445
|
+
@condition = condition
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
def then(value = nil, &block)
|
|
449
|
+
Case.new(@kase.operand,
|
|
450
|
+
@kase.whens + [[@condition, Case.argument(:then, value, block)]],
|
|
451
|
+
@kase.default)
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
def to_arel(_table, _model)
|
|
455
|
+
raise ArgumentError, "when needs a matching then"
|
|
456
|
+
end
|
|
457
|
+
end
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
# A path into a JSON document, spelled the two ways the adapters want it.
|
|
461
|
+
# Shared, because reading a value and setting one walk the same path.
|
|
462
|
+
module JsonSteps
|
|
463
|
+
def check_steps(path, called)
|
|
464
|
+
raise ArgumentError, "#{called} needs a key or an index" if path.empty?
|
|
465
|
+
path.each do |step|
|
|
466
|
+
next if step.is_a?(::Integer) || step.is_a?(::String) || step.is_a?(::Symbol)
|
|
467
|
+
raise ArgumentError, "a step is a key or an array index, not #{step.inspect}"
|
|
468
|
+
end
|
|
469
|
+
path
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
# PostgreSQL takes the steps as a text array, where every element is
|
|
473
|
+
# quoted so that a comma or a brace in a key is part of it.
|
|
474
|
+
def steps_array
|
|
475
|
+
"{#{path.map {|step| %("#{escape_step(step)}") }.join(',')}}"
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
# MySQL and SQLite take a path expression instead, where an integer is
|
|
479
|
+
# a subscript and a name that is not plain has to be quoted.
|
|
480
|
+
def dollar_path
|
|
481
|
+
path.inject(+'$') do |so_far, step|
|
|
482
|
+
next so_far << "[#{step}]" if step.is_a?(::Integer)
|
|
483
|
+
name = step.to_s
|
|
484
|
+
so_far << '.' << (name.match?(/\A[[:alpha:]_][[:alnum:]_]*\z/) ?
|
|
485
|
+
name : %("#{escape_step(step)}"))
|
|
486
|
+
end
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
def escape_step(step)
|
|
490
|
+
step.to_s.gsub('\\', '\\\\').gsub('"', '\\"')
|
|
491
|
+
end
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
# Reading inside a JSON document. Every adapter can do it and no two
|
|
495
|
+
# spell it alike: PostgreSQL walks an array of steps, SQLite has the
|
|
496
|
+
# operators with a $ path, and MySQL has the functions -- which is what
|
|
497
|
+
# this uses for that family, since MariaDB answers to the same adapter
|
|
498
|
+
# and has no -> at all.
|
|
499
|
+
#
|
|
500
|
+
# The path is turned into a string either way, so a key with a space or
|
|
501
|
+
# a quote in it travels as itself rather than having to be refused.
|
|
502
|
+
class JsonPath < Node
|
|
503
|
+
include Predications
|
|
504
|
+
include Arithmetics
|
|
505
|
+
include JsonSteps
|
|
506
|
+
|
|
507
|
+
attr_reader :operand, :path, :as_json
|
|
508
|
+
|
|
509
|
+
def initialize(operand, path, as_json: false)
|
|
510
|
+
@operand = operand
|
|
511
|
+
@path = check_steps(path, 'dig')
|
|
512
|
+
@as_json = as_json
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
def to_arel(table, model)
|
|
516
|
+
document = to_arel_operand(operand, table, model)
|
|
517
|
+
case AST.adapter_family(model)
|
|
518
|
+
when :postgresql
|
|
519
|
+
Arel::Nodes::InfixOperation.new(
|
|
520
|
+
as_json ? :"#>" : :"#>>", document, Arel::Nodes.build_quoted(steps_array))
|
|
521
|
+
when :mysql
|
|
522
|
+
extracted = Arel::Nodes::NamedFunction.new(
|
|
523
|
+
'JSON_EXTRACT', [document, Arel::Nodes.build_quoted(dollar_path)])
|
|
524
|
+
as_json ? extracted : Arel::Nodes::NamedFunction.new('JSON_UNQUOTE', [extracted])
|
|
525
|
+
else
|
|
526
|
+
extracted = Arel::Nodes::InfixOperation.new(
|
|
527
|
+
as_json ? :"->" : :"->>", document, Arel::Nodes.build_quoted(dollar_path))
|
|
528
|
+
# SQLite's ->> gives back the value with its type, where the other
|
|
529
|
+
# two give text. Cast so that `dig(:n) == '5'` means the same
|
|
530
|
+
# thing everywhere, and a number wants a cast everywhere too.
|
|
531
|
+
as_json ? extracted : Arel::Nodes::NamedFunction.new(
|
|
532
|
+
'CAST', [Arel::Nodes::As.new(extracted, Arel::Nodes::SqlLiteral.new('text'))])
|
|
533
|
+
end
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
# Setting a value inside a JSON document, which is what bury does to what
|
|
539
|
+
# dig reads. The document comes back changed rather than being written
|
|
540
|
+
# anywhere; update_all is what writes it.
|
|
541
|
+
class JsonSet < Node
|
|
542
|
+
include Predications
|
|
543
|
+
include JsonSteps
|
|
544
|
+
|
|
545
|
+
attr_reader :operand, :path, :value
|
|
546
|
+
|
|
547
|
+
def initialize(operand, path, value)
|
|
548
|
+
@operand = operand
|
|
549
|
+
@path = check_steps(path, 'bury')
|
|
550
|
+
@value = value
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
def to_arel(table, model)
|
|
554
|
+
document = to_arel_operand(operand, table, model)
|
|
555
|
+
if AST.adapter_family(model) == :postgresql
|
|
556
|
+
Arel::Nodes::NamedFunction.new(
|
|
557
|
+
'jsonb_set',
|
|
558
|
+
[document, Arel::Nodes.build_quoted(steps_array), postgresql_value(table, model)])
|
|
559
|
+
else
|
|
560
|
+
Arel::Nodes::NamedFunction.new(
|
|
561
|
+
'JSON_SET',
|
|
562
|
+
[document, Arel::Nodes.build_quoted(dollar_path), other_value(table, model)])
|
|
563
|
+
end
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
private
|
|
567
|
+
|
|
568
|
+
# jsonb_set takes jsonb, so an expression is turned into it and a Ruby
|
|
569
|
+
# value goes in as the JSON that says it -- '"x"' rather than 'x',
|
|
570
|
+
# which is not a document at all.
|
|
571
|
+
def postgresql_value(table, model)
|
|
572
|
+
return Arel::Nodes::NamedFunction.new(
|
|
573
|
+
'to_jsonb', [to_arel_operand(value, table, model)]) if expression?
|
|
574
|
+
Arel::Nodes.build_quoted(JSON.generate(value))
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
# The others take the value as it is, except a whole document, which
|
|
578
|
+
# they read out of a literal rather than take as a string. MySQL casts
|
|
579
|
+
# to JSON where MariaDB, which answers to the same adapter, does not.
|
|
580
|
+
def other_value(table, model)
|
|
581
|
+
return to_arel_operand(value, table, model) if expression?
|
|
582
|
+
return Arel::Nodes.build_quoted(value) unless value.is_a?(::Hash) || value.is_a?(::Array)
|
|
583
|
+
|
|
584
|
+
Arel::Nodes::NamedFunction.new(
|
|
585
|
+
'JSON_EXTRACT',
|
|
586
|
+
[Arel::Nodes.build_quoted(JSON.generate(value)), Arel::Nodes.build_quoted('$')])
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
def expression?
|
|
590
|
+
value.is_a?(Node) || value.is_a?(::Symbol)
|
|
591
|
+
end
|
|
592
|
+
end
|
|
593
|
+
|
|
594
|
+
# JSON containment: whether the document holds what is given.
|
|
595
|
+
class JsonContains < Predicate
|
|
596
|
+
attr_reader :operand, :value
|
|
597
|
+
|
|
598
|
+
def initialize(operand, value)
|
|
599
|
+
@operand = operand
|
|
600
|
+
@value = value
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
def to_arel(table, model)
|
|
604
|
+
document = to_arel_operand(operand, table, model)
|
|
605
|
+
json = Arel::Nodes.build_quoted(JSON.generate(value))
|
|
606
|
+
case AST.adapter_family(model)
|
|
607
|
+
when :postgresql then Arel::Nodes::Contains.new(document, json)
|
|
608
|
+
when :mysql
|
|
609
|
+
Arel::Nodes::NamedFunction.new('JSON_CONTAINS', [document, json])
|
|
610
|
+
else
|
|
611
|
+
# Later than the others, since the adapter is only known here.
|
|
612
|
+
raise NotImplementedError,
|
|
613
|
+
"contains? has no equivalent on #{model.connection_db_config.adapter}"
|
|
614
|
+
end
|
|
615
|
+
end
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
# Whether a key is in the document. PostgreSQL has an operator for it,
|
|
619
|
+
# ?, which is also what a bind parameter looks like to several drivers;
|
|
620
|
+
# the function it is shorthand for says the same thing and survives.
|
|
621
|
+
class JsonHasKey < Predicate
|
|
622
|
+
attr_reader :operand, :key
|
|
623
|
+
|
|
624
|
+
def initialize(operand, key)
|
|
625
|
+
@operand = operand
|
|
626
|
+
@key = key
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
def to_arel(table, model)
|
|
630
|
+
document = to_arel_operand(operand, table, model)
|
|
631
|
+
name = Arel::Nodes.build_quoted(key.to_s)
|
|
632
|
+
path = Arel::Nodes.build_quoted("$.#{key}")
|
|
633
|
+
case AST.adapter_family(model)
|
|
634
|
+
when :postgresql
|
|
635
|
+
Arel::Nodes::NamedFunction.new('jsonb_exists', [document, name])
|
|
636
|
+
when :mysql
|
|
637
|
+
Arel::Nodes::NamedFunction.new(
|
|
638
|
+
'JSON_CONTAINS_PATH', [document, Arel::Nodes.build_quoted('one'), path])
|
|
639
|
+
else
|
|
640
|
+
Arel::Nodes::NamedFunction.new('json_type', [document, path]).not_eq(nil)
|
|
641
|
+
end
|
|
642
|
+
end
|
|
643
|
+
end
|
|
644
|
+
|
|
645
|
+
# GROUP BY GROUPING SETS / ROLLUP / CUBE: several groupings asked for at
|
|
646
|
+
# once, the totals of each coming back beside the rows. PostgreSQL has
|
|
647
|
+
# all three; the block raises for the others before it gets this far.
|
|
648
|
+
#
|
|
649
|
+
# Each set is a list of its own, so grouping_sets takes lists and rollup
|
|
650
|
+
# and cube take the columns themselves.
|
|
651
|
+
class GroupingSets < Node
|
|
652
|
+
KINDS = {
|
|
653
|
+
grouping_sets: Arel::Nodes::GroupingSet,
|
|
654
|
+
rollup: Arel::Nodes::RollUp,
|
|
655
|
+
cube: Arel::Nodes::Cube,
|
|
656
|
+
}.freeze
|
|
657
|
+
|
|
658
|
+
attr_reader :kind, :sets
|
|
659
|
+
|
|
660
|
+
def initialize(kind, sets)
|
|
661
|
+
raise ArgumentError, "#{kind} needs something to group by" if sets.empty?
|
|
662
|
+
@kind = kind
|
|
663
|
+
@sets = sets
|
|
664
|
+
end
|
|
665
|
+
|
|
666
|
+
def to_arel(table, model)
|
|
667
|
+
KINDS.fetch(kind).new(
|
|
668
|
+
if kind == :grouping_sets
|
|
669
|
+
sets.map do |set|
|
|
670
|
+
Arel::Nodes::GroupingElement.new(
|
|
671
|
+
Array(set).map {|column| to_arel_operand(column, table, model) })
|
|
672
|
+
end
|
|
673
|
+
else
|
|
674
|
+
sets.map {|column| to_arel_operand(column, table, model) }
|
|
675
|
+
end)
|
|
676
|
+
end
|
|
677
|
+
end
|
|
678
|
+
|
|
249
679
|
class Column < Node
|
|
250
680
|
include Predications
|
|
251
681
|
include Arithmetics
|
|
252
|
-
include Aggregations
|
|
253
682
|
|
|
254
683
|
attr_reader :table_name, :column_name
|
|
255
684
|
|
|
@@ -258,7 +687,7 @@ module ActiveRecord
|
|
|
258
687
|
@column_name = column_name
|
|
259
688
|
end
|
|
260
689
|
|
|
261
|
-
def to_arel(_table)
|
|
690
|
+
def to_arel(_table, _model)
|
|
262
691
|
Arel::Table.new(table_name)[column_name]
|
|
263
692
|
end
|
|
264
693
|
end
|
|
@@ -269,7 +698,6 @@ module ActiveRecord
|
|
|
269
698
|
class Arithmetic < Node
|
|
270
699
|
include Predications
|
|
271
700
|
include Arithmetics
|
|
272
|
-
include Aggregations
|
|
273
701
|
|
|
274
702
|
attr_reader :left, :operator, :right
|
|
275
703
|
|
|
@@ -279,19 +707,218 @@ module ActiveRecord
|
|
|
279
707
|
@right = right
|
|
280
708
|
end
|
|
281
709
|
|
|
282
|
-
def to_arel(table)
|
|
283
|
-
to_arel_operand(left, table).
|
|
284
|
-
public_send(operator, to_arel_operand(right, table))
|
|
710
|
+
def to_arel(table, model)
|
|
711
|
+
to_arel_operand(left, table, model).
|
|
712
|
+
public_send(operator, to_arel_operand(right, table, model))
|
|
713
|
+
end
|
|
714
|
+
end
|
|
715
|
+
|
|
716
|
+
# What the bitwise operators refuse. Both refusals are there because
|
|
717
|
+
# the same Ruby would otherwise mean different things per adapter: MySQL
|
|
718
|
+
# and SQLite take a boolean for the one bit it is stored as, so
|
|
719
|
+
# `published & active` would quietly be the AND it looks like, while
|
|
720
|
+
# PostgreSQL has no such operator and would say so.
|
|
721
|
+
module BitwiseOperands
|
|
722
|
+
private
|
|
723
|
+
|
|
724
|
+
def check_operand(operand, operator)
|
|
725
|
+
return operand unless operand.is_a?(Predicate)
|
|
726
|
+
raise ArgumentError,
|
|
727
|
+
"a condition cannot be an operand of #{operator}; " \
|
|
728
|
+
"& and | between conditions are AND and OR"
|
|
729
|
+
end
|
|
730
|
+
|
|
731
|
+
# Only the unqualified column can be checked, since that is the one
|
|
732
|
+
# the model is known to have.
|
|
733
|
+
def check_not_boolean(operand, operator, model)
|
|
734
|
+
return unless operand.is_a?(::Symbol)
|
|
735
|
+
return unless model.type_for_attribute(operand).type == :boolean
|
|
736
|
+
raise ArgumentError,
|
|
737
|
+
"#{operand.inspect} is a boolean column, which #{operator} does " \
|
|
738
|
+
"not take; #{operand.inspect}.true? is the condition"
|
|
739
|
+
end
|
|
740
|
+
end
|
|
741
|
+
|
|
742
|
+
# SQL's bitwise operators. Each parenthesises itself, which is what
|
|
743
|
+
# keeps Ruby's grouping: PostgreSQL gives & and | the same precedence
|
|
744
|
+
# and reads a | b & c from the left, where Ruby reads the & first.
|
|
745
|
+
class Bitwise < Node
|
|
746
|
+
include Predications
|
|
747
|
+
include Arithmetics
|
|
748
|
+
include BitwiseOperands
|
|
749
|
+
|
|
750
|
+
NODES = {
|
|
751
|
+
:& => Arel::Nodes::BitwiseAnd,
|
|
752
|
+
:| => Arel::Nodes::BitwiseOr,
|
|
753
|
+
:<< => Arel::Nodes::BitwiseShiftLeft,
|
|
754
|
+
:>> => Arel::Nodes::BitwiseShiftRight,
|
|
755
|
+
}.freeze
|
|
756
|
+
|
|
757
|
+
attr_reader :left, :operator, :right
|
|
758
|
+
|
|
759
|
+
def initialize(left, operator, right)
|
|
760
|
+
@left = left
|
|
761
|
+
@operator = operator
|
|
762
|
+
@right = check_operand(right, operator)
|
|
763
|
+
end
|
|
764
|
+
|
|
765
|
+
def to_arel(table, model)
|
|
766
|
+
check_not_boolean(left, operator, model)
|
|
767
|
+
check_not_boolean(right, operator, model)
|
|
768
|
+
arel_left = to_arel_operand(left, table, model)
|
|
769
|
+
arel_right = to_arel_argument(right, table, model)
|
|
770
|
+
Arel::Nodes::Grouping.new(
|
|
771
|
+
if operator == :^
|
|
772
|
+
xor(arel_left, arel_right, model)
|
|
773
|
+
else
|
|
774
|
+
NODES.fetch(operator).new(arel_left, arel_right)
|
|
775
|
+
end)
|
|
776
|
+
end
|
|
777
|
+
|
|
778
|
+
private
|
|
779
|
+
|
|
780
|
+
# Arel has a node for XOR, but it writes ^ on every adapter, and ^ is
|
|
781
|
+
# exponentiation to PostgreSQL -- a wrong answer rather than an error.
|
|
782
|
+
# PostgreSQL's own spelling, #, is where a comment starts on MySQL, so
|
|
783
|
+
# it cannot be the portable one either. SQLite has no XOR at all;
|
|
784
|
+
# (a | b) - (a & b) is it, at the cost of naming each operand twice.
|
|
785
|
+
def xor(left, right, model)
|
|
786
|
+
case AST.adapter_family(model)
|
|
787
|
+
when :postgresql then Arel::Nodes::InfixOperation.new('#', left, right)
|
|
788
|
+
when :mysql then Arel::Nodes::BitwiseXor.new(left, right)
|
|
789
|
+
else
|
|
790
|
+
Arel::Nodes::Subtraction.new(
|
|
791
|
+
Arel::Nodes::Grouping.new(Arel::Nodes::BitwiseOr.new(left, right)),
|
|
792
|
+
Arel::Nodes::Grouping.new(Arel::Nodes::BitwiseAnd.new(left, right)))
|
|
793
|
+
end
|
|
794
|
+
end
|
|
795
|
+
end
|
|
796
|
+
|
|
797
|
+
# ~, which every adapter has. MySQL answers with the unsigned 64-bit
|
|
798
|
+
# number where the others answer with a negative one; the bits are the
|
|
799
|
+
# same, and only reading the value back tells them apart.
|
|
800
|
+
class BitwiseNot < Node
|
|
801
|
+
include Predications
|
|
802
|
+
include Arithmetics
|
|
803
|
+
include BitwiseOperands
|
|
804
|
+
|
|
805
|
+
attr_reader :operand
|
|
806
|
+
|
|
807
|
+
def initialize(operand)
|
|
808
|
+
@operand = check_operand(operand, :~)
|
|
809
|
+
end
|
|
810
|
+
|
|
811
|
+
def to_arel(table, model)
|
|
812
|
+
check_not_boolean(operand, :~, model)
|
|
813
|
+
Arel::Nodes::Grouping.new(
|
|
814
|
+
Arel::Nodes::BitwiseNot.new(to_arel_operand(operand, table, model)))
|
|
815
|
+
end
|
|
816
|
+
end
|
|
817
|
+
|
|
818
|
+
# OVER, on the two things that can carry a window: an aggregate, and a
|
|
819
|
+
# function.
|
|
820
|
+
module Windowing
|
|
821
|
+
def over
|
|
822
|
+
Over.new(self)
|
|
823
|
+
end
|
|
824
|
+
end
|
|
825
|
+
|
|
826
|
+
# A function with a window. The window is built by chaining, the way
|
|
827
|
+
# Arel's own is, and each method returns a new node rather than adding to
|
|
828
|
+
# this one, so a window can be finished more than one way.
|
|
829
|
+
class Over < Node
|
|
830
|
+
include Predications
|
|
831
|
+
include Arithmetics
|
|
832
|
+
|
|
833
|
+
attr_reader :function, :partitions, :orders, :frame
|
|
834
|
+
|
|
835
|
+
def initialize(function, partitions = [], orders = [], frame = nil)
|
|
836
|
+
@function = function
|
|
837
|
+
@partitions = partitions
|
|
838
|
+
@orders = orders
|
|
839
|
+
@frame = frame
|
|
840
|
+
end
|
|
841
|
+
|
|
842
|
+
def partition(*exprs)
|
|
843
|
+
raise ArgumentError, "partition needs an expression" if exprs.empty?
|
|
844
|
+
Over.new(function, partitions + exprs, orders, frame)
|
|
845
|
+
end
|
|
846
|
+
|
|
847
|
+
def order(*exprs)
|
|
848
|
+
raise ArgumentError, "order needs an expression" if exprs.empty?
|
|
849
|
+
Over.new(function, partitions, orders + exprs, frame)
|
|
850
|
+
end
|
|
851
|
+
|
|
852
|
+
def rows(bounds)
|
|
853
|
+
Over.new(function, partitions, orders, framing(:rows, bounds))
|
|
854
|
+
end
|
|
855
|
+
|
|
856
|
+
def range(bounds)
|
|
857
|
+
Over.new(function, partitions, orders, framing(:range, bounds))
|
|
858
|
+
end
|
|
859
|
+
|
|
860
|
+
def to_arel(table, model)
|
|
861
|
+
window = Arel::Nodes::Window.new
|
|
862
|
+
partitions.each {|expr| window.partition(to_arel_operand(expr, table, model)) }
|
|
863
|
+
orders.each {|expr| window.order(to_arel_operand(expr, table, model)) }
|
|
864
|
+
frame_arel(window) if frame
|
|
865
|
+
|
|
866
|
+
# A window-only function refuses to build on its own; here is where
|
|
867
|
+
# it is asked for the call itself.
|
|
868
|
+
arel_function =
|
|
869
|
+
function.is_a?(WindowFunction) ? function.call_arel(table, model)
|
|
870
|
+
: function.to_arel(table, model)
|
|
871
|
+
Arel::Nodes::Over.new(arel_function, window)
|
|
872
|
+
end
|
|
873
|
+
|
|
874
|
+
private
|
|
875
|
+
|
|
876
|
+
# The frame is a range of rows counted from the current one: negative
|
|
877
|
+
# before it, positive after, 0 the row itself, and an open end for
|
|
878
|
+
# unbounded. `rows(..0)` is what a running total wants.
|
|
879
|
+
def framing(kind, bounds)
|
|
880
|
+
raise ArgumentError, "a window has one frame" if frame
|
|
881
|
+
unless bounds.is_a?(::Range)
|
|
882
|
+
raise ArgumentError, "#{kind} takes a range of rows, as in rows(..0)"
|
|
883
|
+
end
|
|
884
|
+
if bounds.exclude_end?
|
|
885
|
+
raise ArgumentError, "a frame ends on a row rather than before one; use .."
|
|
886
|
+
end
|
|
887
|
+
[bounds.begin, bounds.end].each do |bound|
|
|
888
|
+
next if bound.nil? || bound.is_a?(::Integer)
|
|
889
|
+
raise ArgumentError,
|
|
890
|
+
"a frame bound is a number of rows, or nothing for unbounded"
|
|
891
|
+
end
|
|
892
|
+
[kind, bounds.begin, bounds.end]
|
|
893
|
+
end
|
|
894
|
+
|
|
895
|
+
# Arel wants the keyword itself on the left of the BETWEEN, which is
|
|
896
|
+
# what window.rows with no argument hands back.
|
|
897
|
+
def frame_arel(window)
|
|
898
|
+
kind, from, to = frame
|
|
899
|
+
window.frame(
|
|
900
|
+
Arel::Nodes::Between.new(
|
|
901
|
+
window.public_send(kind),
|
|
902
|
+
Arel::Nodes::And.new([bound(from, Arel::Nodes::Preceding.new),
|
|
903
|
+
bound(to, Arel::Nodes::Following.new)])))
|
|
904
|
+
end
|
|
905
|
+
|
|
906
|
+
def bound(rows, unbounded)
|
|
907
|
+
return unbounded if rows.nil?
|
|
908
|
+
return Arel::Nodes::CurrentRow.new if rows.zero?
|
|
909
|
+
rows.negative? ? Arel::Nodes::Preceding.new(-rows)
|
|
910
|
+
: Arel::Nodes::Following.new(rows)
|
|
285
911
|
end
|
|
286
912
|
end
|
|
287
913
|
|
|
288
914
|
class Aggregate < Node
|
|
289
915
|
include Predications
|
|
290
916
|
include Arithmetics
|
|
917
|
+
include Windowing
|
|
291
918
|
|
|
292
|
-
attr_reader :operand, :function, :distinct
|
|
919
|
+
attr_reader :operand, :function, :distinct, :condition
|
|
293
920
|
|
|
294
|
-
def initialize(operand, function, distinct: false)
|
|
921
|
+
def initialize(operand, function, distinct: false, condition: nil)
|
|
295
922
|
if distinct && function != :count
|
|
296
923
|
raise ArgumentError, "#{function} does not take distinct"
|
|
297
924
|
end
|
|
@@ -301,10 +928,35 @@ module ActiveRecord
|
|
|
301
928
|
@operand = operand
|
|
302
929
|
@function = function
|
|
303
930
|
@distinct = distinct
|
|
931
|
+
@condition = condition
|
|
304
932
|
end
|
|
305
933
|
|
|
306
|
-
|
|
307
|
-
|
|
934
|
+
# FILTER (WHERE ...): the aggregate is taken over the rows the
|
|
935
|
+
# condition holds for. A value or a block, as `when` takes them.
|
|
936
|
+
def filter(condition = nil, &block)
|
|
937
|
+
Aggregate.new(operand, function, distinct: distinct,
|
|
938
|
+
condition: Case.argument(:filter, condition, block))
|
|
939
|
+
end
|
|
940
|
+
|
|
941
|
+
def to_arel(table, model)
|
|
942
|
+
return aggregate(operand, table, model) unless condition
|
|
943
|
+
|
|
944
|
+
# MySQL has no FILTER clause. An aggregate passes over a NULL, so
|
|
945
|
+
# the case that yields nothing for the rows the condition misses is
|
|
946
|
+
# the same aggregate over the same rows -- count(*) has no operand to
|
|
947
|
+
# keep, and counts a 1 instead.
|
|
948
|
+
if AST.adapter_family(model) == :mysql
|
|
949
|
+
kept = Case.new.when(condition).then(operand == :* ? 1 : operand)
|
|
950
|
+
return aggregate(kept, table, model)
|
|
951
|
+
end
|
|
952
|
+
|
|
953
|
+
aggregate(operand, table, model).filter(condition.to_arel(table, model))
|
|
954
|
+
end
|
|
955
|
+
|
|
956
|
+
private
|
|
957
|
+
|
|
958
|
+
def aggregate(over, table, model)
|
|
959
|
+
arel_operand = to_arel_operand(over, table, model)
|
|
308
960
|
if function == :count
|
|
309
961
|
arel_operand.count(distinct)
|
|
310
962
|
else
|
|
@@ -313,16 +965,37 @@ module ActiveRecord
|
|
|
313
965
|
end
|
|
314
966
|
end
|
|
315
967
|
|
|
968
|
+
# A column alias, quoted by the adapter, so that the name asked for is
|
|
969
|
+
# the name that comes back: unquoted, PostgreSQL folds a capital away
|
|
970
|
+
# and the other two keep it, which is one block meaning two things.
|
|
971
|
+
# Quoting also leaves nothing to refuse -- a name that would have been
|
|
972
|
+
# SQL is an identifier with a strange name instead.
|
|
973
|
+
#
|
|
974
|
+
# `quote: false` asks for the name as written, for a schema that wants
|
|
975
|
+
# the folding.
|
|
316
976
|
class As < Node
|
|
317
|
-
attr_reader :operand, :alias_name
|
|
977
|
+
attr_reader :operand, :alias_name, :quote
|
|
318
978
|
|
|
319
|
-
def initialize(operand, alias_name)
|
|
979
|
+
def initialize(operand, alias_name, quote: true)
|
|
980
|
+
# Checked here rather than where the SQL is built, so that a name
|
|
981
|
+
# the adapter is not being asked to quote is refused where it was
|
|
982
|
+
# written.
|
|
983
|
+
AST.check_name(alias_name, ALIAS_NAME, "column alias") unless quote
|
|
320
984
|
@operand = operand
|
|
321
|
-
@alias_name =
|
|
985
|
+
@alias_name = alias_name
|
|
986
|
+
@quote = quote
|
|
322
987
|
end
|
|
323
988
|
|
|
324
|
-
def to_arel(table)
|
|
325
|
-
to_arel_operand(operand, table).as(
|
|
989
|
+
def to_arel(table, model)
|
|
990
|
+
to_arel_operand(operand, table, model).as(alias_sql(model))
|
|
991
|
+
end
|
|
992
|
+
|
|
993
|
+
private
|
|
994
|
+
|
|
995
|
+
def alias_sql(model)
|
|
996
|
+
name = alias_name.to_s
|
|
997
|
+
return name unless quote
|
|
998
|
+
model.with_connection {|connection| connection.quote_column_name(name) }
|
|
326
999
|
end
|
|
327
1000
|
end
|
|
328
1001
|
|
|
@@ -345,8 +1018,8 @@ module ActiveRecord
|
|
|
345
1018
|
Ordering.new(operand, direction, :nulls_last)
|
|
346
1019
|
end
|
|
347
1020
|
|
|
348
|
-
def to_arel(table)
|
|
349
|
-
ordering = to_arel_operand(operand, table).public_send(direction)
|
|
1021
|
+
def to_arel(table, model)
|
|
1022
|
+
ordering = to_arel_operand(operand, table, model).public_send(direction)
|
|
350
1023
|
nulls ? ordering.public_send(nulls) : ordering
|
|
351
1024
|
end
|
|
352
1025
|
end
|
|
@@ -354,6 +1027,7 @@ module ActiveRecord
|
|
|
354
1027
|
class Function < Node
|
|
355
1028
|
include Predications
|
|
356
1029
|
include Arithmetics
|
|
1030
|
+
include Windowing
|
|
357
1031
|
|
|
358
1032
|
attr_reader :name, :args
|
|
359
1033
|
|
|
@@ -362,12 +1036,23 @@ module ActiveRecord
|
|
|
362
1036
|
@args = args
|
|
363
1037
|
end
|
|
364
1038
|
|
|
365
|
-
def to_arel(table)
|
|
366
|
-
arel_args = args.map {|arg| to_arel_argument(arg, table) }
|
|
1039
|
+
def to_arel(table, model)
|
|
1040
|
+
arel_args = args.map {|arg| to_arel_argument(arg, table, model) }
|
|
367
1041
|
Arel::Nodes::NamedFunction.new(name, arel_args)
|
|
368
1042
|
end
|
|
369
1043
|
end
|
|
370
1044
|
|
|
1045
|
+
# ROW_NUMBER and its kind: functions that say nothing without a window.
|
|
1046
|
+
# On its own this refuses rather than reaching the database as an error
|
|
1047
|
+
# there; over asks it for call_arel instead.
|
|
1048
|
+
class WindowFunction < Function
|
|
1049
|
+
alias_method :call_arel, :to_arel
|
|
1050
|
+
|
|
1051
|
+
def to_arel(_table, _model)
|
|
1052
|
+
raise ArgumentError, "#{name.downcase} is a window function; it needs over"
|
|
1053
|
+
end
|
|
1054
|
+
end
|
|
1055
|
+
|
|
371
1056
|
# EXTRACT(field FROM expr). The field is grammar rather than a value --
|
|
372
1057
|
# a keyword the adapter reads bare -- so it has to be a plain name,
|
|
373
1058
|
# which Arel upcases on the way out.
|
|
@@ -382,8 +1067,8 @@ module ActiveRecord
|
|
|
382
1067
|
@operand = operand
|
|
383
1068
|
end
|
|
384
1069
|
|
|
385
|
-
def to_arel(table)
|
|
386
|
-
Arel::Nodes::Extract.new(to_arel_argument(operand, table), field.to_s)
|
|
1070
|
+
def to_arel(table, model)
|
|
1071
|
+
Arel::Nodes::Extract.new(to_arel_argument(operand, table, model), field.to_s)
|
|
387
1072
|
end
|
|
388
1073
|
end
|
|
389
1074
|
|
|
@@ -402,10 +1087,10 @@ module ActiveRecord
|
|
|
402
1087
|
@sql_type = AST.check_name(sql_type, TYPE_NAME, "SQL type")
|
|
403
1088
|
end
|
|
404
1089
|
|
|
405
|
-
def to_arel(table)
|
|
1090
|
+
def to_arel(table, model)
|
|
406
1091
|
Arel::Nodes::NamedFunction.new(
|
|
407
1092
|
"CAST",
|
|
408
|
-
[Arel::Nodes::As.new(to_arel_argument(operand, table),
|
|
1093
|
+
[Arel::Nodes::As.new(to_arel_argument(operand, table, model),
|
|
409
1094
|
Arel::Nodes::SqlLiteral.new(sql_type.to_s))])
|
|
410
1095
|
end
|
|
411
1096
|
end
|
|
@@ -431,7 +1116,7 @@ module ActiveRecord
|
|
|
431
1116
|
@precision = precision
|
|
432
1117
|
end
|
|
433
1118
|
|
|
434
|
-
def to_arel(_table)
|
|
1119
|
+
def to_arel(_table, _model)
|
|
435
1120
|
Arel::Nodes::SqlLiteral.new(
|
|
436
1121
|
precision ? "#{name}(#{precision})" : name)
|
|
437
1122
|
end
|
|
@@ -454,11 +1139,11 @@ module ActiveRecord
|
|
|
454
1139
|
@value = value
|
|
455
1140
|
end
|
|
456
1141
|
|
|
457
|
-
def to_arel(table)
|
|
458
|
-
arel_column = to_arel_operand(column, table)
|
|
1142
|
+
def to_arel(table, model)
|
|
1143
|
+
arel_column = to_arel_operand(column, table, model)
|
|
459
1144
|
arel_value =
|
|
460
1145
|
case value
|
|
461
|
-
when Node then value.to_arel(table)
|
|
1146
|
+
when Node then value.to_arel(table, model)
|
|
462
1147
|
when ActiveRecord::Relation then scalar_subquery(value)
|
|
463
1148
|
else value
|
|
464
1149
|
end
|
|
@@ -479,39 +1164,38 @@ module ActiveRecord
|
|
|
479
1164
|
end
|
|
480
1165
|
end
|
|
481
1166
|
|
|
482
|
-
#
|
|
483
|
-
#
|
|
484
|
-
class
|
|
485
|
-
attr_reader :operand, :
|
|
1167
|
+
# IS TRUE, IS FALSE and their negations, which every adapter spells the
|
|
1168
|
+
# same way and answers alike, NULL included.
|
|
1169
|
+
class TruthValue < Predicate
|
|
1170
|
+
attr_reader :operand, :value, :negated
|
|
486
1171
|
|
|
487
|
-
def initialize(operand,
|
|
1172
|
+
def initialize(operand, value, negated: false)
|
|
488
1173
|
@operand = operand
|
|
489
|
-
@
|
|
1174
|
+
@value = value
|
|
1175
|
+
@negated = negated
|
|
490
1176
|
end
|
|
491
1177
|
|
|
492
|
-
def to_arel(table)
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
when ActiveRecord::Relation then arel_operand.in(subquery(values))
|
|
497
|
-
else arel_operand.in(values)
|
|
498
|
-
end
|
|
1178
|
+
def to_arel(table, model)
|
|
1179
|
+
literal = value ? Arel::Nodes::True.new : Arel::Nodes::False.new
|
|
1180
|
+
Arel::Nodes::InfixOperation.new(negated ? 'IS NOT' : 'IS',
|
|
1181
|
+
to_arel_operand(operand, table, model), literal)
|
|
499
1182
|
end
|
|
1183
|
+
end
|
|
500
1184
|
|
|
1185
|
+
# A relation standing for a set of values, which is what IN and the
|
|
1186
|
+
# quantifiers each take. The treatment is ActiveRecord's own
|
|
1187
|
+
# RelationHandler's: without an explicit select list the subquery
|
|
1188
|
+
# selects the model's primary key.
|
|
1189
|
+
module SetSubquery
|
|
501
1190
|
private
|
|
502
1191
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
# subquery selects the model's primary key.
|
|
506
|
-
def subquery(relation)
|
|
507
|
-
if relation.eager_loading?
|
|
508
|
-
relation = relation.send(:apply_join_dependency)
|
|
509
|
-
end
|
|
1192
|
+
def set_subquery(relation, spelling)
|
|
1193
|
+
relation = relation.send(:apply_join_dependency) if relation.eager_loading?
|
|
510
1194
|
if relation.select_values.empty?
|
|
511
1195
|
model = relation.model
|
|
512
1196
|
if model.composite_primary_key?
|
|
513
1197
|
raise ArgumentError,
|
|
514
|
-
"Cannot map composite primary key #{model.primary_key} to
|
|
1198
|
+
"Cannot map composite primary key #{model.primary_key} to #{spelling}"
|
|
515
1199
|
end
|
|
516
1200
|
relation = relation.select(relation.table[model.primary_key])
|
|
517
1201
|
end
|
|
@@ -519,6 +1203,55 @@ module ActiveRecord
|
|
|
519
1203
|
end
|
|
520
1204
|
end
|
|
521
1205
|
|
|
1206
|
+
# IN for a list of values, BETWEEN for a range, IN (SELECT ...) for a
|
|
1207
|
+
# relation.
|
|
1208
|
+
class In < Predicate
|
|
1209
|
+
include SetSubquery
|
|
1210
|
+
|
|
1211
|
+
attr_reader :operand, :values, :negated
|
|
1212
|
+
|
|
1213
|
+
def initialize(operand, values, negated: false)
|
|
1214
|
+
@operand = operand
|
|
1215
|
+
@values = values
|
|
1216
|
+
@negated = negated
|
|
1217
|
+
end
|
|
1218
|
+
|
|
1219
|
+
def to_arel(table, model)
|
|
1220
|
+
arel_operand = to_arel_operand(operand, table, model)
|
|
1221
|
+
if values.is_a?(Range)
|
|
1222
|
+
arel_operand.public_send(negated ? :not_between : :between, values)
|
|
1223
|
+
else
|
|
1224
|
+
arg = values.is_a?(ActiveRecord::Relation) ? set_subquery(values, 'IN') : values
|
|
1225
|
+
arel_operand.public_send(negated ? :not_in : :in, arg)
|
|
1226
|
+
end
|
|
1227
|
+
end
|
|
1228
|
+
end
|
|
1229
|
+
|
|
1230
|
+
# ANY and ALL, which stand on the right of a comparison and say how many
|
|
1231
|
+
# of the subquery's rows have to satisfy it. Where a scalar subquery
|
|
1232
|
+
# has to return one row, these take as many as come.
|
|
1233
|
+
class Quantified < Node
|
|
1234
|
+
include SetSubquery
|
|
1235
|
+
|
|
1236
|
+
attr_reader :kind, :relation
|
|
1237
|
+
|
|
1238
|
+
def initialize(kind, relation)
|
|
1239
|
+
unless relation.is_a?(ActiveRecord::Relation)
|
|
1240
|
+
raise ArgumentError,
|
|
1241
|
+
"#{kind} takes a relation as its subquery; a list is what in? takes"
|
|
1242
|
+
end
|
|
1243
|
+
@kind = kind
|
|
1244
|
+
@relation = relation
|
|
1245
|
+
end
|
|
1246
|
+
|
|
1247
|
+
# The subquery goes in as its own AST rather than as the manager,
|
|
1248
|
+
# which would parenthesise it a second time -- and to PostgreSQL
|
|
1249
|
+
# `ANY ((SELECT ...))` is ANY of one scalar, which it refuses.
|
|
1250
|
+
def to_arel(_table, _model)
|
|
1251
|
+
Arel::Nodes::NamedFunction.new(kind, [set_subquery(relation, kind).ast])
|
|
1252
|
+
end
|
|
1253
|
+
end
|
|
1254
|
+
|
|
522
1255
|
# EXISTS (SELECT ...) for a relation. Correlate the subquery with the
|
|
523
1256
|
# outer table through qualified columns. EXISTS only asks whether a row
|
|
524
1257
|
# comes back, so unlike In there is no select list to fix up.
|
|
@@ -529,7 +1262,7 @@ module ActiveRecord
|
|
|
529
1262
|
@relation = relation
|
|
530
1263
|
end
|
|
531
1264
|
|
|
532
|
-
def to_arel(_table)
|
|
1265
|
+
def to_arel(_table, _model)
|
|
533
1266
|
subquery = relation
|
|
534
1267
|
if subquery.eager_loading?
|
|
535
1268
|
subquery = subquery.send(:apply_join_dependency)
|
|
@@ -555,19 +1288,23 @@ module ActiveRecord
|
|
|
555
1288
|
inject {|left, right| Or.new(left, right) }
|
|
556
1289
|
end
|
|
557
1290
|
|
|
558
|
-
attr_reader :operand, :pattern, :escape, :case_sensitive
|
|
1291
|
+
attr_reader :operand, :pattern, :escape, :case_sensitive, :negated
|
|
559
1292
|
|
|
560
|
-
def initialize(operand, pattern, escape = nil, case_sensitive: true
|
|
1293
|
+
def initialize(operand, pattern, escape = nil, case_sensitive: true,
|
|
1294
|
+
negated: false)
|
|
561
1295
|
@operand = operand
|
|
562
1296
|
@pattern = pattern
|
|
563
1297
|
@escape = escape
|
|
564
1298
|
@case_sensitive = case_sensitive
|
|
1299
|
+
@negated = negated
|
|
565
1300
|
end
|
|
566
1301
|
|
|
567
|
-
def to_arel(table)
|
|
1302
|
+
def to_arel(table, model)
|
|
568
1303
|
# Arel matches case-insensitively unless told otherwise, which is
|
|
569
1304
|
# what picks ILIKE over LIKE on PostgreSQL.
|
|
570
|
-
to_arel_operand(operand, table
|
|
1305
|
+
to_arel_operand(operand, table, model).
|
|
1306
|
+
public_send(negated ? :does_not_match : :matches,
|
|
1307
|
+
pattern, escape, case_sensitive)
|
|
571
1308
|
end
|
|
572
1309
|
end
|
|
573
1310
|
|
|
@@ -583,9 +1320,9 @@ module ActiveRecord
|
|
|
583
1320
|
@negated = negated
|
|
584
1321
|
end
|
|
585
1322
|
|
|
586
|
-
def to_arel(table)
|
|
587
|
-
arel_operand = to_arel_operand(operand, table)
|
|
588
|
-
arel_value = value.is_a?(Node) ? value.to_arel(table) : value
|
|
1323
|
+
def to_arel(table, model)
|
|
1324
|
+
arel_operand = to_arel_operand(operand, table, model)
|
|
1325
|
+
arel_value = value.is_a?(Node) ? value.to_arel(table, model) : value
|
|
589
1326
|
if negated
|
|
590
1327
|
arel_operand.is_distinct_from(arel_value)
|
|
591
1328
|
else
|
|
@@ -621,8 +1358,8 @@ module ActiveRecord
|
|
|
621
1358
|
@elements = elements
|
|
622
1359
|
end
|
|
623
1360
|
|
|
624
|
-
def to_arel(table)
|
|
625
|
-
arel_operand = to_arel_operand(operand, table)
|
|
1361
|
+
def to_arel(table, model)
|
|
1362
|
+
arel_operand = to_arel_operand(operand, table, model)
|
|
626
1363
|
quoted = Arel::Nodes.build_quoted(array_literal)
|
|
627
1364
|
case operator
|
|
628
1365
|
when :"@>" then Arel::Nodes::Contains.new(arel_operand, quoted)
|
|
@@ -660,8 +1397,8 @@ module ActiveRecord
|
|
|
660
1397
|
@negated = negated
|
|
661
1398
|
end
|
|
662
1399
|
|
|
663
|
-
def to_arel(table)
|
|
664
|
-
arel_operand = to_arel_operand(operand, table)
|
|
1400
|
+
def to_arel(table, model)
|
|
1401
|
+
arel_operand = to_arel_operand(operand, table, model)
|
|
665
1402
|
if negated
|
|
666
1403
|
arel_operand.does_not_match_regexp(pattern)
|
|
667
1404
|
else
|
|
@@ -693,8 +1430,8 @@ module ActiveRecord
|
|
|
693
1430
|
@right = right
|
|
694
1431
|
end
|
|
695
1432
|
|
|
696
|
-
def to_arel(table)
|
|
697
|
-
left.to_arel(table).and(right.to_arel(table))
|
|
1433
|
+
def to_arel(table, model)
|
|
1434
|
+
left.to_arel(table, model).and(right.to_arel(table, model))
|
|
698
1435
|
end
|
|
699
1436
|
end
|
|
700
1437
|
|
|
@@ -706,8 +1443,8 @@ module ActiveRecord
|
|
|
706
1443
|
@right = right
|
|
707
1444
|
end
|
|
708
1445
|
|
|
709
|
-
def to_arel(table)
|
|
710
|
-
left.to_arel(table).or(right.to_arel(table))
|
|
1446
|
+
def to_arel(table, model)
|
|
1447
|
+
left.to_arel(table, model).or(right.to_arel(table, model))
|
|
711
1448
|
end
|
|
712
1449
|
end
|
|
713
1450
|
|
|
@@ -718,8 +1455,8 @@ module ActiveRecord
|
|
|
718
1455
|
@operand = operand
|
|
719
1456
|
end
|
|
720
1457
|
|
|
721
|
-
def to_arel(table)
|
|
722
|
-
Arel::Nodes::Not.new(operand.to_arel(table))
|
|
1458
|
+
def to_arel(table, model)
|
|
1459
|
+
Arel::Nodes::Not.new(operand.to_arel(table, model))
|
|
723
1460
|
end
|
|
724
1461
|
end
|
|
725
1462
|
end
|