activerecord-refined 0.5.1 → 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 +161 -25
- data/.github/workflows/test.yml +15 -2
- data/README.md +412 -49
- data/examples/ctes.rb +3 -3
- 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 +776 -92
- data/lib/active_record/refined.rb +317 -26
- data/lib/activerecord-refined/version.rb +1 -1
- data/lib/activerecord-refined.rb +5 -2
- data/test/test_block_syntax.rb +949 -60
- 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}"
|
|
@@ -75,6 +89,27 @@ module ActiveRecord
|
|
|
75
89
|
Comparison.new(self, :!=, nil)
|
|
76
90
|
end
|
|
77
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
|
+
|
|
78
113
|
def in?(values)
|
|
79
114
|
In.new(self, values)
|
|
80
115
|
end
|
|
@@ -91,6 +126,13 @@ module ActiveRecord
|
|
|
91
126
|
In.new(self, min..max, negated: true)
|
|
92
127
|
end
|
|
93
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
|
+
|
|
94
136
|
def like?(pattern)
|
|
95
137
|
Like.new(self, pattern)
|
|
96
138
|
end
|
|
@@ -168,6 +210,39 @@ module ActiveRecord
|
|
|
168
210
|
def intersect?(elements)
|
|
169
211
|
ArrayPredicate.new(self, :"&&", ArrayPredicate.elements(elements, "intersect?"))
|
|
170
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
|
|
171
246
|
end
|
|
172
247
|
|
|
173
248
|
# Arithmetic builders shared by symbols, qualified columns and
|
|
@@ -189,41 +264,48 @@ module ActiveRecord
|
|
|
189
264
|
def /(other)
|
|
190
265
|
Arithmetic.new(self, :/, other)
|
|
191
266
|
end
|
|
192
|
-
end
|
|
193
267
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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)
|
|
201
278
|
end
|
|
202
279
|
|
|
203
|
-
def
|
|
204
|
-
|
|
280
|
+
def ^(other)
|
|
281
|
+
Bitwise.new(self, :^, other)
|
|
205
282
|
end
|
|
206
283
|
|
|
207
|
-
def
|
|
208
|
-
|
|
284
|
+
def <<(other)
|
|
285
|
+
Bitwise.new(self, :<<, other)
|
|
209
286
|
end
|
|
210
287
|
|
|
211
|
-
def
|
|
212
|
-
|
|
288
|
+
def >>(other)
|
|
289
|
+
Bitwise.new(self, :>>, other)
|
|
213
290
|
end
|
|
214
291
|
|
|
215
|
-
def
|
|
216
|
-
|
|
292
|
+
def ~
|
|
293
|
+
BitwiseNot.new(self)
|
|
217
294
|
end
|
|
218
295
|
end
|
|
219
296
|
|
|
220
297
|
class Node
|
|
221
|
-
|
|
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)
|
|
222
304
|
raise ScriptError, "subclass must override this method"
|
|
223
305
|
end
|
|
224
306
|
|
|
225
|
-
def as(alias_name)
|
|
226
|
-
As.new(self, alias_name)
|
|
307
|
+
def as(alias_name, quote: true)
|
|
308
|
+
As.new(self, alias_name, quote: quote)
|
|
227
309
|
end
|
|
228
310
|
|
|
229
311
|
def asc
|
|
@@ -237,9 +319,9 @@ module ActiveRecord
|
|
|
237
319
|
private
|
|
238
320
|
|
|
239
321
|
# Resolves an operand denoting a column or an expression.
|
|
240
|
-
def to_arel_operand(operand, table)
|
|
322
|
+
def to_arel_operand(operand, table, model)
|
|
241
323
|
case operand
|
|
242
|
-
when Node then operand.to_arel(table)
|
|
324
|
+
when Node then operand.to_arel(table, model)
|
|
243
325
|
when :* then Arel.star
|
|
244
326
|
when Symbol then table[operand]
|
|
245
327
|
else operand
|
|
@@ -248,9 +330,9 @@ module ActiveRecord
|
|
|
248
330
|
|
|
249
331
|
# Resolves a function argument: a column or an expression as above,
|
|
250
332
|
# anything else a value to be quoted.
|
|
251
|
-
def to_arel_argument(arg, table)
|
|
333
|
+
def to_arel_argument(arg, table, model)
|
|
252
334
|
case arg
|
|
253
|
-
when Node, Symbol then to_arel_operand(arg, table)
|
|
335
|
+
when Node, Symbol then to_arel_operand(arg, table, model)
|
|
254
336
|
else Arel::Nodes.build_quoted(arg)
|
|
255
337
|
end
|
|
256
338
|
end
|
|
@@ -280,7 +362,6 @@ module ActiveRecord
|
|
|
280
362
|
class Value < Node
|
|
281
363
|
include Predications
|
|
282
364
|
include Arithmetics
|
|
283
|
-
include Aggregations
|
|
284
365
|
|
|
285
366
|
attr_reader :value
|
|
286
367
|
|
|
@@ -288,15 +369,316 @@ module ActiveRecord
|
|
|
288
369
|
@value = value
|
|
289
370
|
end
|
|
290
371
|
|
|
291
|
-
def to_arel(_table)
|
|
372
|
+
def to_arel(_table, _model)
|
|
292
373
|
Arel::Nodes.build_quoted(value)
|
|
293
374
|
end
|
|
294
375
|
end
|
|
295
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
|
+
|
|
296
679
|
class Column < Node
|
|
297
680
|
include Predications
|
|
298
681
|
include Arithmetics
|
|
299
|
-
include Aggregations
|
|
300
682
|
|
|
301
683
|
attr_reader :table_name, :column_name
|
|
302
684
|
|
|
@@ -305,7 +687,7 @@ module ActiveRecord
|
|
|
305
687
|
@column_name = column_name
|
|
306
688
|
end
|
|
307
689
|
|
|
308
|
-
def to_arel(_table)
|
|
690
|
+
def to_arel(_table, _model)
|
|
309
691
|
Arel::Table.new(table_name)[column_name]
|
|
310
692
|
end
|
|
311
693
|
end
|
|
@@ -316,7 +698,6 @@ module ActiveRecord
|
|
|
316
698
|
class Arithmetic < Node
|
|
317
699
|
include Predications
|
|
318
700
|
include Arithmetics
|
|
319
|
-
include Aggregations
|
|
320
701
|
|
|
321
702
|
attr_reader :left, :operator, :right
|
|
322
703
|
|
|
@@ -326,19 +707,218 @@ module ActiveRecord
|
|
|
326
707
|
@right = right
|
|
327
708
|
end
|
|
328
709
|
|
|
329
|
-
def to_arel(table)
|
|
330
|
-
to_arel_operand(left, table).
|
|
331
|
-
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)
|
|
332
911
|
end
|
|
333
912
|
end
|
|
334
913
|
|
|
335
914
|
class Aggregate < Node
|
|
336
915
|
include Predications
|
|
337
916
|
include Arithmetics
|
|
917
|
+
include Windowing
|
|
338
918
|
|
|
339
|
-
attr_reader :operand, :function, :distinct
|
|
919
|
+
attr_reader :operand, :function, :distinct, :condition
|
|
340
920
|
|
|
341
|
-
def initialize(operand, function, distinct: false)
|
|
921
|
+
def initialize(operand, function, distinct: false, condition: nil)
|
|
342
922
|
if distinct && function != :count
|
|
343
923
|
raise ArgumentError, "#{function} does not take distinct"
|
|
344
924
|
end
|
|
@@ -348,10 +928,35 @@ module ActiveRecord
|
|
|
348
928
|
@operand = operand
|
|
349
929
|
@function = function
|
|
350
930
|
@distinct = distinct
|
|
931
|
+
@condition = condition
|
|
932
|
+
end
|
|
933
|
+
|
|
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))
|
|
351
939
|
end
|
|
352
940
|
|
|
353
|
-
def to_arel(table)
|
|
354
|
-
|
|
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)
|
|
355
960
|
if function == :count
|
|
356
961
|
arel_operand.count(distinct)
|
|
357
962
|
else
|
|
@@ -360,16 +965,37 @@ module ActiveRecord
|
|
|
360
965
|
end
|
|
361
966
|
end
|
|
362
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.
|
|
363
976
|
class As < Node
|
|
364
|
-
attr_reader :operand, :alias_name
|
|
977
|
+
attr_reader :operand, :alias_name, :quote
|
|
365
978
|
|
|
366
|
-
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
|
|
367
984
|
@operand = operand
|
|
368
|
-
@alias_name =
|
|
985
|
+
@alias_name = alias_name
|
|
986
|
+
@quote = quote
|
|
369
987
|
end
|
|
370
988
|
|
|
371
|
-
def to_arel(table)
|
|
372
|
-
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) }
|
|
373
999
|
end
|
|
374
1000
|
end
|
|
375
1001
|
|
|
@@ -392,8 +1018,8 @@ module ActiveRecord
|
|
|
392
1018
|
Ordering.new(operand, direction, :nulls_last)
|
|
393
1019
|
end
|
|
394
1020
|
|
|
395
|
-
def to_arel(table)
|
|
396
|
-
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)
|
|
397
1023
|
nulls ? ordering.public_send(nulls) : ordering
|
|
398
1024
|
end
|
|
399
1025
|
end
|
|
@@ -401,6 +1027,7 @@ module ActiveRecord
|
|
|
401
1027
|
class Function < Node
|
|
402
1028
|
include Predications
|
|
403
1029
|
include Arithmetics
|
|
1030
|
+
include Windowing
|
|
404
1031
|
|
|
405
1032
|
attr_reader :name, :args
|
|
406
1033
|
|
|
@@ -409,12 +1036,23 @@ module ActiveRecord
|
|
|
409
1036
|
@args = args
|
|
410
1037
|
end
|
|
411
1038
|
|
|
412
|
-
def to_arel(table)
|
|
413
|
-
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) }
|
|
414
1041
|
Arel::Nodes::NamedFunction.new(name, arel_args)
|
|
415
1042
|
end
|
|
416
1043
|
end
|
|
417
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
|
+
|
|
418
1056
|
# EXTRACT(field FROM expr). The field is grammar rather than a value --
|
|
419
1057
|
# a keyword the adapter reads bare -- so it has to be a plain name,
|
|
420
1058
|
# which Arel upcases on the way out.
|
|
@@ -429,8 +1067,8 @@ module ActiveRecord
|
|
|
429
1067
|
@operand = operand
|
|
430
1068
|
end
|
|
431
1069
|
|
|
432
|
-
def to_arel(table)
|
|
433
|
-
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)
|
|
434
1072
|
end
|
|
435
1073
|
end
|
|
436
1074
|
|
|
@@ -449,10 +1087,10 @@ module ActiveRecord
|
|
|
449
1087
|
@sql_type = AST.check_name(sql_type, TYPE_NAME, "SQL type")
|
|
450
1088
|
end
|
|
451
1089
|
|
|
452
|
-
def to_arel(table)
|
|
1090
|
+
def to_arel(table, model)
|
|
453
1091
|
Arel::Nodes::NamedFunction.new(
|
|
454
1092
|
"CAST",
|
|
455
|
-
[Arel::Nodes::As.new(to_arel_argument(operand, table),
|
|
1093
|
+
[Arel::Nodes::As.new(to_arel_argument(operand, table, model),
|
|
456
1094
|
Arel::Nodes::SqlLiteral.new(sql_type.to_s))])
|
|
457
1095
|
end
|
|
458
1096
|
end
|
|
@@ -478,7 +1116,7 @@ module ActiveRecord
|
|
|
478
1116
|
@precision = precision
|
|
479
1117
|
end
|
|
480
1118
|
|
|
481
|
-
def to_arel(_table)
|
|
1119
|
+
def to_arel(_table, _model)
|
|
482
1120
|
Arel::Nodes::SqlLiteral.new(
|
|
483
1121
|
precision ? "#{name}(#{precision})" : name)
|
|
484
1122
|
end
|
|
@@ -501,11 +1139,11 @@ module ActiveRecord
|
|
|
501
1139
|
@value = value
|
|
502
1140
|
end
|
|
503
1141
|
|
|
504
|
-
def to_arel(table)
|
|
505
|
-
arel_column = to_arel_operand(column, table)
|
|
1142
|
+
def to_arel(table, model)
|
|
1143
|
+
arel_column = to_arel_operand(column, table, model)
|
|
506
1144
|
arel_value =
|
|
507
1145
|
case value
|
|
508
|
-
when Node then value.to_arel(table)
|
|
1146
|
+
when Node then value.to_arel(table, model)
|
|
509
1147
|
when ActiveRecord::Relation then scalar_subquery(value)
|
|
510
1148
|
else value
|
|
511
1149
|
end
|
|
@@ -526,9 +1164,50 @@ module ActiveRecord
|
|
|
526
1164
|
end
|
|
527
1165
|
end
|
|
528
1166
|
|
|
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
|
|
1171
|
+
|
|
1172
|
+
def initialize(operand, value, negated: false)
|
|
1173
|
+
@operand = operand
|
|
1174
|
+
@value = value
|
|
1175
|
+
@negated = negated
|
|
1176
|
+
end
|
|
1177
|
+
|
|
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)
|
|
1182
|
+
end
|
|
1183
|
+
end
|
|
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
|
|
1190
|
+
private
|
|
1191
|
+
|
|
1192
|
+
def set_subquery(relation, spelling)
|
|
1193
|
+
relation = relation.send(:apply_join_dependency) if relation.eager_loading?
|
|
1194
|
+
if relation.select_values.empty?
|
|
1195
|
+
model = relation.model
|
|
1196
|
+
if model.composite_primary_key?
|
|
1197
|
+
raise ArgumentError,
|
|
1198
|
+
"Cannot map composite primary key #{model.primary_key} to #{spelling}"
|
|
1199
|
+
end
|
|
1200
|
+
relation = relation.select(relation.table[model.primary_key])
|
|
1201
|
+
end
|
|
1202
|
+
relation.arel
|
|
1203
|
+
end
|
|
1204
|
+
end
|
|
1205
|
+
|
|
529
1206
|
# IN for a list of values, BETWEEN for a range, IN (SELECT ...) for a
|
|
530
1207
|
# relation.
|
|
531
1208
|
class In < Predicate
|
|
1209
|
+
include SetSubquery
|
|
1210
|
+
|
|
532
1211
|
attr_reader :operand, :values, :negated
|
|
533
1212
|
|
|
534
1213
|
def initialize(operand, values, negated: false)
|
|
@@ -537,34 +1216,39 @@ module ActiveRecord
|
|
|
537
1216
|
@negated = negated
|
|
538
1217
|
end
|
|
539
1218
|
|
|
540
|
-
def to_arel(table)
|
|
541
|
-
arel_operand = to_arel_operand(operand, table)
|
|
1219
|
+
def to_arel(table, model)
|
|
1220
|
+
arel_operand = to_arel_operand(operand, table, model)
|
|
542
1221
|
if values.is_a?(Range)
|
|
543
1222
|
arel_operand.public_send(negated ? :not_between : :between, values)
|
|
544
1223
|
else
|
|
545
|
-
arg = values.is_a?(ActiveRecord::Relation) ?
|
|
1224
|
+
arg = values.is_a?(ActiveRecord::Relation) ? set_subquery(values, 'IN') : values
|
|
546
1225
|
arel_operand.public_send(negated ? :not_in : :in, arg)
|
|
547
1226
|
end
|
|
548
1227
|
end
|
|
1228
|
+
end
|
|
549
1229
|
|
|
550
|
-
|
|
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
|
|
551
1235
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
end
|
|
559
|
-
if relation.select_values.empty?
|
|
560
|
-
model = relation.model
|
|
561
|
-
if model.composite_primary_key?
|
|
562
|
-
raise ArgumentError,
|
|
563
|
-
"Cannot map composite primary key #{model.primary_key} to IN"
|
|
564
|
-
end
|
|
565
|
-
relation = relation.select(relation.table[model.primary_key])
|
|
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"
|
|
566
1242
|
end
|
|
567
|
-
|
|
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])
|
|
568
1252
|
end
|
|
569
1253
|
end
|
|
570
1254
|
|
|
@@ -578,7 +1262,7 @@ module ActiveRecord
|
|
|
578
1262
|
@relation = relation
|
|
579
1263
|
end
|
|
580
1264
|
|
|
581
|
-
def to_arel(_table)
|
|
1265
|
+
def to_arel(_table, _model)
|
|
582
1266
|
subquery = relation
|
|
583
1267
|
if subquery.eager_loading?
|
|
584
1268
|
subquery = subquery.send(:apply_join_dependency)
|
|
@@ -615,10 +1299,10 @@ module ActiveRecord
|
|
|
615
1299
|
@negated = negated
|
|
616
1300
|
end
|
|
617
1301
|
|
|
618
|
-
def to_arel(table)
|
|
1302
|
+
def to_arel(table, model)
|
|
619
1303
|
# Arel matches case-insensitively unless told otherwise, which is
|
|
620
1304
|
# what picks ILIKE over LIKE on PostgreSQL.
|
|
621
|
-
to_arel_operand(operand, table).
|
|
1305
|
+
to_arel_operand(operand, table, model).
|
|
622
1306
|
public_send(negated ? :does_not_match : :matches,
|
|
623
1307
|
pattern, escape, case_sensitive)
|
|
624
1308
|
end
|
|
@@ -636,9 +1320,9 @@ module ActiveRecord
|
|
|
636
1320
|
@negated = negated
|
|
637
1321
|
end
|
|
638
1322
|
|
|
639
|
-
def to_arel(table)
|
|
640
|
-
arel_operand = to_arel_operand(operand, table)
|
|
641
|
-
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
|
|
642
1326
|
if negated
|
|
643
1327
|
arel_operand.is_distinct_from(arel_value)
|
|
644
1328
|
else
|
|
@@ -674,8 +1358,8 @@ module ActiveRecord
|
|
|
674
1358
|
@elements = elements
|
|
675
1359
|
end
|
|
676
1360
|
|
|
677
|
-
def to_arel(table)
|
|
678
|
-
arel_operand = to_arel_operand(operand, table)
|
|
1361
|
+
def to_arel(table, model)
|
|
1362
|
+
arel_operand = to_arel_operand(operand, table, model)
|
|
679
1363
|
quoted = Arel::Nodes.build_quoted(array_literal)
|
|
680
1364
|
case operator
|
|
681
1365
|
when :"@>" then Arel::Nodes::Contains.new(arel_operand, quoted)
|
|
@@ -713,8 +1397,8 @@ module ActiveRecord
|
|
|
713
1397
|
@negated = negated
|
|
714
1398
|
end
|
|
715
1399
|
|
|
716
|
-
def to_arel(table)
|
|
717
|
-
arel_operand = to_arel_operand(operand, table)
|
|
1400
|
+
def to_arel(table, model)
|
|
1401
|
+
arel_operand = to_arel_operand(operand, table, model)
|
|
718
1402
|
if negated
|
|
719
1403
|
arel_operand.does_not_match_regexp(pattern)
|
|
720
1404
|
else
|
|
@@ -746,8 +1430,8 @@ module ActiveRecord
|
|
|
746
1430
|
@right = right
|
|
747
1431
|
end
|
|
748
1432
|
|
|
749
|
-
def to_arel(table)
|
|
750
|
-
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))
|
|
751
1435
|
end
|
|
752
1436
|
end
|
|
753
1437
|
|
|
@@ -759,8 +1443,8 @@ module ActiveRecord
|
|
|
759
1443
|
@right = right
|
|
760
1444
|
end
|
|
761
1445
|
|
|
762
|
-
def to_arel(table)
|
|
763
|
-
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))
|
|
764
1448
|
end
|
|
765
1449
|
end
|
|
766
1450
|
|
|
@@ -771,8 +1455,8 @@ module ActiveRecord
|
|
|
771
1455
|
@operand = operand
|
|
772
1456
|
end
|
|
773
1457
|
|
|
774
|
-
def to_arel(table)
|
|
775
|
-
Arel::Nodes::Not.new(operand.to_arel(table))
|
|
1458
|
+
def to_arel(table, model)
|
|
1459
|
+
Arel::Nodes::Not.new(operand.to_arel(table, model))
|
|
776
1460
|
end
|
|
777
1461
|
end
|
|
778
1462
|
end
|