activerecord-refined 0.8.1 → 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 +239 -71
- 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 +828 -299
- 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 +843 -337
- 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,11 +673,12 @@ 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
|
-
#
|
|
528
|
-
#
|
|
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.
|
|
529
682
|
#
|
|
530
683
|
# A string against dig_text, and anything the block itself built -- a
|
|
531
684
|
# column, a function, another dug value -- go through untouched.
|
|
@@ -537,15 +690,14 @@ module ActiveRecord
|
|
|
537
690
|
module JsonComparable
|
|
538
691
|
%i[== != < <= > >=].each do |operator|
|
|
539
692
|
define_method(operator) do |other|
|
|
540
|
-
|
|
541
|
-
super(other)
|
|
693
|
+
super(comparison_value(other))
|
|
542
694
|
end
|
|
543
695
|
end
|
|
544
696
|
|
|
545
|
-
def in?(values) = super(
|
|
546
|
-
def not_in?(values) = super(
|
|
547
|
-
def between?(min, max) = super(
|
|
548
|
-
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))
|
|
549
701
|
|
|
550
702
|
%i[+ - * / & | ^ << >>].each do |operator|
|
|
551
703
|
define_method(operator) do |_other|
|
|
@@ -558,39 +710,91 @@ module ActiveRecord
|
|
|
558
710
|
end
|
|
559
711
|
|
|
560
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)
|
|
561
722
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
other.is_a?(Arel::Nodes::Node) ||
|
|
567
|
-
other.is_a?(Arel::Attributes::Attribute) ||
|
|
568
|
-
other.is_a?(ActiveRecord::Relation)
|
|
569
|
-
return if other.is_a?(::String) && !as_json
|
|
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
|
|
570
727
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
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
|
|
577
741
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
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
|
|
583
750
|
end
|
|
584
|
-
|
|
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
|
|
760
|
+
|
|
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
|
|
585
774
|
end
|
|
586
775
|
|
|
587
|
-
def
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
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)
|
|
782
|
+
end
|
|
593
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
|
|
594
798
|
end
|
|
595
799
|
|
|
596
800
|
# The JSON operations read a document, and what dig gives is one:
|
|
@@ -600,9 +804,9 @@ module ActiveRecord
|
|
|
600
804
|
# again is where they part company: SQLite parses it back and MySQL
|
|
601
805
|
# takes it as written, where PostgreSQL has no such function for text.
|
|
602
806
|
module JsonDocument
|
|
603
|
-
%i[dig dig_text key? contains? bury except].each do |name|
|
|
807
|
+
%i[dig dig_text key? keys contains? bury except].each do |name|
|
|
604
808
|
define_method(name) do |*args|
|
|
605
|
-
unless
|
|
809
|
+
unless json_value?
|
|
606
810
|
raise ArgumentError,
|
|
607
811
|
"dig_text gives text, and #{name} reads JSON; dig keeps it"
|
|
608
812
|
end
|
|
@@ -611,6 +815,22 @@ module ActiveRecord
|
|
|
611
815
|
end
|
|
612
816
|
end
|
|
613
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
|
+
|
|
614
834
|
class JsonPath < Node
|
|
615
835
|
include Predications
|
|
616
836
|
include Arithmetics
|
|
@@ -618,12 +838,16 @@ module ActiveRecord
|
|
|
618
838
|
include JsonComparable
|
|
619
839
|
include JsonDocument
|
|
620
840
|
|
|
621
|
-
attr_reader :operand, :path
|
|
841
|
+
attr_reader :operand, :path
|
|
622
842
|
|
|
623
|
-
def initialize(operand, path,
|
|
843
|
+
def initialize(operand, path, json_value: true)
|
|
624
844
|
@operand = operand
|
|
625
|
-
@path = check_steps(path,
|
|
626
|
-
@
|
|
845
|
+
@path = check_steps(path, "dig")
|
|
846
|
+
@json_value = json_value
|
|
847
|
+
end
|
|
848
|
+
|
|
849
|
+
def json_value?
|
|
850
|
+
@json_value
|
|
627
851
|
end
|
|
628
852
|
|
|
629
853
|
def to_arel(table, model)
|
|
@@ -631,27 +855,26 @@ module ActiveRecord
|
|
|
631
855
|
case AST.adapter_family(model)
|
|
632
856
|
when :postgresql
|
|
633
857
|
Arel::Nodes::InfixOperation.new(
|
|
634
|
-
|
|
858
|
+
json_value? ? :"#>" : :"#>>", document, Arel::Nodes.build_quoted(steps_array))
|
|
635
859
|
when :mysql
|
|
636
860
|
extracted = Arel::Nodes::NamedFunction.new(
|
|
637
|
-
|
|
638
|
-
|
|
861
|
+
"JSON_EXTRACT", [document, Arel::Nodes.build_quoted(dollar_path)])
|
|
862
|
+
json_value? ? extracted : Arel::Nodes::NamedFunction.new("JSON_UNQUOTE", [extracted])
|
|
639
863
|
else
|
|
640
864
|
extracted = Arel::Nodes::InfixOperation.new(
|
|
641
|
-
|
|
865
|
+
json_value? ? :"->" : :"->>", document, Arel::Nodes.build_quoted(dollar_path))
|
|
642
866
|
# SQLite's ->> gives back the value with its type, where the other
|
|
643
867
|
# two give text. Cast so that `dig_text(:n) == '5'` means the
|
|
644
868
|
# same thing everywhere, and a number wants a cast everywhere too.
|
|
645
|
-
|
|
646
|
-
|
|
869
|
+
json_value? ? extracted : Arel::Nodes::NamedFunction.new(
|
|
870
|
+
"CAST", [Arel::Nodes::As.new(extracted, Arel::Nodes::SqlLiteral.new("text"))])
|
|
647
871
|
end
|
|
648
872
|
end
|
|
649
873
|
|
|
650
874
|
private
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
end
|
|
875
|
+
def json_source
|
|
876
|
+
"dig"
|
|
877
|
+
end
|
|
655
878
|
end
|
|
656
879
|
|
|
657
880
|
# Setting a value inside a JSON document, which is what bury does to what
|
|
@@ -666,12 +889,12 @@ module ActiveRecord
|
|
|
666
889
|
|
|
667
890
|
def initialize(operand, path, value)
|
|
668
891
|
@operand = operand
|
|
669
|
-
@path = check_steps(path,
|
|
892
|
+
@path = check_steps(path, "bury")
|
|
670
893
|
@value = value
|
|
671
894
|
end
|
|
672
895
|
|
|
673
896
|
# Always JSON, which is what the comparison guard asks.
|
|
674
|
-
def
|
|
897
|
+
def json_value?
|
|
675
898
|
true
|
|
676
899
|
end
|
|
677
900
|
|
|
@@ -679,54 +902,44 @@ module ActiveRecord
|
|
|
679
902
|
document = to_arel_operand(operand, table, model)
|
|
680
903
|
if AST.adapter_family(model) == :postgresql
|
|
681
904
|
Arel::Nodes::NamedFunction.new(
|
|
682
|
-
|
|
905
|
+
"jsonb_set",
|
|
683
906
|
[document, Arel::Nodes.build_quoted(steps_array), postgresql_value(table, model)])
|
|
684
907
|
else
|
|
685
908
|
Arel::Nodes::NamedFunction.new(
|
|
686
|
-
|
|
909
|
+
"JSON_SET",
|
|
687
910
|
[document, Arel::Nodes.build_quoted(dollar_path), other_value(table, model)])
|
|
688
911
|
end
|
|
689
912
|
end
|
|
690
913
|
|
|
691
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
|
|
692
923
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
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
|
|
701
932
|
|
|
702
|
-
|
|
703
|
-
# boolean, which go in as JSON: taken as they are, a document would
|
|
704
|
-
# be the string that spells it, and a boolean SQLite's own 1.
|
|
705
|
-
# SQLite's json() marks the literal for JSON_SET; the MySQL family,
|
|
706
|
-
# which has no json(), reads it with JSON_EXTRACT.
|
|
707
|
-
def other_value(table, model)
|
|
708
|
-
return to_arel_operand(value, table, model) if expression?
|
|
709
|
-
unless value.is_a?(::Hash) || value.is_a?(::Array) ||
|
|
710
|
-
value == true || value == false
|
|
711
|
-
return Arel::Nodes.build_quoted(value)
|
|
933
|
+
AST.json_argument(value, model)
|
|
712
934
|
end
|
|
713
935
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
Arel::Nodes::NamedFunction.new('json', [json])
|
|
717
|
-
else
|
|
718
|
-
Arel::Nodes::NamedFunction.new(
|
|
719
|
-
'JSON_EXTRACT', [json, Arel::Nodes.build_quoted('$')])
|
|
936
|
+
def expression?
|
|
937
|
+
value.is_a?(Node) || value.is_a?(::Symbol)
|
|
720
938
|
end
|
|
721
|
-
end
|
|
722
939
|
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
def json_source
|
|
728
|
-
'bury'
|
|
729
|
-
end
|
|
940
|
+
def json_source
|
|
941
|
+
"bury"
|
|
942
|
+
end
|
|
730
943
|
end
|
|
731
944
|
|
|
732
945
|
# Keys taken out of a JSON document. PostgreSQL subtracts them, the
|
|
@@ -743,7 +956,7 @@ module ActiveRecord
|
|
|
743
956
|
@keys = check_keys(keys)
|
|
744
957
|
end
|
|
745
958
|
|
|
746
|
-
def
|
|
959
|
+
def json_value?
|
|
747
960
|
true
|
|
748
961
|
end
|
|
749
962
|
|
|
@@ -757,38 +970,37 @@ module ActiveRecord
|
|
|
757
970
|
end
|
|
758
971
|
|
|
759
972
|
Arel::Nodes::NamedFunction.new(
|
|
760
|
-
|
|
761
|
-
[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)}") }])
|
|
762
975
|
end
|
|
763
976
|
|
|
764
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
|
|
765
988
|
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
# Keys, as Hash#except takes them: an index into an array is not what
|
|
778
|
-
# the name says anywhere, and is bury's business through a path.
|
|
779
|
-
def check_keys(keys)
|
|
780
|
-
raise ArgumentError, 'except needs a key' if keys.empty?
|
|
781
|
-
keys.each do |key|
|
|
782
|
-
next if key.is_a?(::String) || key.is_a?(::Symbol)
|
|
783
|
-
raise ArgumentError,
|
|
784
|
-
"except takes keys of the document, not #{key.inspect}"
|
|
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
|
|
785
999
|
end
|
|
786
|
-
keys
|
|
787
|
-
end
|
|
788
1000
|
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
1001
|
+
def json_source
|
|
1002
|
+
"except"
|
|
1003
|
+
end
|
|
792
1004
|
end
|
|
793
1005
|
|
|
794
1006
|
# JSON containment: whether the document holds what is given.
|
|
@@ -806,7 +1018,7 @@ module ActiveRecord
|
|
|
806
1018
|
case AST.adapter_family(model)
|
|
807
1019
|
when :postgresql then Arel::Nodes::Contains.new(document, json)
|
|
808
1020
|
when :mysql
|
|
809
|
-
Arel::Nodes::NamedFunction.new(
|
|
1021
|
+
Arel::Nodes::NamedFunction.new("JSON_CONTAINS", [document, json])
|
|
810
1022
|
else
|
|
811
1023
|
# Later than the others, since the adapter is only known here.
|
|
812
1024
|
raise NotImplementedError,
|
|
@@ -815,9 +1027,11 @@ module ActiveRecord
|
|
|
815
1027
|
end
|
|
816
1028
|
end
|
|
817
1029
|
|
|
818
|
-
# Whether a key is in the document. PostgreSQL
|
|
819
|
-
#
|
|
820
|
-
#
|
|
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.
|
|
821
1035
|
class JsonHasKey < Predicate
|
|
822
1036
|
attr_reader :operand, :key
|
|
823
1037
|
|
|
@@ -832,19 +1046,150 @@ module ActiveRecord
|
|
|
832
1046
|
path = Arel::Nodes.build_quoted("$.#{key}")
|
|
833
1047
|
case AST.adapter_family(model)
|
|
834
1048
|
when :postgresql
|
|
835
|
-
Arel::Nodes::
|
|
1049
|
+
Arel::Nodes::InfixOperation.new(:"?", document, name)
|
|
836
1050
|
when :mysql
|
|
837
1051
|
Arel::Nodes::NamedFunction.new(
|
|
838
|
-
|
|
1052
|
+
"JSON_CONTAINS_PATH", [document, Arel::Nodes.build_quoted("one"), path])
|
|
839
1053
|
else
|
|
840
|
-
Arel::Nodes::NamedFunction.new(
|
|
1054
|
+
Arel::Nodes::NamedFunction.new("json_type", [document, path]).not_eq(nil)
|
|
1055
|
+
end
|
|
1056
|
+
end
|
|
1057
|
+
end
|
|
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"
|
|
841
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))
|
|
842
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
|
|
843
1187
|
end
|
|
844
1188
|
|
|
845
1189
|
# GROUP BY GROUPING SETS / ROLLUP / CUBE: several groupings asked for at
|
|
846
1190
|
# once, the totals of each coming back beside the rows. PostgreSQL has
|
|
847
|
-
# 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.
|
|
848
1193
|
#
|
|
849
1194
|
# Each set is a list of its own, so grouping_sets takes lists and rollup
|
|
850
1195
|
# and cube take the columns themselves.
|
|
@@ -864,16 +1209,32 @@ module ActiveRecord
|
|
|
864
1209
|
end
|
|
865
1210
|
|
|
866
1211
|
def to_arel(table, model)
|
|
1212
|
+
return with_rollup(table, model) if AST.adapter_family(model) == :mysql
|
|
1213
|
+
|
|
867
1214
|
KINDS.fetch(kind).new(
|
|
868
1215
|
if kind == :grouping_sets
|
|
869
1216
|
sets.map do |set|
|
|
870
1217
|
Arel::Nodes::GroupingElement.new(
|
|
871
|
-
Array(set).map {|column| to_arel_operand(column, table, model) })
|
|
1218
|
+
Array(set).map { |column| to_arel_operand(column, table, model) })
|
|
872
1219
|
end
|
|
873
1220
|
else
|
|
874
|
-
sets.map {|column| to_arel_operand(column, table, model) }
|
|
1221
|
+
sets.map { |column| to_arel_operand(column, table, model) }
|
|
875
1222
|
end)
|
|
876
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
|
|
877
1238
|
end
|
|
878
1239
|
|
|
879
1240
|
class Column < Node
|
|
@@ -908,8 +1269,11 @@ module ActiveRecord
|
|
|
908
1269
|
end
|
|
909
1270
|
|
|
910
1271
|
def to_arel(table, model)
|
|
911
|
-
to_arel_operand(left, table, model)
|
|
912
|
-
|
|
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))
|
|
913
1277
|
end
|
|
914
1278
|
end
|
|
915
1279
|
|
|
@@ -920,23 +1284,22 @@ module ActiveRecord
|
|
|
920
1284
|
# PostgreSQL has no such operator and would say so.
|
|
921
1285
|
module BitwiseOperands
|
|
922
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
|
|
923
1293
|
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
def check_not_boolean(operand, operator, model)
|
|
934
|
-
return unless operand.is_a?(::Symbol)
|
|
935
|
-
return unless model.type_for_attribute(operand).type == :boolean
|
|
936
|
-
raise ArgumentError,
|
|
937
|
-
"#{operand.inspect} is a boolean column, which #{operator} does " \
|
|
938
|
-
"not take; #{operand.inspect}.true? is the condition"
|
|
939
|
-
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
|
|
940
1303
|
end
|
|
941
1304
|
|
|
942
1305
|
# SQL's bitwise operators. Each parenthesises itself, which is what
|
|
@@ -976,22 +1339,21 @@ module ActiveRecord
|
|
|
976
1339
|
end
|
|
977
1340
|
|
|
978
1341
|
private
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
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
|
|
993
1356
|
end
|
|
994
|
-
end
|
|
995
1357
|
end
|
|
996
1358
|
|
|
997
1359
|
# ~, which every adapter has. MySQL answers with the unsigned 64-bit
|
|
@@ -1059,10 +1421,14 @@ module ActiveRecord
|
|
|
1059
1421
|
|
|
1060
1422
|
def to_arel(table, model)
|
|
1061
1423
|
window = Arel::Nodes::Window.new
|
|
1062
|
-
partitions.each {|expr| window.partition(to_arel_operand(expr, table, model)) }
|
|
1063
|
-
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)) }
|
|
1064
1426
|
frame_arel(window) if frame
|
|
1065
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
|
+
|
|
1066
1432
|
# A window-only function refuses to build on its own; here is where
|
|
1067
1433
|
# it is asked for the call itself.
|
|
1068
1434
|
arel_function =
|
|
@@ -1072,43 +1438,42 @@ module ActiveRecord
|
|
|
1072
1438
|
end
|
|
1073
1439
|
|
|
1074
1440
|
private
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
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]
|
|
1091
1458
|
end
|
|
1092
|
-
[kind, bounds.begin, bounds.end]
|
|
1093
|
-
end
|
|
1094
1459
|
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
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
|
|
1105
1470
|
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
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
|
|
1112
1477
|
end
|
|
1113
1478
|
|
|
1114
1479
|
class Aggregate < Node
|
|
@@ -1154,15 +1519,80 @@ module ActiveRecord
|
|
|
1154
1519
|
end
|
|
1155
1520
|
|
|
1156
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
|
|
1157
1531
|
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
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"
|
|
1164
1588
|
end
|
|
1589
|
+
call.filter(condition.to_arel(table, model))
|
|
1165
1590
|
end
|
|
1591
|
+
|
|
1592
|
+
private
|
|
1593
|
+
def json_source
|
|
1594
|
+
"json_#{kind}"
|
|
1595
|
+
end
|
|
1166
1596
|
end
|
|
1167
1597
|
|
|
1168
1598
|
# A column alias, quoted by the adapter, so that the name asked for is
|
|
@@ -1191,12 +1621,11 @@ module ActiveRecord
|
|
|
1191
1621
|
end
|
|
1192
1622
|
|
|
1193
1623
|
private
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
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
|
|
1200
1629
|
end
|
|
1201
1630
|
|
|
1202
1631
|
class Ordering < Node
|
|
@@ -1237,11 +1666,54 @@ module ActiveRecord
|
|
|
1237
1666
|
end
|
|
1238
1667
|
|
|
1239
1668
|
def to_arel(table, model)
|
|
1240
|
-
arel_args = args.map {|arg| to_arel_argument(arg, table, model) }
|
|
1669
|
+
arel_args = args.map { |arg| to_arel_argument(arg, table, model) }
|
|
1241
1670
|
Arel::Nodes::NamedFunction.new(name, arel_args)
|
|
1242
1671
|
end
|
|
1243
1672
|
end
|
|
1244
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
|
+
|
|
1245
1717
|
# ROW_NUMBER and its kind: functions that say nothing without a window.
|
|
1246
1718
|
# On its own this refuses rather than reaching the database as an error
|
|
1247
1719
|
# there; over asks it for call_arel instead.
|
|
@@ -1344,24 +1816,24 @@ module ActiveRecord
|
|
|
1344
1816
|
arel_value =
|
|
1345
1817
|
case value
|
|
1346
1818
|
when Node then value.to_arel(table, model)
|
|
1819
|
+
when ::Symbol then column_operand(value, table, model)
|
|
1347
1820
|
when ActiveRecord::Relation then scalar_subquery(value)
|
|
1348
|
-
else value
|
|
1821
|
+
else quote_number(value)
|
|
1349
1822
|
end
|
|
1350
1823
|
arel_column.public_send(OPERATOR_MAP.fetch(operator), arel_value)
|
|
1351
1824
|
end
|
|
1352
1825
|
|
|
1353
1826
|
private
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
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
|
|
1361
1836
|
end
|
|
1362
|
-
relation = relation.send(:apply_join_dependency) if relation.eager_loading?
|
|
1363
|
-
relation.arel
|
|
1364
|
-
end
|
|
1365
1837
|
end
|
|
1366
1838
|
|
|
1367
1839
|
# IS TRUE, IS FALSE and their negations, which every adapter spells the
|
|
@@ -1377,7 +1849,7 @@ module ActiveRecord
|
|
|
1377
1849
|
|
|
1378
1850
|
def to_arel(table, model)
|
|
1379
1851
|
literal = value ? Arel::Nodes::True.new : Arel::Nodes::False.new
|
|
1380
|
-
Arel::Nodes::InfixOperation.new(negated ?
|
|
1852
|
+
Arel::Nodes::InfixOperation.new(negated ? "IS NOT" : "IS",
|
|
1381
1853
|
to_arel_operand(operand, table, model), literal)
|
|
1382
1854
|
end
|
|
1383
1855
|
end
|
|
@@ -1388,19 +1860,18 @@ module ActiveRecord
|
|
|
1388
1860
|
# selects the model's primary key.
|
|
1389
1861
|
module SetSubquery
|
|
1390
1862
|
private
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
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])
|
|
1399
1872
|
end
|
|
1400
|
-
relation
|
|
1873
|
+
relation.arel
|
|
1401
1874
|
end
|
|
1402
|
-
relation.arel
|
|
1403
|
-
end
|
|
1404
1875
|
end
|
|
1405
1876
|
|
|
1406
1877
|
# IN for a list of values, BETWEEN for a range, IN (SELECT ...) for a
|
|
@@ -1408,6 +1879,12 @@ module ActiveRecord
|
|
|
1408
1879
|
class In < Predicate
|
|
1409
1880
|
include SetSubquery
|
|
1410
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
|
+
|
|
1411
1888
|
attr_reader :operand, :values, :negated
|
|
1412
1889
|
|
|
1413
1890
|
def initialize(operand, values, negated: false)
|
|
@@ -1418,13 +1895,67 @@ module ActiveRecord
|
|
|
1418
1895
|
|
|
1419
1896
|
def to_arel(table, model)
|
|
1420
1897
|
arel_operand = to_arel_operand(operand, table, model)
|
|
1421
|
-
|
|
1422
|
-
|
|
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"))
|
|
1423
1910
|
else
|
|
1424
|
-
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
|
|
1425
1916
|
arel_operand.public_send(negated ? :not_in : :in, arg)
|
|
1426
1917
|
end
|
|
1427
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
|
|
1428
1959
|
end
|
|
1429
1960
|
|
|
1430
1961
|
# ANY and ALL, which stand on the right of a comparison and say how many
|
|
@@ -1472,7 +2003,7 @@ module ActiveRecord
|
|
|
1472
2003
|
end
|
|
1473
2004
|
|
|
1474
2005
|
class Like < Predicate
|
|
1475
|
-
ESCAPE = "\\"
|
|
2006
|
+
ESCAPE = "\\"
|
|
1476
2007
|
|
|
1477
2008
|
# Escapes % and _ so that they match literally. The pattern built from
|
|
1478
2009
|
# the result must be used with ESCAPE, since SQLite has no default
|
|
@@ -1484,8 +2015,8 @@ module ActiveRecord
|
|
|
1484
2015
|
# ORs one LIKE per pattern, for the shortcuts that accept several
|
|
1485
2016
|
# literals the way String#start_with? does.
|
|
1486
2017
|
def self.any(operand, patterns)
|
|
1487
|
-
patterns.map {|pattern| new(operand, pattern, ESCAPE) }.
|
|
1488
|
-
inject {|left, right| Or.new(left, right) }
|
|
2018
|
+
patterns.map { |pattern| new(operand, pattern, ESCAPE) }.
|
|
2019
|
+
inject { |left, right| Or.new(left, right) }
|
|
1489
2020
|
end
|
|
1490
2021
|
|
|
1491
2022
|
attr_reader :operand, :pattern, :escape, :case_sensitive, :negated
|
|
@@ -1569,21 +2100,20 @@ module ActiveRecord
|
|
|
1569
2100
|
end
|
|
1570
2101
|
|
|
1571
2102
|
private
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
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
|
|
1583
2114
|
end
|
|
2115
|
+
"{#{encoded.join(',')}}"
|
|
1584
2116
|
end
|
|
1585
|
-
"{#{encoded.join(',')}}"
|
|
1586
|
-
end
|
|
1587
2117
|
end
|
|
1588
2118
|
|
|
1589
2119
|
# Regular expression match: REGEXP on MySQL, ~ on PostgreSQL. SQLite has
|
|
@@ -1607,19 +2137,18 @@ module ActiveRecord
|
|
|
1607
2137
|
end
|
|
1608
2138
|
|
|
1609
2139
|
private
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
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
|
|
1620
2151
|
end
|
|
1621
|
-
regexp.source
|
|
1622
|
-
end
|
|
1623
2152
|
end
|
|
1624
2153
|
|
|
1625
2154
|
class And < Predicate
|