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.
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # JSON.generate, for the document a containment test is given.
2
- require 'json'
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
- 'sqlite3' => :sqlite,
23
- 'postgresql' => :postgresql,
24
- 'postgis' => :postgresql,
25
- 'pglite' => :postgresql,
26
- 'mysql2' => :mysql,
27
- 'trilogy' => :mysql,
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..max)
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..max, negated: true)
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, as_json: true)
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
- # Resolves an operand denoting a column or an expression.
334
- def to_arel_operand(operand, table, model)
335
- case operand
336
- when Node then operand.to_arel(table, model)
337
- when :* then Arel.star
338
- when Symbol then table[operand]
339
- else operand
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
- # Resolves a function argument: a column or an expression as above,
344
- # anything else a value to be quoted.
345
- def to_arel_argument(arg, table, model)
346
- case arg
347
- when Node, Symbol then to_arel_operand(arg, table, model)
348
- else Arel::Nodes.build_quoted(arg)
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 the
370
- # top of a select list is Active Record's, and a bare string there is SQL
371
- # rather than a string. Saying `value` is how you ask for the other
372
- # meaning, and it carries the predications with it, so a literal can be
373
- # compared and combined like anything else.
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(+'$') {|so_far, step| so_far << dollar_step(step) }
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
- '.' + (name.match?(/\A[[:alpha:]_][[:alnum:]_]*\z/) ?
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('\\', '\\\\').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 is refused the other way about: the JSON for a string carries
525
- # its quotes, so `dig(:name) == 'alice'` is false on SQLite, an
526
- # error on PostgreSQL and true on MySQL. dig_text is the one that
527
- # gives the value. What bury and except give back is JSON as dig's
528
- # is, and is refused the same way.
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
- check_comparable(other)
541
- super(other)
693
+ super(comparison_value(other))
542
694
  end
543
695
  end
544
696
 
545
- def in?(values) = super(check_each(values))
546
- def not_in?(values) = super(check_each(values))
547
- def between?(min, max) = super(*check_each([min, max]))
548
- def not_between?(min, max) = super(*check_each([min, max]))
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
- # nil is left to the comparison itself, which says to use null?, and
563
- # so is anything the block built rather than wrote as a literal.
564
- def check_comparable(other)
565
- return if other.nil? || other.is_a?(Node) || other.is_a?(::Symbol) ||
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
- raise ArgumentError, as_json ?
572
- "#{json_source} gives JSON, and comparing it with #{other.inspect} " \
573
- "means something different on every adapter; dig_text gives the value" :
574
- "dig_text gives text, and comparing it with #{other.inspect} means " \
575
- "something different on every adapter; cast it to the type meant"
576
- end
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
- def check_each(values)
579
- case values
580
- when ActiveRecord::Relation then values
581
- when ::Range then [values.begin, values.end].each {|v| check_comparable(v) }
582
- else values.each {|value| check_comparable(value) }
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
- values
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 arithmetic_refusal(operator)
588
- as_json ?
589
- "#{json_source} gives JSON, and #{operator} on it means something " \
590
- "different on every adapter; cast dig_text to the type meant" :
591
- "dig_text gives text, and #{operator} on it means something " \
592
- "different on every adapter; cast it to the type meant"
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 as_json
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, :as_json
841
+ attr_reader :operand, :path
622
842
 
623
- def initialize(operand, path, as_json: false)
843
+ def initialize(operand, path, json_value: true)
624
844
  @operand = operand
625
- @path = check_steps(path, 'dig')
626
- @as_json = as_json
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
- as_json ? :"#>" : :"#>>", document, Arel::Nodes.build_quoted(steps_array))
858
+ json_value? ? :"#>" : :"#>>", document, Arel::Nodes.build_quoted(steps_array))
635
859
  when :mysql
636
860
  extracted = Arel::Nodes::NamedFunction.new(
637
- 'JSON_EXTRACT', [document, Arel::Nodes.build_quoted(dollar_path)])
638
- as_json ? extracted : Arel::Nodes::NamedFunction.new('JSON_UNQUOTE', [extracted])
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
- as_json ? :"->" : :"->>", document, Arel::Nodes.build_quoted(dollar_path))
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
- as_json ? extracted : Arel::Nodes::NamedFunction.new(
646
- 'CAST', [Arel::Nodes::As.new(extracted, Arel::Nodes::SqlLiteral.new('text'))])
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
- def json_source
653
- 'dig'
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, 'bury')
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 as_json
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
- 'jsonb_set',
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
- 'JSON_SET',
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
- # jsonb_set takes jsonb, so an expression is turned into it and a Ruby
694
- # value goes in as the JSON that says it -- '"x"' rather than 'x',
695
- # which is not a document at all.
696
- def postgresql_value(table, model)
697
- return Arel::Nodes::NamedFunction.new(
698
- 'to_jsonb', [to_arel_operand(value, table, model)]) if expression?
699
- Arel::Nodes.build_quoted(JSON.generate(value))
700
- end
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
- # The others take the value as it is, except a whole document or a
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
- json = Arel::Nodes.build_quoted(JSON.generate(value))
715
- if AST.adapter_family(model) == :sqlite
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
- def expression?
724
- value.is_a?(Node) || value.is_a?(::Symbol)
725
- end
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 as_json
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
- 'JSON_REMOVE',
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
- # jsonb has three subtractions -- a key, an array of keys, an element
767
- # by index -- and an array literal written without a type is read as
768
- # the first of them: `meta - '{draft}'` takes out the key spelled
769
- # {draft}, which is nothing, and says nothing about it.
770
- def key_array
771
- Arel::Nodes::NamedFunction.new(
772
- 'CAST',
773
- [Arel::Nodes::As.new(Arel::Nodes.build_quoted(steps_array(keys)),
774
- Arel::Nodes::SqlLiteral.new('text[]'))])
775
- end
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
- def json_source
790
- 'except'
791
- end
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('JSON_CONTAINS', [document, json])
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 has an operator for it,
819
- # ?, which is also what a bind parameter looks like to several drivers;
820
- # the function it is shorthand for says the same thing and survives.
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::NamedFunction.new('jsonb_exists', [document, name])
1049
+ Arel::Nodes::InfixOperation.new(:"?", document, name)
836
1050
  when :mysql
837
1051
  Arel::Nodes::NamedFunction.new(
838
- 'JSON_CONTAINS_PATH', [document, Arel::Nodes.build_quoted('one'), path])
1052
+ "JSON_CONTAINS_PATH", [document, Arel::Nodes.build_quoted("one"), path])
839
1053
  else
840
- Arel::Nodes::NamedFunction.new('json_type', [document, path]).not_eq(nil)
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 others before it gets this far.
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
- public_send(operator, to_arel_operand(right, table, model))
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
- def check_operand(operand, operator)
925
- return operand unless operand.is_a?(Predicate)
926
- raise ArgumentError,
927
- "a condition cannot be an operand of #{operator}; " \
928
- "& and | between conditions are AND and OR"
929
- end
930
-
931
- # Only the unqualified column can be checked, since that is the one
932
- # the model is known to have.
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
- # Arel has a node for XOR, but it writes ^ on every adapter, and ^ is
981
- # exponentiation to PostgreSQL -- a wrong answer rather than an error.
982
- # PostgreSQL's own spelling, #, is where a comment starts on MySQL, so
983
- # it cannot be the portable one either. SQLite has no XOR at all;
984
- # (a | b) - (a & b) is it, at the cost of naming each operand twice.
985
- def xor(left, right, model)
986
- case AST.adapter_family(model)
987
- when :postgresql then Arel::Nodes::InfixOperation.new('#', left, right)
988
- when :mysql then Arel::Nodes::BitwiseXor.new(left, right)
989
- else
990
- Arel::Nodes::Subtraction.new(
991
- Arel::Nodes::Grouping.new(Arel::Nodes::BitwiseOr.new(left, right)),
992
- Arel::Nodes::Grouping.new(Arel::Nodes::BitwiseAnd.new(left, right)))
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
- # The frame is a range of rows counted from the current one: negative
1077
- # before it, positive after, 0 the row itself, and an open end for
1078
- # unbounded. `rows(..0)` is what a running total wants.
1079
- def framing(kind, bounds)
1080
- raise ArgumentError, "a window has one frame" if frame
1081
- unless bounds.is_a?(::Range)
1082
- raise ArgumentError, "#{kind} takes a range of rows, as in rows(..0)"
1083
- end
1084
- if bounds.exclude_end?
1085
- raise ArgumentError, "a frame ends on a row rather than before one; use .."
1086
- end
1087
- [bounds.begin, bounds.end].each do |bound|
1088
- next if bound.nil? || bound.is_a?(::Integer)
1089
- raise ArgumentError,
1090
- "a frame bound is a number of rows, or nothing for unbounded"
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
- # Arel wants the keyword itself on the left of the BETWEEN, which is
1096
- # what window.rows with no argument hands back.
1097
- def frame_arel(window)
1098
- kind, from, to = frame
1099
- window.frame(
1100
- Arel::Nodes::Between.new(
1101
- window.public_send(kind),
1102
- Arel::Nodes::And.new([bound(from, Arel::Nodes::Preceding.new),
1103
- bound(to, Arel::Nodes::Following.new)])))
1104
- end
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
- def bound(rows, unbounded)
1107
- return unbounded if rows.nil?
1108
- return Arel::Nodes::CurrentRow.new if rows.zero?
1109
- rows.negative? ? Arel::Nodes::Preceding.new(-rows)
1110
- : Arel::Nodes::Following.new(rows)
1111
- end
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
- def aggregate(over, table, model)
1159
- arel_operand = to_arel_operand(over, table, model)
1160
- if function == :count
1161
- arel_operand.count(distinct)
1162
- else
1163
- arel_operand.public_send(function)
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
- def alias_sql(model)
1196
- name = alias_name.to_s
1197
- return name unless quote
1198
- model.with_connection {|connection| connection.quote_column_name(name) }
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
- # A relation compared against a column has to yield a single value, so
1356
- # unlike In there is no sensible default select list to fall back on.
1357
- def scalar_subquery(relation)
1358
- if relation.select_values.empty?
1359
- raise ArgumentError,
1360
- "#{operator} needs a subquery selecting one value; add a select"
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 ? 'IS NOT' : 'IS',
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
- def set_subquery(relation, spelling)
1393
- relation = relation.send(:apply_join_dependency) if relation.eager_loading?
1394
- if relation.select_values.empty?
1395
- model = relation.model
1396
- if model.composite_primary_key?
1397
- raise ArgumentError,
1398
- "Cannot map composite primary key #{model.primary_key} to #{spelling}"
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 = relation.select(relation.table[model.primary_key])
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
- if values.is_a?(Range)
1422
- arel_operand.public_send(negated ? :not_between : :between, values)
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.is_a?(ActiveRecord::Relation) ? set_subquery(values, 'IN') : 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 = "\\".freeze
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
- # PostgreSQL array input syntax: elements joined by commas inside
1574
- # braces, and an element is double-quoted whenever it is empty, spells
1575
- # NULL, or contains a character the parser treats specially.
1576
- def array_literal
1577
- encoded = elements.map do |value|
1578
- s = value.to_s
1579
- if s.empty? || s.casecmp?("null") || s.match?(/[\s{},"\\]/)
1580
- "\"#{s.gsub(/["\\]/) {|c| "\\#{c}" }}\""
1581
- else
1582
- s
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
- # A Regexp literal reads naturally with =~, but only its source crosses
1612
- # over; the database has its own dialect and no notion of Ruby's flags.
1613
- # Dropping a flag would silently change what the query matches, so
1614
- # anything beyond a plain literal is refused rather than ignored.
1615
- def regexp_source(regexp)
1616
- unless regexp.options.zero?
1617
- raise ArgumentError,
1618
- "#{regexp.inspect} has options that SQL cannot express; " \
1619
- "pass the pattern as a string instead"
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