activerecord-refined 0.8.0 → 0.9.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/test.yml +14 -0
- data/.rubocop.yml +393 -0
- data/Gemfile +5 -3
- data/README.md +254 -74
- data/Rakefile +31 -3
- data/activerecord-refined.gemspec +29 -15
- data/benchmark/query_building.rb +11 -11
- data/examples/aggregations.rb +13 -11
- data/examples/complex_joins.rb +12 -10
- data/examples/ctes.rb +22 -20
- data/examples/expressions.rb +79 -44
- data/examples/json.rb +77 -38
- data/examples/postgresql.rb +64 -53
- data/examples/predicates.rb +35 -33
- data/examples/subqueries.rb +20 -18
- data/examples/windows.rb +23 -21
- data/examples/writes.rb +26 -24
- data/lib/active_record/refined/ast.rb +862 -272
- data/lib/active_record/refined.rb +260 -180
- data/lib/activerecord-refined/version.rb +3 -1
- data/lib/activerecord-refined.rb +8 -5
- data/test/test_block_syntax.rb +879 -323
- data/test/test_helper.rb +56 -39
- metadata +130 -1
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
# JSON.generate, for the document a containment test is given.
|
|
2
|
-
require
|
|
4
|
+
require "json"
|
|
3
5
|
|
|
4
6
|
module ActiveRecord
|
|
5
7
|
module Refined
|
|
@@ -10,6 +12,10 @@ module ActiveRecord
|
|
|
10
12
|
# A SQL type as cast writes it: words, at most parenthesized with
|
|
11
13
|
# lengths -- double precision, decimal(10,2).
|
|
12
14
|
TYPE_NAME = /\A[[:alpha:]_][[:alnum:]_ ]*(\(\d+(, ?\d+)?\))?\z/
|
|
15
|
+
# The characters PostgreSQL allows an operator to be made of, the
|
|
16
|
+
# widest operator alphabet of the three; op admits nothing else, so a
|
|
17
|
+
# letter, a space or a quote never reaches the SQL as an operator.
|
|
18
|
+
OPERATOR = %r{\A[+\-*/<>=~!@\#%^&|`?]+\z}
|
|
13
19
|
|
|
14
20
|
# Which family of spellings an adapter belongs to. MariaDB answers to
|
|
15
21
|
# the mysql2 adapter and is counted with MySQL, though the two part
|
|
@@ -19,12 +25,12 @@ module ActiveRecord
|
|
|
19
25
|
# pglite is PostgreSQL itself compiled to WebAssembly, reached through
|
|
20
26
|
# wasmify-rails' adapter; the server it answers for is the same one.
|
|
21
27
|
ADAPTER_FAMILIES = {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
"sqlite3" => :sqlite,
|
|
29
|
+
"postgresql" => :postgresql,
|
|
30
|
+
"postgis" => :postgresql,
|
|
31
|
+
"pglite" => :postgresql,
|
|
32
|
+
"mysql2" => :mysql,
|
|
33
|
+
"trilogy" => :mysql,
|
|
28
34
|
}.freeze
|
|
29
35
|
|
|
30
36
|
def self.adapter_family(model)
|
|
@@ -36,6 +42,21 @@ module ActiveRecord
|
|
|
36
42
|
raise ArgumentError, "#{name.inspect} is not a plain #{what}"
|
|
37
43
|
end
|
|
38
44
|
|
|
45
|
+
# A Ruby document or boolean written where JSON is wanted. Taken as it
|
|
46
|
+
# is, a document would be the string that spells it and a boolean
|
|
47
|
+
# SQLite's own 1. SQLite's json() marks the literal for the JSON
|
|
48
|
+
# functions; the MySQL family, which has no json(), reads it with
|
|
49
|
+
# JSON_EXTRACT.
|
|
50
|
+
def self.json_argument(value, model)
|
|
51
|
+
json = Arel::Nodes.build_quoted(JSON.generate(value))
|
|
52
|
+
if adapter_family(model) == :sqlite
|
|
53
|
+
Arel::Nodes::NamedFunction.new("json", [json])
|
|
54
|
+
else
|
|
55
|
+
Arel::Nodes::NamedFunction.new(
|
|
56
|
+
"JSON_EXTRACT", [json, Arel::Nodes.build_quoted("$")])
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
39
60
|
# Predicate builders shared by symbols, qualified columns and
|
|
40
61
|
# expressions. Imported into the Symbol refinement with
|
|
41
62
|
# Refinement#import_methods, so every method must be defined with def.
|
|
@@ -122,12 +143,14 @@ module ActiveRecord
|
|
|
122
143
|
In.new(self, values, negated: true)
|
|
123
144
|
end
|
|
124
145
|
|
|
146
|
+
# Not min..max: an endpoint may be an expression, which Range would
|
|
147
|
+
# refuse to hold, since expressions do not compare among themselves.
|
|
125
148
|
def between?(min, max)
|
|
126
|
-
In.new(self, min
|
|
149
|
+
In.new(self, In::QuotedRange.new(min, max, false))
|
|
127
150
|
end
|
|
128
151
|
|
|
129
152
|
def not_between?(min, max)
|
|
130
|
-
In.new(self, min
|
|
153
|
+
In.new(self, In::QuotedRange.new(min, max, false), negated: true)
|
|
131
154
|
end
|
|
132
155
|
|
|
133
156
|
# CASE with this as the operand, compared against each `when`:
|
|
@@ -177,14 +200,14 @@ module ActiveRecord
|
|
|
177
200
|
if prefixes.empty?
|
|
178
201
|
raise ArgumentError, "start_with? needs at least one prefix"
|
|
179
202
|
end
|
|
180
|
-
Like.any(self, prefixes.map {|prefix| "#{Like.escape(prefix)}%" })
|
|
203
|
+
Like.any(self, prefixes.map { |prefix| "#{Like.escape(prefix)}%" })
|
|
181
204
|
end
|
|
182
205
|
|
|
183
206
|
def end_with?(*suffixes)
|
|
184
207
|
if suffixes.empty?
|
|
185
208
|
raise ArgumentError, "end_with? needs at least one suffix"
|
|
186
209
|
end
|
|
187
|
-
Like.any(self, suffixes.map {|suffix| "%#{Like.escape(suffix)}" })
|
|
210
|
+
Like.any(self, suffixes.map { |suffix| "%#{Like.escape(suffix)}" })
|
|
188
211
|
end
|
|
189
212
|
|
|
190
213
|
def include?(substring)
|
|
@@ -222,11 +245,11 @@ module ActiveRecord
|
|
|
222
245
|
# the JSON questions. dig_text gives the value as text instead,
|
|
223
246
|
# which is what a comparison wants.
|
|
224
247
|
def dig(*path)
|
|
225
|
-
JsonPath.new(self, path
|
|
248
|
+
JsonPath.new(self, path)
|
|
226
249
|
end
|
|
227
250
|
|
|
228
251
|
def dig_text(*path)
|
|
229
|
-
JsonPath.new(self, path)
|
|
252
|
+
JsonPath.new(self, path, json_value: false)
|
|
230
253
|
end
|
|
231
254
|
|
|
232
255
|
# Keys taken out of a JSON document, by the name of what Hash does,
|
|
@@ -255,6 +278,11 @@ module ActiveRecord
|
|
|
255
278
|
def key?(key)
|
|
256
279
|
JsonHasKey.new(self, key)
|
|
257
280
|
end
|
|
281
|
+
|
|
282
|
+
# The keys of the document, as Hash#keys gives them: a JSON array.
|
|
283
|
+
def keys
|
|
284
|
+
JsonKeys.new(self)
|
|
285
|
+
end
|
|
258
286
|
end
|
|
259
287
|
|
|
260
288
|
# Arithmetic builders shared by symbols, qualified columns and
|
|
@@ -306,6 +334,58 @@ module ActiveRecord
|
|
|
306
334
|
end
|
|
307
335
|
end
|
|
308
336
|
|
|
337
|
+
# Arithmetic with the number on the left, imported into the numeric
|
|
338
|
+
# refinements: 20 - :quantity builds what :quantity + 20 builds. Only
|
|
339
|
+
# a column or an expression on the right means a query; anything else
|
|
340
|
+
# goes back to the number through super, so 1 + 2 is 3 inside a block
|
|
341
|
+
# too.
|
|
342
|
+
module NumericArithmetics
|
|
343
|
+
def +(other)
|
|
344
|
+
return super unless other.is_a?(::Symbol) || other.is_a?(Node)
|
|
345
|
+
Arithmetic.new(self, :+, other)
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def -(other)
|
|
349
|
+
return super unless other.is_a?(::Symbol) || other.is_a?(Node)
|
|
350
|
+
Arithmetic.new(self, :-, other)
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def *(other)
|
|
354
|
+
return super unless other.is_a?(::Symbol) || other.is_a?(Node)
|
|
355
|
+
Arithmetic.new(self, :*, other)
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def /(other)
|
|
359
|
+
return super unless other.is_a?(::Symbol) || other.is_a?(Node)
|
|
360
|
+
Arithmetic.new(self, :/, other)
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def &(other)
|
|
364
|
+
return super unless other.is_a?(::Symbol) || other.is_a?(Node)
|
|
365
|
+
Bitwise.new(self, :&, other)
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def |(other)
|
|
369
|
+
return super unless other.is_a?(::Symbol) || other.is_a?(Node)
|
|
370
|
+
Bitwise.new(self, :|, other)
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def ^(other)
|
|
374
|
+
return super unless other.is_a?(::Symbol) || other.is_a?(Node)
|
|
375
|
+
Bitwise.new(self, :^, other)
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def <<(other)
|
|
379
|
+
return super unless other.is_a?(::Symbol) || other.is_a?(Node)
|
|
380
|
+
Bitwise.new(self, :<<, other)
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def >>(other)
|
|
384
|
+
return super unless other.is_a?(::Symbol) || other.is_a?(Node)
|
|
385
|
+
Bitwise.new(self, :>>, other)
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
|
|
309
389
|
class Node
|
|
310
390
|
# The model travels with the table because some SQL cannot be written
|
|
311
391
|
# without knowing the adapter, and a node is built before anything
|
|
@@ -329,25 +409,60 @@ module ActiveRecord
|
|
|
329
409
|
end
|
|
330
410
|
|
|
331
411
|
private
|
|
412
|
+
# Resolves an operand denoting a column or an expression. A number
|
|
413
|
+
# rides along for Arel to write out, which it can do for Integer and
|
|
414
|
+
# Float alone: a BigDecimal is quoted, which the adapter spells as
|
|
415
|
+
# the exact decimal, and a Rational, which no decimal spells exactly,
|
|
416
|
+
# is refused.
|
|
417
|
+
def to_arel_operand(operand, table, model)
|
|
418
|
+
case operand
|
|
419
|
+
when Node then operand.to_arel(table, model)
|
|
420
|
+
when :* then Arel.star
|
|
421
|
+
when Symbol then table[operand]
|
|
422
|
+
when ::BigDecimal, ::Rational then quote_number(operand)
|
|
423
|
+
else operand
|
|
424
|
+
end
|
|
425
|
+
end
|
|
332
426
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
427
|
+
# A bare symbol is a column in every position, the value side of a
|
|
428
|
+
# comparison included. The name is checked against the model, since
|
|
429
|
+
# a name it has no column for is almost always an enum value spelled
|
|
430
|
+
# as a symbol -- which, taken as a column, would quietly compare
|
|
431
|
+
# against nothing anyone meant.
|
|
432
|
+
def column_operand(name, table, model)
|
|
433
|
+
unless model.column_names.include?(name.to_s)
|
|
434
|
+
raise ArgumentError,
|
|
435
|
+
"#{name.inspect} is no column of #{model.table_name}; an enum " \
|
|
436
|
+
"value is written as its string, a column of another table " \
|
|
437
|
+
"qualified"
|
|
438
|
+
end
|
|
439
|
+
table[name]
|
|
340
440
|
end
|
|
341
|
-
end
|
|
342
441
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
442
|
+
# A number compares as itself, the way a bound ? does: the typed path
|
|
443
|
+
# would cast 99.5 against an integer column to 99 and quietly move
|
|
444
|
+
# the boundary. Everything else keeps the column's own
|
|
445
|
+
# serialization -- an enum's name, a time's zone, a custom type's
|
|
446
|
+
# scaling.
|
|
447
|
+
def quote_number(value)
|
|
448
|
+
case value
|
|
449
|
+
when ::Rational
|
|
450
|
+
raise ArgumentError,
|
|
451
|
+
"a Rational has no exact SQL spelling; to_d says the decimal meant"
|
|
452
|
+
when ::Integer, ::Float, ::BigDecimal
|
|
453
|
+
Arel::Nodes.build_quoted(value)
|
|
454
|
+
else value
|
|
455
|
+
end
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
# Resolves a function argument: a column or an expression as above,
|
|
459
|
+
# anything else a value to be quoted.
|
|
460
|
+
def to_arel_argument(arg, table, model)
|
|
461
|
+
case arg
|
|
462
|
+
when Node, Symbol, ::Rational then to_arel_operand(arg, table, model)
|
|
463
|
+
else Arel::Nodes.build_quoted(arg)
|
|
464
|
+
end
|
|
349
465
|
end
|
|
350
|
-
end
|
|
351
466
|
end
|
|
352
467
|
|
|
353
468
|
class Predicate < Node
|
|
@@ -366,11 +481,11 @@ module ActiveRecord
|
|
|
366
481
|
|
|
367
482
|
# A literal standing where an expression would: `select { value(0).as(:depth) }`.
|
|
368
483
|
#
|
|
369
|
-
# Values reach the SQL quoted wherever they appear as an operand, but
|
|
370
|
-
# top of a select list is
|
|
371
|
-
#
|
|
372
|
-
#
|
|
373
|
-
#
|
|
484
|
+
# Values reach the SQL quoted wherever they appear as an operand, but a
|
|
485
|
+
# bare Ruby literal at the top of a select list is refused as saying
|
|
486
|
+
# nothing. `value` is the spelling that quotes it there, and it carries
|
|
487
|
+
# the predications with it, so a literal can be compared and combined
|
|
488
|
+
# like anything else.
|
|
374
489
|
class Value < Node
|
|
375
490
|
include Predications
|
|
376
491
|
include Arithmetics
|
|
@@ -386,6 +501,43 @@ module ActiveRecord
|
|
|
386
501
|
end
|
|
387
502
|
end
|
|
388
503
|
|
|
504
|
+
# SQL as written: `sql("length(name) > ?", 10)`. The ? and :name
|
|
505
|
+
# placeholders take quoted values through sanitize_sql_array, which
|
|
506
|
+
# needs the connection, so the binds wait here until the model is
|
|
507
|
+
# known. Without binds the statement passes untouched -- which is
|
|
508
|
+
# what leaves PostgreSQL's ? operators writable, since only the
|
|
509
|
+
# positional-bind rewrite reads ? as a placeholder.
|
|
510
|
+
#
|
|
511
|
+
# As an operand the statement is parenthesized: its precedence is
|
|
512
|
+
# whatever was written inside. The top of a select list gets it bare,
|
|
513
|
+
# through field_arel, where parentheses would refuse an alias written
|
|
514
|
+
# into the string.
|
|
515
|
+
class Sql < Node
|
|
516
|
+
include Predications
|
|
517
|
+
include Arithmetics
|
|
518
|
+
|
|
519
|
+
attr_reader :statement, :binds
|
|
520
|
+
|
|
521
|
+
def initialize(statement, binds)
|
|
522
|
+
unless statement.is_a?(::String)
|
|
523
|
+
raise ArgumentError,
|
|
524
|
+
"sql takes the statement as a string, not #{statement.inspect}"
|
|
525
|
+
end
|
|
526
|
+
@statement = statement
|
|
527
|
+
@binds = binds
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
def to_arel(_table, model)
|
|
531
|
+
Arel::Nodes::Grouping.new(field_arel(model))
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
def field_arel(model)
|
|
535
|
+
return Arel.sql(statement) if binds.empty?
|
|
536
|
+
|
|
537
|
+
Arel.sql(model.sanitize_sql_array([statement, *binds]))
|
|
538
|
+
end
|
|
539
|
+
end
|
|
540
|
+
|
|
389
541
|
# CASE, in both of the shapes SQL has for it. With an operand, each
|
|
390
542
|
# `when` is something to compare it against; without one, each `when` is
|
|
391
543
|
# a condition of its own.
|
|
@@ -485,24 +637,24 @@ module ActiveRecord
|
|
|
485
637
|
# quoted so that a comma or a brace in a key is part of it. except
|
|
486
638
|
# writes its keys the same way, which are steps of no one path.
|
|
487
639
|
def steps_array(steps = path)
|
|
488
|
-
"{#{steps.map {|step| %("#{escape_step(step)}") }.join(',')}}"
|
|
640
|
+
"{#{steps.map { |step| %("#{escape_step(step)}") }.join(',')}}"
|
|
489
641
|
end
|
|
490
642
|
|
|
491
643
|
# MySQL and SQLite take a path expression instead, where an integer is
|
|
492
644
|
# a subscript and a name that is not plain has to be quoted.
|
|
493
645
|
def dollar_path
|
|
494
|
-
path.inject(+
|
|
646
|
+
path.inject(+"$") { |so_far, step| so_far << dollar_step(step) }
|
|
495
647
|
end
|
|
496
648
|
|
|
497
649
|
def dollar_step(step)
|
|
498
650
|
return "[#{step}]" if step.is_a?(::Integer)
|
|
499
651
|
name = step.to_s
|
|
500
|
-
|
|
652
|
+
"." + (name.match?(/\A[[:alpha:]_][[:alnum:]_]*\z/) ?
|
|
501
653
|
name : %("#{escape_step(step)}"))
|
|
502
654
|
end
|
|
503
655
|
|
|
504
656
|
def escape_step(step)
|
|
505
|
-
step.to_s.gsub(
|
|
657
|
+
step.to_s.gsub("\\", "\\\\").gsub('"', '\\"')
|
|
506
658
|
end
|
|
507
659
|
end
|
|
508
660
|
|
|
@@ -521,52 +673,128 @@ module ActiveRecord
|
|
|
521
673
|
# `dig_text(:flag) == true` is true, an error, and false. cast is what
|
|
522
674
|
# says which type was meant, and then all three agree.
|
|
523
675
|
#
|
|
524
|
-
# dig
|
|
525
|
-
#
|
|
526
|
-
#
|
|
527
|
-
#
|
|
676
|
+
# dig, bury and except give JSON, and a JSON comparison belongs to the
|
|
677
|
+
# JSON types -- jsonb and MySQL's -- where numbers compare as numbers
|
|
678
|
+
# and documents structurally, key order and spelling aside. The Ruby
|
|
679
|
+
# value goes in as a JSON literal, and the adapters without such a
|
|
680
|
+
# type refuse it from JsonLiteral when the SQL is written, which is
|
|
681
|
+
# when the adapter is known.
|
|
528
682
|
#
|
|
529
683
|
# A string against dig_text, and anything the block itself built -- a
|
|
530
684
|
# column, a function, another dug value -- go through untouched.
|
|
685
|
+
#
|
|
686
|
+
# Arithmetic and the bit operators are refused outright on both sides:
|
|
687
|
+
# `dig_text(:n) + 1` is 6 on SQLite, an error on PostgreSQL and 6.0 on
|
|
688
|
+
# MariaDB, and an expression on the right does not change what the
|
|
689
|
+
# dug side is.
|
|
531
690
|
module JsonComparable
|
|
532
691
|
%i[== != < <= > >=].each do |operator|
|
|
533
692
|
define_method(operator) do |other|
|
|
534
|
-
|
|
535
|
-
super(other)
|
|
693
|
+
super(comparison_value(other))
|
|
536
694
|
end
|
|
537
695
|
end
|
|
538
696
|
|
|
539
|
-
def in?(values) = super(
|
|
540
|
-
def not_in?(values) = super(
|
|
541
|
-
def between?(min, max) = super(
|
|
542
|
-
def not_between?(min, max) = super(
|
|
697
|
+
def in?(values) = super(comparison_set(values))
|
|
698
|
+
def not_in?(values) = super(comparison_set(values))
|
|
699
|
+
def between?(min, max) = super(comparison_value(min), comparison_value(max))
|
|
700
|
+
def not_between?(min, max) = super(comparison_value(min), comparison_value(max))
|
|
701
|
+
|
|
702
|
+
%i[+ - * / & | ^ << >>].each do |operator|
|
|
703
|
+
define_method(operator) do |_other|
|
|
704
|
+
raise ArgumentError, arithmetic_refusal(operator)
|
|
705
|
+
end
|
|
706
|
+
end
|
|
707
|
+
|
|
708
|
+
def ~
|
|
709
|
+
raise ArgumentError, arithmetic_refusal(:~)
|
|
710
|
+
end
|
|
543
711
|
|
|
544
712
|
private
|
|
713
|
+
# nil is left to the comparison itself, which says to use null?, and
|
|
714
|
+
# so is anything the block built rather than wrote as a literal.
|
|
715
|
+
def comparison_value(other)
|
|
716
|
+
return other if other.nil? || other.is_a?(Node) || other.is_a?(::Symbol) ||
|
|
717
|
+
other.is_a?(Arel::Nodes::Node) ||
|
|
718
|
+
other.is_a?(Arel::Attributes::Attribute) ||
|
|
719
|
+
other.is_a?(ActiveRecord::Relation)
|
|
720
|
+
return json_literal(other) if json_value?
|
|
721
|
+
return other if other.is_a?(::String)
|
|
722
|
+
|
|
723
|
+
raise ArgumentError,
|
|
724
|
+
"dig_text gives text, and comparing it with #{other.inspect} means " \
|
|
725
|
+
"something different on every adapter; cast it to the type meant"
|
|
726
|
+
end
|
|
727
|
+
|
|
728
|
+
def json_literal(other)
|
|
729
|
+
case other
|
|
730
|
+
when ::String, ::Integer, ::Float, ::BigDecimal, true, false, ::Hash, ::Array
|
|
731
|
+
JsonLiteral.new(other)
|
|
732
|
+
when ::Rational
|
|
733
|
+
raise ArgumentError,
|
|
734
|
+
"a Rational has no exact SQL spelling; to_d says the decimal meant"
|
|
735
|
+
else
|
|
736
|
+
raise ArgumentError,
|
|
737
|
+
"#{json_source} gives JSON, and #{other.inspect} has no JSON " \
|
|
738
|
+
"spelling; dig_text gives the value"
|
|
739
|
+
end
|
|
740
|
+
end
|
|
545
741
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
742
|
+
def comparison_set(values)
|
|
743
|
+
case values
|
|
744
|
+
when ActiveRecord::Relation then values
|
|
745
|
+
when ::Range
|
|
746
|
+
In::QuotedRange.new(comparison_value(values.begin),
|
|
747
|
+
comparison_value(values.end), values.exclude_end?)
|
|
748
|
+
else values.map { |value| comparison_value(value) }
|
|
749
|
+
end
|
|
750
|
+
end
|
|
751
|
+
|
|
752
|
+
def arithmetic_refusal(operator)
|
|
753
|
+
json_value? ?
|
|
754
|
+
"#{json_source} gives JSON, and #{operator} on it means something " \
|
|
755
|
+
"different on every adapter; cast dig_text to the type meant" :
|
|
756
|
+
"dig_text gives text, and #{operator} on it means something " \
|
|
757
|
+
"different on every adapter; cast it to the type meant"
|
|
758
|
+
end
|
|
759
|
+
end
|
|
554
760
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
761
|
+
# A Ruby value on the JSON side of a comparison, which jsonb and
|
|
762
|
+
# MySQL's JSON type answer alike: numbers compare as numbers and
|
|
763
|
+
# documents structurally. SQLite and MariaDB have only the text of
|
|
764
|
+
# each -- spelling and key order deciding what equality means -- and
|
|
765
|
+
# refuse here. PostgreSQL needs no cast, an untyped literal beside a
|
|
766
|
+
# jsonb operand coercing to jsonb; MySQL is told CAST(... AS JSON),
|
|
767
|
+
# since a bare string beside JSON would be a JSON string, and every
|
|
768
|
+
# string outranks every number in its ordering.
|
|
769
|
+
class JsonLiteral < Node
|
|
770
|
+
attr_reader :value
|
|
771
|
+
|
|
772
|
+
def initialize(value)
|
|
773
|
+
@value = value
|
|
560
774
|
end
|
|
561
775
|
|
|
562
|
-
def
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
when
|
|
566
|
-
|
|
776
|
+
def to_arel(_table, model)
|
|
777
|
+
json = Arel::Nodes.build_quoted(JSON.generate(value))
|
|
778
|
+
case AST.adapter_family(model)
|
|
779
|
+
when :postgresql then json
|
|
780
|
+
when :mysql then mysql_literal(json, model)
|
|
781
|
+
else refuse(model.connection_db_config.adapter)
|
|
567
782
|
end
|
|
568
|
-
values
|
|
569
783
|
end
|
|
784
|
+
|
|
785
|
+
private
|
|
786
|
+
# MariaDB answers to the same adapter and has no JSON type at all.
|
|
787
|
+
def mysql_literal(json, model)
|
|
788
|
+
refuse("MariaDB") if model.with_connection { |c| c.mariadb? }
|
|
789
|
+
Arel::Nodes::NamedFunction.new(
|
|
790
|
+
"CAST", [Arel::Nodes::As.new(json, Arel::Nodes::SqlLiteral.new("JSON"))])
|
|
791
|
+
end
|
|
792
|
+
|
|
793
|
+
def refuse(database)
|
|
794
|
+
raise NotImplementedError,
|
|
795
|
+
"a JSON comparison has no equivalent on #{database}; " \
|
|
796
|
+
"dig_text gives the value"
|
|
797
|
+
end
|
|
570
798
|
end
|
|
571
799
|
|
|
572
800
|
# The JSON operations read a document, and what dig gives is one:
|
|
@@ -576,9 +804,9 @@ module ActiveRecord
|
|
|
576
804
|
# again is where they part company: SQLite parses it back and MySQL
|
|
577
805
|
# takes it as written, where PostgreSQL has no such function for text.
|
|
578
806
|
module JsonDocument
|
|
579
|
-
%i[dig dig_text key? contains? bury except].each do |name|
|
|
807
|
+
%i[dig dig_text key? keys contains? bury except].each do |name|
|
|
580
808
|
define_method(name) do |*args|
|
|
581
|
-
unless
|
|
809
|
+
unless json_value?
|
|
582
810
|
raise ArgumentError,
|
|
583
811
|
"dig_text gives text, and #{name} reads JSON; dig keeps it"
|
|
584
812
|
end
|
|
@@ -587,6 +815,22 @@ module ActiveRecord
|
|
|
587
815
|
end
|
|
588
816
|
end
|
|
589
817
|
|
|
818
|
+
# JSON the query computes rather than reads out of a document: always
|
|
819
|
+
# a JSON value, with no dig_text counterpart for JsonComparable's
|
|
820
|
+
# advice to name. Included after JsonComparable, whose own
|
|
821
|
+
# arithmetic_refusal it overrides.
|
|
822
|
+
module ComputedJson
|
|
823
|
+
def json_value?
|
|
824
|
+
true
|
|
825
|
+
end
|
|
826
|
+
|
|
827
|
+
private
|
|
828
|
+
def arithmetic_refusal(operator)
|
|
829
|
+
"#{json_source} gives JSON, and #{operator} on it means " \
|
|
830
|
+
"something different on every adapter"
|
|
831
|
+
end
|
|
832
|
+
end
|
|
833
|
+
|
|
590
834
|
class JsonPath < Node
|
|
591
835
|
include Predications
|
|
592
836
|
include Arithmetics
|
|
@@ -594,12 +838,16 @@ module ActiveRecord
|
|
|
594
838
|
include JsonComparable
|
|
595
839
|
include JsonDocument
|
|
596
840
|
|
|
597
|
-
attr_reader :operand, :path
|
|
841
|
+
attr_reader :operand, :path
|
|
598
842
|
|
|
599
|
-
def initialize(operand, path,
|
|
843
|
+
def initialize(operand, path, json_value: true)
|
|
600
844
|
@operand = operand
|
|
601
|
-
@path = check_steps(path,
|
|
602
|
-
@
|
|
845
|
+
@path = check_steps(path, "dig")
|
|
846
|
+
@json_value = json_value
|
|
847
|
+
end
|
|
848
|
+
|
|
849
|
+
def json_value?
|
|
850
|
+
@json_value
|
|
603
851
|
end
|
|
604
852
|
|
|
605
853
|
def to_arel(table, model)
|
|
@@ -607,22 +855,26 @@ module ActiveRecord
|
|
|
607
855
|
case AST.adapter_family(model)
|
|
608
856
|
when :postgresql
|
|
609
857
|
Arel::Nodes::InfixOperation.new(
|
|
610
|
-
|
|
858
|
+
json_value? ? :"#>" : :"#>>", document, Arel::Nodes.build_quoted(steps_array))
|
|
611
859
|
when :mysql
|
|
612
860
|
extracted = Arel::Nodes::NamedFunction.new(
|
|
613
|
-
|
|
614
|
-
|
|
861
|
+
"JSON_EXTRACT", [document, Arel::Nodes.build_quoted(dollar_path)])
|
|
862
|
+
json_value? ? extracted : Arel::Nodes::NamedFunction.new("JSON_UNQUOTE", [extracted])
|
|
615
863
|
else
|
|
616
864
|
extracted = Arel::Nodes::InfixOperation.new(
|
|
617
|
-
|
|
865
|
+
json_value? ? :"->" : :"->>", document, Arel::Nodes.build_quoted(dollar_path))
|
|
618
866
|
# SQLite's ->> gives back the value with its type, where the other
|
|
619
867
|
# two give text. Cast so that `dig_text(:n) == '5'` means the
|
|
620
868
|
# same thing everywhere, and a number wants a cast everywhere too.
|
|
621
|
-
|
|
622
|
-
|
|
869
|
+
json_value? ? extracted : Arel::Nodes::NamedFunction.new(
|
|
870
|
+
"CAST", [Arel::Nodes::As.new(extracted, Arel::Nodes::SqlLiteral.new("text"))])
|
|
623
871
|
end
|
|
624
872
|
end
|
|
625
873
|
|
|
874
|
+
private
|
|
875
|
+
def json_source
|
|
876
|
+
"dig"
|
|
877
|
+
end
|
|
626
878
|
end
|
|
627
879
|
|
|
628
880
|
# Setting a value inside a JSON document, which is what bury does to what
|
|
@@ -631,54 +883,63 @@ module ActiveRecord
|
|
|
631
883
|
class JsonSet < Node
|
|
632
884
|
include Predications
|
|
633
885
|
include JsonSteps
|
|
886
|
+
include JsonComparable
|
|
634
887
|
|
|
635
888
|
attr_reader :operand, :path, :value
|
|
636
889
|
|
|
637
890
|
def initialize(operand, path, value)
|
|
638
891
|
@operand = operand
|
|
639
|
-
@path = check_steps(path,
|
|
892
|
+
@path = check_steps(path, "bury")
|
|
640
893
|
@value = value
|
|
641
894
|
end
|
|
642
895
|
|
|
896
|
+
# Always JSON, which is what the comparison guard asks.
|
|
897
|
+
def json_value?
|
|
898
|
+
true
|
|
899
|
+
end
|
|
900
|
+
|
|
643
901
|
def to_arel(table, model)
|
|
644
902
|
document = to_arel_operand(operand, table, model)
|
|
645
903
|
if AST.adapter_family(model) == :postgresql
|
|
646
904
|
Arel::Nodes::NamedFunction.new(
|
|
647
|
-
|
|
905
|
+
"jsonb_set",
|
|
648
906
|
[document, Arel::Nodes.build_quoted(steps_array), postgresql_value(table, model)])
|
|
649
907
|
else
|
|
650
908
|
Arel::Nodes::NamedFunction.new(
|
|
651
|
-
|
|
909
|
+
"JSON_SET",
|
|
652
910
|
[document, Arel::Nodes.build_quoted(dollar_path), other_value(table, model)])
|
|
653
911
|
end
|
|
654
912
|
end
|
|
655
913
|
|
|
656
914
|
private
|
|
915
|
+
# jsonb_set takes jsonb, so an expression is turned into it and a Ruby
|
|
916
|
+
# value goes in as the JSON that says it -- '"x"' rather than 'x',
|
|
917
|
+
# which is not a document at all.
|
|
918
|
+
def postgresql_value(table, model)
|
|
919
|
+
return Arel::Nodes::NamedFunction.new(
|
|
920
|
+
"to_jsonb", [to_arel_operand(value, table, model)]) if expression?
|
|
921
|
+
Arel::Nodes.build_quoted(JSON.generate(value))
|
|
922
|
+
end
|
|
657
923
|
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
924
|
+
# The others take the value as it is, except a whole document or a
|
|
925
|
+
# boolean, which go in as JSON through json_argument.
|
|
926
|
+
def other_value(table, model)
|
|
927
|
+
return to_arel_operand(value, table, model) if expression?
|
|
928
|
+
unless value.is_a?(::Hash) || value.is_a?(::Array) ||
|
|
929
|
+
value == true || value == false
|
|
930
|
+
return Arel::Nodes.build_quoted(value)
|
|
931
|
+
end
|
|
666
932
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
# to JSON where MariaDB, which answers to the same adapter, does not.
|
|
670
|
-
def other_value(table, model)
|
|
671
|
-
return to_arel_operand(value, table, model) if expression?
|
|
672
|
-
return Arel::Nodes.build_quoted(value) unless value.is_a?(::Hash) || value.is_a?(::Array)
|
|
933
|
+
AST.json_argument(value, model)
|
|
934
|
+
end
|
|
673
935
|
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
end
|
|
936
|
+
def expression?
|
|
937
|
+
value.is_a?(Node) || value.is_a?(::Symbol)
|
|
938
|
+
end
|
|
678
939
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
940
|
+
def json_source
|
|
941
|
+
"bury"
|
|
942
|
+
end
|
|
682
943
|
end
|
|
683
944
|
|
|
684
945
|
# Keys taken out of a JSON document. PostgreSQL subtracts them, the
|
|
@@ -686,6 +947,7 @@ module ActiveRecord
|
|
|
686
947
|
class JsonExcept < Node
|
|
687
948
|
include Predications
|
|
688
949
|
include JsonSteps
|
|
950
|
+
include JsonComparable
|
|
689
951
|
|
|
690
952
|
attr_reader :operand, :keys
|
|
691
953
|
|
|
@@ -694,40 +956,51 @@ module ActiveRecord
|
|
|
694
956
|
@keys = check_keys(keys)
|
|
695
957
|
end
|
|
696
958
|
|
|
959
|
+
def json_value?
|
|
960
|
+
true
|
|
961
|
+
end
|
|
962
|
+
|
|
697
963
|
def to_arel(table, model)
|
|
698
964
|
document = to_arel_operand(operand, table, model)
|
|
699
|
-
|
|
700
|
-
|
|
965
|
+
if AST.adapter_family(model) == :postgresql
|
|
966
|
+
# Grouped because - binds tighter than #>: dug out of a document,
|
|
967
|
+
# the subtraction would otherwise take the path literal first.
|
|
968
|
+
return Arel::Nodes::InfixOperation.new(
|
|
969
|
+
:-, Arel::Nodes::Grouping.new(document), key_array)
|
|
970
|
+
end
|
|
701
971
|
|
|
702
972
|
Arel::Nodes::NamedFunction.new(
|
|
703
|
-
|
|
704
|
-
[document, *keys.map {|key| Arel::Nodes.build_quoted("$#{dollar_step(key)}") }])
|
|
973
|
+
"JSON_REMOVE",
|
|
974
|
+
[document, *keys.map { |key| Arel::Nodes.build_quoted("$#{dollar_step(key)}") }])
|
|
705
975
|
end
|
|
706
976
|
|
|
707
977
|
private
|
|
978
|
+
# jsonb has three subtractions -- a key, an array of keys, an element
|
|
979
|
+
# by index -- and an array literal written without a type is read as
|
|
980
|
+
# the first of them: `meta - '{draft}'` takes out the key spelled
|
|
981
|
+
# {draft}, which is nothing, and says nothing about it.
|
|
982
|
+
def key_array
|
|
983
|
+
Arel::Nodes::NamedFunction.new(
|
|
984
|
+
"CAST",
|
|
985
|
+
[Arel::Nodes::As.new(Arel::Nodes.build_quoted(steps_array(keys)),
|
|
986
|
+
Arel::Nodes::SqlLiteral.new("text[]"))])
|
|
987
|
+
end
|
|
708
988
|
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
989
|
+
# Keys, as Hash#except takes them: an index into an array is not what
|
|
990
|
+
# the name says anywhere, and is bury's business through a path.
|
|
991
|
+
def check_keys(keys)
|
|
992
|
+
raise ArgumentError, "except needs a key" if keys.empty?
|
|
993
|
+
keys.each do |key|
|
|
994
|
+
next if key.is_a?(::String) || key.is_a?(::Symbol)
|
|
995
|
+
raise ArgumentError,
|
|
996
|
+
"except takes keys of the document, not #{key.inspect}"
|
|
997
|
+
end
|
|
998
|
+
keys
|
|
999
|
+
end
|
|
719
1000
|
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
def check_keys(keys)
|
|
723
|
-
raise ArgumentError, 'except needs a key' if keys.empty?
|
|
724
|
-
keys.each do |key|
|
|
725
|
-
next if key.is_a?(::String) || key.is_a?(::Symbol)
|
|
726
|
-
raise ArgumentError,
|
|
727
|
-
"except takes keys of the document, not #{key.inspect}"
|
|
1001
|
+
def json_source
|
|
1002
|
+
"except"
|
|
728
1003
|
end
|
|
729
|
-
keys
|
|
730
|
-
end
|
|
731
1004
|
end
|
|
732
1005
|
|
|
733
1006
|
# JSON containment: whether the document holds what is given.
|
|
@@ -745,7 +1018,7 @@ module ActiveRecord
|
|
|
745
1018
|
case AST.adapter_family(model)
|
|
746
1019
|
when :postgresql then Arel::Nodes::Contains.new(document, json)
|
|
747
1020
|
when :mysql
|
|
748
|
-
Arel::Nodes::NamedFunction.new(
|
|
1021
|
+
Arel::Nodes::NamedFunction.new("JSON_CONTAINS", [document, json])
|
|
749
1022
|
else
|
|
750
1023
|
# Later than the others, since the adapter is only known here.
|
|
751
1024
|
raise NotImplementedError,
|
|
@@ -754,9 +1027,11 @@ module ActiveRecord
|
|
|
754
1027
|
end
|
|
755
1028
|
end
|
|
756
1029
|
|
|
757
|
-
# Whether a key is in the document. PostgreSQL
|
|
758
|
-
#
|
|
759
|
-
#
|
|
1030
|
+
# Whether a key is in the document. PostgreSQL's spelling is the ?
|
|
1031
|
+
# operator rather than jsonb_exists, the function it is shorthand for,
|
|
1032
|
+
# because a GIN index matches the operator and never the function. A
|
|
1033
|
+
# ? is a bind placeholder only to sanitize_sql, which none of the SQL
|
|
1034
|
+
# written here passes through.
|
|
760
1035
|
class JsonHasKey < Predicate
|
|
761
1036
|
attr_reader :operand, :key
|
|
762
1037
|
|
|
@@ -771,19 +1046,150 @@ module ActiveRecord
|
|
|
771
1046
|
path = Arel::Nodes.build_quoted("$.#{key}")
|
|
772
1047
|
case AST.adapter_family(model)
|
|
773
1048
|
when :postgresql
|
|
774
|
-
Arel::Nodes::
|
|
1049
|
+
Arel::Nodes::InfixOperation.new(:"?", document, name)
|
|
775
1050
|
when :mysql
|
|
776
1051
|
Arel::Nodes::NamedFunction.new(
|
|
777
|
-
|
|
1052
|
+
"JSON_CONTAINS_PATH", [document, Arel::Nodes.build_quoted("one"), path])
|
|
778
1053
|
else
|
|
779
|
-
Arel::Nodes::NamedFunction.new(
|
|
1054
|
+
Arel::Nodes::NamedFunction.new("json_type", [document, path]).not_eq(nil)
|
|
780
1055
|
end
|
|
781
1056
|
end
|
|
782
1057
|
end
|
|
783
1058
|
|
|
1059
|
+
# The keys of a JSON document, as Hash#keys gives them: a JSON array.
|
|
1060
|
+
# Only the MySQL family has a function for it; the other two reach the
|
|
1061
|
+
# same array through a subquery over their key-listing functions. The
|
|
1062
|
+
# type guard is what makes all four answer alike: the keys of what is
|
|
1063
|
+
# not an object are NULL rather than SQLite's array indices or
|
|
1064
|
+
# PostgreSQL's error, and the keys of {} are [] rather than
|
|
1065
|
+
# PostgreSQL's NULL, jsonb_agg over no rows.
|
|
1066
|
+
class JsonKeys < Node
|
|
1067
|
+
include Predications
|
|
1068
|
+
include JsonComparable
|
|
1069
|
+
include ComputedJson
|
|
1070
|
+
|
|
1071
|
+
attr_reader :operand
|
|
1072
|
+
|
|
1073
|
+
def initialize(operand)
|
|
1074
|
+
@operand = operand
|
|
1075
|
+
end
|
|
1076
|
+
|
|
1077
|
+
def to_arel(table, model)
|
|
1078
|
+
document = to_arel_operand(operand, table, model)
|
|
1079
|
+
case AST.adapter_family(model)
|
|
1080
|
+
when :sqlite
|
|
1081
|
+
sql = compile(document, model)
|
|
1082
|
+
Arel.sql("CASE WHEN json_type(#{sql}) = 'object' " \
|
|
1083
|
+
"THEN (SELECT json_group_array(key) FROM json_each(#{sql})) END")
|
|
1084
|
+
when :postgresql
|
|
1085
|
+
sql = compile(document, model)
|
|
1086
|
+
Arel.sql("CASE WHEN jsonb_typeof(#{sql}) = 'object' " \
|
|
1087
|
+
"THEN COALESCE((SELECT jsonb_agg(k) FROM jsonb_object_keys(#{sql}) k), " \
|
|
1088
|
+
"CAST('[]' AS jsonb)) END")
|
|
1089
|
+
else
|
|
1090
|
+
Arel::Nodes::NamedFunction.new("JSON_KEYS", [document])
|
|
1091
|
+
end
|
|
1092
|
+
end
|
|
1093
|
+
|
|
1094
|
+
private
|
|
1095
|
+
# The document appears more than once, the way SQLite's XOR names
|
|
1096
|
+
# its operands twice; the connection's own visitor compiles it, so
|
|
1097
|
+
# its quoting is the adapter's.
|
|
1098
|
+
def compile(document, model)
|
|
1099
|
+
model.with_connection { |connection| connection.visitor.compile(document) }
|
|
1100
|
+
end
|
|
1101
|
+
|
|
1102
|
+
def json_source
|
|
1103
|
+
"keys"
|
|
1104
|
+
end
|
|
1105
|
+
end
|
|
1106
|
+
|
|
1107
|
+
# A JSON document built in the row: json_array from the values given,
|
|
1108
|
+
# json_object from a Ruby hash. SQLite and the MySQL family both say
|
|
1109
|
+
# the standard names; PostgreSQL is asked to build jsonb, whose
|
|
1110
|
+
# documents the other JSON operations here read.
|
|
1111
|
+
class JsonBuild < Node
|
|
1112
|
+
include Predications
|
|
1113
|
+
include JsonComparable
|
|
1114
|
+
include ComputedJson
|
|
1115
|
+
|
|
1116
|
+
NAMES = {
|
|
1117
|
+
array: { postgresql: "jsonb_build_array" },
|
|
1118
|
+
object: { postgresql: "jsonb_build_object" },
|
|
1119
|
+
}.freeze
|
|
1120
|
+
|
|
1121
|
+
attr_reader :kind, :values
|
|
1122
|
+
|
|
1123
|
+
def initialize(kind, values)
|
|
1124
|
+
@kind = kind
|
|
1125
|
+
@values = kind == :object ? check_pairs(values) : values
|
|
1126
|
+
end
|
|
1127
|
+
|
|
1128
|
+
def to_arel(table, model)
|
|
1129
|
+
Arel::Nodes::NamedFunction.new(
|
|
1130
|
+
NAMES.fetch(kind).fetch(AST.adapter_family(model)) { "JSON_#{kind.to_s.upcase}" },
|
|
1131
|
+
arguments(table, model))
|
|
1132
|
+
end
|
|
1133
|
+
|
|
1134
|
+
private
|
|
1135
|
+
def arguments(table, model)
|
|
1136
|
+
if kind == :array
|
|
1137
|
+
values.map { |value| build_argument(value, table, model) }
|
|
1138
|
+
else
|
|
1139
|
+
values.flat_map do |key, value|
|
|
1140
|
+
[Arel::Nodes.build_quoted(key.to_s),
|
|
1141
|
+
build_argument(value, table, model)]
|
|
1142
|
+
end
|
|
1143
|
+
end
|
|
1144
|
+
end
|
|
1145
|
+
|
|
1146
|
+
# An expression is itself and a document or a boolean goes in as
|
|
1147
|
+
# JSON, as bury takes them. PostgreSQL builds from typed
|
|
1148
|
+
# arguments, so its JSON literal is cast -- left untyped it would
|
|
1149
|
+
# be text, and land as a string.
|
|
1150
|
+
def build_argument(value, table, model)
|
|
1151
|
+
case value
|
|
1152
|
+
when Node, ::Symbol then to_arel_operand(value, table, model)
|
|
1153
|
+
when ::Hash, ::Array, true, false
|
|
1154
|
+
if AST.adapter_family(model) == :postgresql
|
|
1155
|
+
Arel::Nodes::NamedFunction.new(
|
|
1156
|
+
"CAST", [Arel::Nodes::As.new(
|
|
1157
|
+
Arel::Nodes.build_quoted(JSON.generate(value)),
|
|
1158
|
+
Arel::Nodes::SqlLiteral.new("jsonb"))])
|
|
1159
|
+
else
|
|
1160
|
+
AST.json_argument(value, model)
|
|
1161
|
+
end
|
|
1162
|
+
when ::Rational then quote_number(value)
|
|
1163
|
+
else Arel::Nodes.build_quoted(value)
|
|
1164
|
+
end
|
|
1165
|
+
end
|
|
1166
|
+
|
|
1167
|
+
# The keys come from Ruby as Hash keys rather than alternating with
|
|
1168
|
+
# the values as SQL has them, which is what keeps a bare symbol
|
|
1169
|
+
# free to mean a column on the value side. Anything but a name is
|
|
1170
|
+
# refused here, before the adapters answer a NULL key three ways.
|
|
1171
|
+
def check_pairs(pairs)
|
|
1172
|
+
unless pairs.is_a?(::Hash)
|
|
1173
|
+
raise ArgumentError,
|
|
1174
|
+
"json_object takes a hash of keys to values, not #{pairs.inspect}"
|
|
1175
|
+
end
|
|
1176
|
+
pairs.each_key do |key|
|
|
1177
|
+
next if key.is_a?(::String) || key.is_a?(::Symbol)
|
|
1178
|
+
raise ArgumentError,
|
|
1179
|
+
"a key of json_object is a string or a symbol, not #{key.inspect}"
|
|
1180
|
+
end
|
|
1181
|
+
pairs
|
|
1182
|
+
end
|
|
1183
|
+
|
|
1184
|
+
def json_source
|
|
1185
|
+
"json_#{kind}"
|
|
1186
|
+
end
|
|
1187
|
+
end
|
|
1188
|
+
|
|
784
1189
|
# GROUP BY GROUPING SETS / ROLLUP / CUBE: several groupings asked for at
|
|
785
1190
|
# once, the totals of each coming back beside the rows. PostgreSQL has
|
|
786
|
-
# all three; the block raises for the
|
|
1191
|
+
# all three and the MySQL family rollup alone; the block raises for the
|
|
1192
|
+
# rest before it gets this far.
|
|
787
1193
|
#
|
|
788
1194
|
# Each set is a list of its own, so grouping_sets takes lists and rollup
|
|
789
1195
|
# and cube take the columns themselves.
|
|
@@ -803,16 +1209,32 @@ module ActiveRecord
|
|
|
803
1209
|
end
|
|
804
1210
|
|
|
805
1211
|
def to_arel(table, model)
|
|
1212
|
+
return with_rollup(table, model) if AST.adapter_family(model) == :mysql
|
|
1213
|
+
|
|
806
1214
|
KINDS.fetch(kind).new(
|
|
807
1215
|
if kind == :grouping_sets
|
|
808
1216
|
sets.map do |set|
|
|
809
1217
|
Arel::Nodes::GroupingElement.new(
|
|
810
|
-
Array(set).map {|column| to_arel_operand(column, table, model) })
|
|
1218
|
+
Array(set).map { |column| to_arel_operand(column, table, model) })
|
|
811
1219
|
end
|
|
812
1220
|
else
|
|
813
|
-
sets.map {|column| to_arel_operand(column, table, model) }
|
|
1221
|
+
sets.map { |column| to_arel_operand(column, table, model) }
|
|
814
1222
|
end)
|
|
815
1223
|
end
|
|
1224
|
+
|
|
1225
|
+
private
|
|
1226
|
+
# The MySQL family spells rollup WITH ROLLUP, trailing the whole
|
|
1227
|
+
# group list rather than wrapping a list of its own -- which is also
|
|
1228
|
+
# why a rollup cannot stand beside other group entries there. The
|
|
1229
|
+
# columns are compiled by the connection's own visitor, so their
|
|
1230
|
+
# quoting is the adapter's.
|
|
1231
|
+
def with_rollup(table, model)
|
|
1232
|
+
columns = sets.map { |column| to_arel_operand(column, table, model) }
|
|
1233
|
+
sql = model.with_connection do |connection|
|
|
1234
|
+
columns.map { |column| connection.visitor.compile(column) }.join(", ")
|
|
1235
|
+
end
|
|
1236
|
+
Arel.sql("#{sql} WITH ROLLUP")
|
|
1237
|
+
end
|
|
816
1238
|
end
|
|
817
1239
|
|
|
818
1240
|
class Column < Node
|
|
@@ -847,8 +1269,11 @@ module ActiveRecord
|
|
|
847
1269
|
end
|
|
848
1270
|
|
|
849
1271
|
def to_arel(table, model)
|
|
850
|
-
to_arel_operand(left, table, model)
|
|
851
|
-
|
|
1272
|
+
arel_left = to_arel_operand(left, table, model)
|
|
1273
|
+
# The operator dispatches Arel's Math, which a bare number carries
|
|
1274
|
+
# none of; quoted, it is a node with the same methods.
|
|
1275
|
+
arel_left = Arel::Nodes.build_quoted(arel_left) if arel_left.is_a?(::Numeric)
|
|
1276
|
+
arel_left.public_send(operator, to_arel_operand(right, table, model))
|
|
852
1277
|
end
|
|
853
1278
|
end
|
|
854
1279
|
|
|
@@ -859,23 +1284,22 @@ module ActiveRecord
|
|
|
859
1284
|
# PostgreSQL has no such operator and would say so.
|
|
860
1285
|
module BitwiseOperands
|
|
861
1286
|
private
|
|
1287
|
+
def check_operand(operand, operator)
|
|
1288
|
+
return operand unless operand.is_a?(Predicate)
|
|
1289
|
+
raise ArgumentError,
|
|
1290
|
+
"a condition cannot be an operand of #{operator}; " \
|
|
1291
|
+
"& and | between conditions are AND and OR"
|
|
1292
|
+
end
|
|
862
1293
|
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
def check_not_boolean(operand, operator, model)
|
|
873
|
-
return unless operand.is_a?(::Symbol)
|
|
874
|
-
return unless model.type_for_attribute(operand).type == :boolean
|
|
875
|
-
raise ArgumentError,
|
|
876
|
-
"#{operand.inspect} is a boolean column, which #{operator} does " \
|
|
877
|
-
"not take; #{operand.inspect}.true? is the condition"
|
|
878
|
-
end
|
|
1294
|
+
# Only the unqualified column can be checked, since that is the one
|
|
1295
|
+
# the model is known to have.
|
|
1296
|
+
def check_not_boolean(operand, operator, model)
|
|
1297
|
+
return unless operand.is_a?(::Symbol)
|
|
1298
|
+
return unless model.type_for_attribute(operand).type == :boolean
|
|
1299
|
+
raise ArgumentError,
|
|
1300
|
+
"#{operand.inspect} is a boolean column, which #{operator} does " \
|
|
1301
|
+
"not take; #{operand.inspect}.true? is the condition"
|
|
1302
|
+
end
|
|
879
1303
|
end
|
|
880
1304
|
|
|
881
1305
|
# SQL's bitwise operators. Each parenthesises itself, which is what
|
|
@@ -915,22 +1339,21 @@ module ActiveRecord
|
|
|
915
1339
|
end
|
|
916
1340
|
|
|
917
1341
|
private
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
1342
|
+
# Arel has a node for XOR, but it writes ^ on every adapter, and ^ is
|
|
1343
|
+
# exponentiation to PostgreSQL -- a wrong answer rather than an error.
|
|
1344
|
+
# PostgreSQL's own spelling, #, is where a comment starts on MySQL, so
|
|
1345
|
+
# it cannot be the portable one either. SQLite has no XOR at all;
|
|
1346
|
+
# (a | b) - (a & b) is it, at the cost of naming each operand twice.
|
|
1347
|
+
def xor(left, right, model)
|
|
1348
|
+
case AST.adapter_family(model)
|
|
1349
|
+
when :postgresql then Arel::Nodes::InfixOperation.new("#", left, right)
|
|
1350
|
+
when :mysql then Arel::Nodes::BitwiseXor.new(left, right)
|
|
1351
|
+
else
|
|
1352
|
+
Arel::Nodes::Subtraction.new(
|
|
1353
|
+
Arel::Nodes::Grouping.new(Arel::Nodes::BitwiseOr.new(left, right)),
|
|
1354
|
+
Arel::Nodes::Grouping.new(Arel::Nodes::BitwiseAnd.new(left, right)))
|
|
1355
|
+
end
|
|
932
1356
|
end
|
|
933
|
-
end
|
|
934
1357
|
end
|
|
935
1358
|
|
|
936
1359
|
# ~, which every adapter has. MySQL answers with the unsigned 64-bit
|
|
@@ -998,10 +1421,14 @@ module ActiveRecord
|
|
|
998
1421
|
|
|
999
1422
|
def to_arel(table, model)
|
|
1000
1423
|
window = Arel::Nodes::Window.new
|
|
1001
|
-
partitions.each {|expr| window.partition(to_arel_operand(expr, table, model)) }
|
|
1002
|
-
orders.each {|expr| window.order(to_arel_operand(expr, table, model)) }
|
|
1424
|
+
partitions.each { |expr| window.partition(to_arel_operand(expr, table, model)) }
|
|
1425
|
+
orders.each { |expr| window.order(to_arel_operand(expr, table, model)) }
|
|
1003
1426
|
frame_arel(window) if frame
|
|
1004
1427
|
|
|
1428
|
+
# The JSON aggregates cannot ride a window everywhere; the node
|
|
1429
|
+
# itself says where, once the adapter is known.
|
|
1430
|
+
function.check_window(model) if function.is_a?(JsonAggregate)
|
|
1431
|
+
|
|
1005
1432
|
# A window-only function refuses to build on its own; here is where
|
|
1006
1433
|
# it is asked for the call itself.
|
|
1007
1434
|
arel_function =
|
|
@@ -1011,43 +1438,42 @@ module ActiveRecord
|
|
|
1011
1438
|
end
|
|
1012
1439
|
|
|
1013
1440
|
private
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1441
|
+
# The frame is a range of rows counted from the current one: negative
|
|
1442
|
+
# before it, positive after, 0 the row itself, and an open end for
|
|
1443
|
+
# unbounded. `rows(..0)` is what a running total wants.
|
|
1444
|
+
def framing(kind, bounds)
|
|
1445
|
+
raise ArgumentError, "a window has one frame" if frame
|
|
1446
|
+
unless bounds.is_a?(::Range)
|
|
1447
|
+
raise ArgumentError, "#{kind} takes a range of rows, as in rows(..0)"
|
|
1448
|
+
end
|
|
1449
|
+
if bounds.exclude_end?
|
|
1450
|
+
raise ArgumentError, "a frame ends on a row rather than before one; use .."
|
|
1451
|
+
end
|
|
1452
|
+
[bounds.begin, bounds.end].each do |bound|
|
|
1453
|
+
next if bound.nil? || bound.is_a?(::Integer)
|
|
1454
|
+
raise ArgumentError,
|
|
1455
|
+
"a frame bound is a number of rows, or nothing for unbounded"
|
|
1456
|
+
end
|
|
1457
|
+
[kind, bounds.begin, bounds.end]
|
|
1030
1458
|
end
|
|
1031
|
-
[kind, bounds.begin, bounds.end]
|
|
1032
|
-
end
|
|
1033
1459
|
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1460
|
+
# Arel wants the keyword itself on the left of the BETWEEN, which is
|
|
1461
|
+
# what window.rows with no argument hands back.
|
|
1462
|
+
def frame_arel(window)
|
|
1463
|
+
kind, from, to = frame
|
|
1464
|
+
window.frame(
|
|
1465
|
+
Arel::Nodes::Between.new(
|
|
1466
|
+
window.public_send(kind),
|
|
1467
|
+
Arel::Nodes::And.new([bound(from, Arel::Nodes::Preceding.new),
|
|
1468
|
+
bound(to, Arel::Nodes::Following.new)])))
|
|
1469
|
+
end
|
|
1044
1470
|
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1471
|
+
def bound(rows, unbounded)
|
|
1472
|
+
return unbounded if rows.nil?
|
|
1473
|
+
return Arel::Nodes::CurrentRow.new if rows.zero?
|
|
1474
|
+
rows.negative? ? Arel::Nodes::Preceding.new(-rows)
|
|
1475
|
+
: Arel::Nodes::Following.new(rows)
|
|
1476
|
+
end
|
|
1051
1477
|
end
|
|
1052
1478
|
|
|
1053
1479
|
class Aggregate < Node
|
|
@@ -1093,15 +1519,80 @@ module ActiveRecord
|
|
|
1093
1519
|
end
|
|
1094
1520
|
|
|
1095
1521
|
private
|
|
1522
|
+
def aggregate(over, table, model)
|
|
1523
|
+
arel_operand = to_arel_operand(over, table, model)
|
|
1524
|
+
if function == :count
|
|
1525
|
+
arel_operand.count(distinct)
|
|
1526
|
+
else
|
|
1527
|
+
arel_operand.public_send(function)
|
|
1528
|
+
end
|
|
1529
|
+
end
|
|
1530
|
+
end
|
|
1096
1531
|
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1532
|
+
# Rows gathered into one JSON document: json_arrayagg collects a value
|
|
1533
|
+
# from each row into an array, json_objectagg a key and a value into an
|
|
1534
|
+
# object. Every adapter has the pair under a name of its own; what
|
|
1535
|
+
# PostgreSQL gets is the jsonb one, whose documents the other JSON
|
|
1536
|
+
# operations here read.
|
|
1537
|
+
class JsonAggregate < Node
|
|
1538
|
+
include Predications
|
|
1539
|
+
include JsonComparable
|
|
1540
|
+
include ComputedJson
|
|
1541
|
+
include Windowing
|
|
1542
|
+
|
|
1543
|
+
# The standard names, which are also the DSL's own and MySQL's, serve
|
|
1544
|
+
# any adapter the table does not list.
|
|
1545
|
+
NAMES = {
|
|
1546
|
+
arrayagg: { sqlite: "json_group_array", postgresql: "jsonb_agg" },
|
|
1547
|
+
objectagg: { sqlite: "json_group_object",
|
|
1548
|
+
postgresql: "jsonb_object_agg" },
|
|
1549
|
+
}.freeze
|
|
1550
|
+
|
|
1551
|
+
attr_reader :kind, :operands, :condition
|
|
1552
|
+
|
|
1553
|
+
def initialize(kind, operands, condition: nil)
|
|
1554
|
+
@kind = kind
|
|
1555
|
+
@operands = operands
|
|
1556
|
+
@condition = condition
|
|
1557
|
+
end
|
|
1558
|
+
|
|
1559
|
+
def filter(condition = nil, &block)
|
|
1560
|
+
JsonAggregate.new(kind, operands,
|
|
1561
|
+
condition: Case.argument(:filter, condition, block))
|
|
1562
|
+
end
|
|
1563
|
+
|
|
1564
|
+
# MariaDB takes every other aggregate as a window function, but not
|
|
1565
|
+
# these two; Over asks here before writing one.
|
|
1566
|
+
def check_window(model)
|
|
1567
|
+
return unless AST.adapter_family(model) == :mysql
|
|
1568
|
+
return unless model.with_connection { |connection| connection.mariadb? }
|
|
1569
|
+
raise NotImplementedError,
|
|
1570
|
+
"#{json_source} over a window has no equivalent on MariaDB"
|
|
1571
|
+
end
|
|
1572
|
+
|
|
1573
|
+
def to_arel(table, model)
|
|
1574
|
+
family = AST.adapter_family(model)
|
|
1575
|
+
call = Arel::Nodes::NamedFunction.new(
|
|
1576
|
+
NAMES.fetch(kind).fetch(family) { "JSON_#{kind.to_s.upcase}" },
|
|
1577
|
+
operands.map { |operand| to_arel_argument(operand, table, model) })
|
|
1578
|
+
return call unless condition
|
|
1579
|
+
|
|
1580
|
+
# The CASE that stands in for FILTER elsewhere hands the aggregate
|
|
1581
|
+
# a NULL for every row the condition misses, and these two keep a
|
|
1582
|
+
# NULL -- as JSON null -- rather than passing over it.
|
|
1583
|
+
if family == :mysql
|
|
1584
|
+
raise NotImplementedError,
|
|
1585
|
+
"#{json_source}.filter has no equivalent on " \
|
|
1586
|
+
"#{model.connection_db_config.adapter}; a CASE would leave a " \
|
|
1587
|
+
"null in the document for every row it drops"
|
|
1103
1588
|
end
|
|
1589
|
+
call.filter(condition.to_arel(table, model))
|
|
1104
1590
|
end
|
|
1591
|
+
|
|
1592
|
+
private
|
|
1593
|
+
def json_source
|
|
1594
|
+
"json_#{kind}"
|
|
1595
|
+
end
|
|
1105
1596
|
end
|
|
1106
1597
|
|
|
1107
1598
|
# A column alias, quoted by the adapter, so that the name asked for is
|
|
@@ -1130,12 +1621,11 @@ module ActiveRecord
|
|
|
1130
1621
|
end
|
|
1131
1622
|
|
|
1132
1623
|
private
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
end
|
|
1624
|
+
def alias_sql(model)
|
|
1625
|
+
name = alias_name.to_s
|
|
1626
|
+
return name unless quote
|
|
1627
|
+
model.with_connection { |connection| connection.quote_column_name(name) }
|
|
1628
|
+
end
|
|
1139
1629
|
end
|
|
1140
1630
|
|
|
1141
1631
|
class Ordering < Node
|
|
@@ -1176,11 +1666,54 @@ module ActiveRecord
|
|
|
1176
1666
|
end
|
|
1177
1667
|
|
|
1178
1668
|
def to_arel(table, model)
|
|
1179
|
-
arel_args = args.map {|arg| to_arel_argument(arg, table, model) }
|
|
1669
|
+
arel_args = args.map { |arg| to_arel_argument(arg, table, model) }
|
|
1180
1670
|
Arel::Nodes::NamedFunction.new(name, arel_args)
|
|
1181
1671
|
end
|
|
1182
1672
|
end
|
|
1183
1673
|
|
|
1674
|
+
# Escape hatch for operators without a spelling of their own, the way
|
|
1675
|
+
# fn is for functions. The operator is emitted as written -- whether
|
|
1676
|
+
# the adapter has it is the caller's assertion, as fn's names are --
|
|
1677
|
+
# and the values ride as quoted literals, so on PostgreSQL an untyped
|
|
1678
|
+
# one takes the type of the operand beside it.
|
|
1679
|
+
class Operation < Node
|
|
1680
|
+
include Predications
|
|
1681
|
+
include Arithmetics
|
|
1682
|
+
|
|
1683
|
+
attr_reader :operator, :left, :right
|
|
1684
|
+
|
|
1685
|
+
def initialize(operator, left, right)
|
|
1686
|
+
@operator = AST.check_name(operator, OPERATOR, "operator").to_s
|
|
1687
|
+
@left = check_side(left)
|
|
1688
|
+
@right = check_side(right)
|
|
1689
|
+
end
|
|
1690
|
+
|
|
1691
|
+
def to_arel(table, model)
|
|
1692
|
+
Arel::Nodes::Grouping.new(
|
|
1693
|
+
Arel::Nodes::InfixOperation.new(
|
|
1694
|
+
operator, side(left, table, model), side(right, table, model)))
|
|
1695
|
+
end
|
|
1696
|
+
|
|
1697
|
+
private
|
|
1698
|
+
# An expression operand is parenthesized: an unknown operator's
|
|
1699
|
+
# precedence is unknown too, and PostgreSQL reads its named
|
|
1700
|
+
# operators from the left, so a bare infix on the right would take
|
|
1701
|
+
# the new operator's left side into its own.
|
|
1702
|
+
def side(operand, table, model)
|
|
1703
|
+
arel = to_arel_argument(operand, table, model)
|
|
1704
|
+
operand.is_a?(Node) ? Arel::Nodes::Grouping.new(arel) : arel
|
|
1705
|
+
end
|
|
1706
|
+
|
|
1707
|
+
def check_side(operand)
|
|
1708
|
+
if operand.is_a?(::Hash) || operand.is_a?(::Array) || operand.is_a?(::Set)
|
|
1709
|
+
raise ArgumentError,
|
|
1710
|
+
"#{operand.inspect} has no one SQL spelling; a string says it " \
|
|
1711
|
+
"in the adapter's own, to_json for a document"
|
|
1712
|
+
end
|
|
1713
|
+
operand
|
|
1714
|
+
end
|
|
1715
|
+
end
|
|
1716
|
+
|
|
1184
1717
|
# ROW_NUMBER and its kind: functions that say nothing without a window.
|
|
1185
1718
|
# On its own this refuses rather than reaching the database as an error
|
|
1186
1719
|
# there; over asks it for call_arel instead.
|
|
@@ -1283,24 +1816,24 @@ module ActiveRecord
|
|
|
1283
1816
|
arel_value =
|
|
1284
1817
|
case value
|
|
1285
1818
|
when Node then value.to_arel(table, model)
|
|
1819
|
+
when ::Symbol then column_operand(value, table, model)
|
|
1286
1820
|
when ActiveRecord::Relation then scalar_subquery(value)
|
|
1287
|
-
else value
|
|
1821
|
+
else quote_number(value)
|
|
1288
1822
|
end
|
|
1289
1823
|
arel_column.public_send(OPERATOR_MAP.fetch(operator), arel_value)
|
|
1290
1824
|
end
|
|
1291
1825
|
|
|
1292
1826
|
private
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1827
|
+
# A relation compared against a column has to yield a single value, so
|
|
1828
|
+
# unlike In there is no sensible default select list to fall back on.
|
|
1829
|
+
def scalar_subquery(relation)
|
|
1830
|
+
if relation.select_values.empty?
|
|
1831
|
+
raise ArgumentError,
|
|
1832
|
+
"#{operator} needs a subquery selecting one value; add a select"
|
|
1833
|
+
end
|
|
1834
|
+
relation = relation.send(:apply_join_dependency) if relation.eager_loading?
|
|
1835
|
+
relation.arel
|
|
1300
1836
|
end
|
|
1301
|
-
relation = relation.send(:apply_join_dependency) if relation.eager_loading?
|
|
1302
|
-
relation.arel
|
|
1303
|
-
end
|
|
1304
1837
|
end
|
|
1305
1838
|
|
|
1306
1839
|
# IS TRUE, IS FALSE and their negations, which every adapter spells the
|
|
@@ -1316,7 +1849,7 @@ module ActiveRecord
|
|
|
1316
1849
|
|
|
1317
1850
|
def to_arel(table, model)
|
|
1318
1851
|
literal = value ? Arel::Nodes::True.new : Arel::Nodes::False.new
|
|
1319
|
-
Arel::Nodes::InfixOperation.new(negated ?
|
|
1852
|
+
Arel::Nodes::InfixOperation.new(negated ? "IS NOT" : "IS",
|
|
1320
1853
|
to_arel_operand(operand, table, model), literal)
|
|
1321
1854
|
end
|
|
1322
1855
|
end
|
|
@@ -1327,19 +1860,18 @@ module ActiveRecord
|
|
|
1327
1860
|
# selects the model's primary key.
|
|
1328
1861
|
module SetSubquery
|
|
1329
1862
|
private
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1863
|
+
def set_subquery(relation, spelling)
|
|
1864
|
+
relation = relation.send(:apply_join_dependency) if relation.eager_loading?
|
|
1865
|
+
if relation.select_values.empty?
|
|
1866
|
+
model = relation.model
|
|
1867
|
+
if model.composite_primary_key?
|
|
1868
|
+
raise ArgumentError,
|
|
1869
|
+
"Cannot map composite primary key #{model.primary_key} to #{spelling}"
|
|
1870
|
+
end
|
|
1871
|
+
relation = relation.select(relation.table[model.primary_key])
|
|
1338
1872
|
end
|
|
1339
|
-
relation
|
|
1873
|
+
relation.arel
|
|
1340
1874
|
end
|
|
1341
|
-
relation.arel
|
|
1342
|
-
end
|
|
1343
1875
|
end
|
|
1344
1876
|
|
|
1345
1877
|
# IN for a list of values, BETWEEN for a range, IN (SELECT ...) for a
|
|
@@ -1347,6 +1879,12 @@ module ActiveRecord
|
|
|
1347
1879
|
class In < Predicate
|
|
1348
1880
|
include SetSubquery
|
|
1349
1881
|
|
|
1882
|
+
# Range holds its endpoints to Comparable, which a quoted node is
|
|
1883
|
+
# not, so this quacks the three methods Arel's between reads.
|
|
1884
|
+
QuotedRange = Struct.new(:begin, :end, :exclude_end) do
|
|
1885
|
+
def exclude_end? = exclude_end
|
|
1886
|
+
end
|
|
1887
|
+
|
|
1350
1888
|
attr_reader :operand, :values, :negated
|
|
1351
1889
|
|
|
1352
1890
|
def initialize(operand, values, negated: false)
|
|
@@ -1357,13 +1895,67 @@ module ActiveRecord
|
|
|
1357
1895
|
|
|
1358
1896
|
def to_arel(table, model)
|
|
1359
1897
|
arel_operand = to_arel_operand(operand, table, model)
|
|
1360
|
-
|
|
1361
|
-
|
|
1898
|
+
case values
|
|
1899
|
+
when Range, QuotedRange
|
|
1900
|
+
lower = quote_value(values.begin, table, model)
|
|
1901
|
+
upper = quote_value(values.end, table, model)
|
|
1902
|
+
if json_between?(model) && lower && upper && !negated
|
|
1903
|
+
return arel_operand.gteq(lower).and(
|
|
1904
|
+
values.exclude_end? ? arel_operand.lt(upper) : arel_operand.lteq(upper))
|
|
1905
|
+
end
|
|
1906
|
+
range = QuotedRange.new(lower, upper, values.exclude_end?)
|
|
1907
|
+
arel_operand.public_send(negated ? :not_between : :between, range)
|
|
1908
|
+
when ActiveRecord::Relation
|
|
1909
|
+
arel_operand.public_send(negated ? :not_in : :in, set_subquery(values, "IN"))
|
|
1362
1910
|
else
|
|
1363
|
-
arg = values
|
|
1911
|
+
arg = values
|
|
1912
|
+
if arg.is_a?(::Array)
|
|
1913
|
+
arg = arg.map { |value| quote_value(value, table, model) }
|
|
1914
|
+
return json_list(arel_operand, arg) if json_list?(model)
|
|
1915
|
+
end
|
|
1364
1916
|
arel_operand.public_send(negated ? :not_in : :in, arg)
|
|
1365
1917
|
end
|
|
1366
1918
|
end
|
|
1919
|
+
|
|
1920
|
+
private
|
|
1921
|
+
# An element that is already an expression resolves, a symbol is a
|
|
1922
|
+
# column here as everywhere, a number is quoted as itself, and the
|
|
1923
|
+
# rest ride for Arel to cast by the column.
|
|
1924
|
+
def quote_value(value, table, model)
|
|
1925
|
+
case value
|
|
1926
|
+
when Node then value.to_arel(table, model)
|
|
1927
|
+
when ::Symbol then column_operand(value, table, model)
|
|
1928
|
+
else quote_number(value)
|
|
1929
|
+
end
|
|
1930
|
+
end
|
|
1931
|
+
|
|
1932
|
+
# MySQL leaves IN and BETWEEN out of its JSON comparisons -- they
|
|
1933
|
+
# fall back to another comparison entirely -- so on it a JSON set is
|
|
1934
|
+
# spelled as the comparisons it means: the closed range as its two
|
|
1935
|
+
# bounds, the list as one equality per element. That names the dug
|
|
1936
|
+
# value once per element, the price SQLite's XOR pays per operand;
|
|
1937
|
+
# a negated range needs nothing, Arel writing it as two comparisons
|
|
1938
|
+
# everywhere. MariaDB never gets this far: the endpoints refuse as
|
|
1939
|
+
# they resolve.
|
|
1940
|
+
def json_between?(model)
|
|
1941
|
+
(values.begin.is_a?(JsonLiteral) || values.end.is_a?(JsonLiteral)) &&
|
|
1942
|
+
AST.adapter_family(model) == :mysql
|
|
1943
|
+
end
|
|
1944
|
+
|
|
1945
|
+
def json_list?(model)
|
|
1946
|
+
values.any? { |value| value.is_a?(JsonLiteral) } &&
|
|
1947
|
+
AST.adapter_family(model) == :mysql
|
|
1948
|
+
end
|
|
1949
|
+
|
|
1950
|
+
def json_list(arel_operand, elements)
|
|
1951
|
+
comparisons = elements.map do |element|
|
|
1952
|
+
negated ? arel_operand.not_eq(element) : arel_operand.eq(element)
|
|
1953
|
+
end
|
|
1954
|
+
joined = comparisons.inject do |so_far, piece|
|
|
1955
|
+
negated ? so_far.and(piece) : so_far.or(piece)
|
|
1956
|
+
end
|
|
1957
|
+
negated ? Arel::Nodes::Grouping.new(joined) : joined
|
|
1958
|
+
end
|
|
1367
1959
|
end
|
|
1368
1960
|
|
|
1369
1961
|
# ANY and ALL, which stand on the right of a comparison and say how many
|
|
@@ -1411,7 +2003,7 @@ module ActiveRecord
|
|
|
1411
2003
|
end
|
|
1412
2004
|
|
|
1413
2005
|
class Like < Predicate
|
|
1414
|
-
ESCAPE = "\\"
|
|
2006
|
+
ESCAPE = "\\"
|
|
1415
2007
|
|
|
1416
2008
|
# Escapes % and _ so that they match literally. The pattern built from
|
|
1417
2009
|
# the result must be used with ESCAPE, since SQLite has no default
|
|
@@ -1423,8 +2015,8 @@ module ActiveRecord
|
|
|
1423
2015
|
# ORs one LIKE per pattern, for the shortcuts that accept several
|
|
1424
2016
|
# literals the way String#start_with? does.
|
|
1425
2017
|
def self.any(operand, patterns)
|
|
1426
|
-
patterns.map {|pattern| new(operand, pattern, ESCAPE) }.
|
|
1427
|
-
inject {|left, right| Or.new(left, right) }
|
|
2018
|
+
patterns.map { |pattern| new(operand, pattern, ESCAPE) }.
|
|
2019
|
+
inject { |left, right| Or.new(left, right) }
|
|
1428
2020
|
end
|
|
1429
2021
|
|
|
1430
2022
|
attr_reader :operand, :pattern, :escape, :case_sensitive, :negated
|
|
@@ -1508,21 +2100,20 @@ module ActiveRecord
|
|
|
1508
2100
|
end
|
|
1509
2101
|
|
|
1510
2102
|
private
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
2103
|
+
# PostgreSQL array input syntax: elements joined by commas inside
|
|
2104
|
+
# braces, and an element is double-quoted whenever it is empty, spells
|
|
2105
|
+
# NULL, or contains a character the parser treats specially.
|
|
2106
|
+
def array_literal
|
|
2107
|
+
encoded = elements.map do |value|
|
|
2108
|
+
s = value.to_s
|
|
2109
|
+
if s.empty? || s.casecmp?("null") || s.match?(/[\s{},"\\]/)
|
|
2110
|
+
"\"#{s.gsub(/["\\]/) { |c| "\\#{c}" }}\""
|
|
2111
|
+
else
|
|
2112
|
+
s
|
|
2113
|
+
end
|
|
1522
2114
|
end
|
|
2115
|
+
"{#{encoded.join(',')}}"
|
|
1523
2116
|
end
|
|
1524
|
-
"{#{encoded.join(',')}}"
|
|
1525
|
-
end
|
|
1526
2117
|
end
|
|
1527
2118
|
|
|
1528
2119
|
# Regular expression match: REGEXP on MySQL, ~ on PostgreSQL. SQLite has
|
|
@@ -1546,19 +2137,18 @@ module ActiveRecord
|
|
|
1546
2137
|
end
|
|
1547
2138
|
|
|
1548
2139
|
private
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
2140
|
+
# A Regexp literal reads naturally with =~, but only its source crosses
|
|
2141
|
+
# over; the database has its own dialect and no notion of Ruby's flags.
|
|
2142
|
+
# Dropping a flag would silently change what the query matches, so
|
|
2143
|
+
# anything beyond a plain literal is refused rather than ignored.
|
|
2144
|
+
def regexp_source(regexp)
|
|
2145
|
+
unless regexp.options.zero?
|
|
2146
|
+
raise ArgumentError,
|
|
2147
|
+
"#{regexp.inspect} has options that SQL cannot express; " \
|
|
2148
|
+
"pass the pattern as a string instead"
|
|
2149
|
+
end
|
|
2150
|
+
regexp.source
|
|
1559
2151
|
end
|
|
1560
|
-
regexp.source
|
|
1561
|
-
end
|
|
1562
2152
|
end
|
|
1563
2153
|
|
|
1564
2154
|
class And < Predicate
|