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