activerecord-refined 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,52 +673,128 @@ module ActiveRecord
521
673
  # `dig_text(:flag) == true` is true, an error, and false. cast is what
522
674
  # says which type was meant, and then all three agree.
523
675
  #
524
- # dig 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.
676
+ # dig, bury and except give JSON, and a JSON comparison belongs to the
677
+ # JSON types -- jsonb and MySQL's -- where numbers compare as numbers
678
+ # and documents structurally, key order and spelling aside. The Ruby
679
+ # value goes in as a JSON literal, and the adapters without such a
680
+ # type refuse it from JsonLiteral when the SQL is written, which is
681
+ # when the adapter is known.
528
682
  #
529
683
  # A string against dig_text, and anything the block itself built -- a
530
684
  # column, a function, another dug value -- go through untouched.
685
+ #
686
+ # Arithmetic and the bit operators are refused outright on both sides:
687
+ # `dig_text(:n) + 1` is 6 on SQLite, an error on PostgreSQL and 6.0 on
688
+ # MariaDB, and an expression on the right does not change what the
689
+ # dug side is.
531
690
  module JsonComparable
532
691
  %i[== != < <= > >=].each do |operator|
533
692
  define_method(operator) do |other|
534
- check_comparable(other)
535
- super(other)
693
+ super(comparison_value(other))
536
694
  end
537
695
  end
538
696
 
539
- def in?(values) = super(check_each(values))
540
- def not_in?(values) = super(check_each(values))
541
- def between?(min, max) = super(*check_each([min, max]))
542
- 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))
701
+
702
+ %i[+ - * / & | ^ << >>].each do |operator|
703
+ define_method(operator) do |_other|
704
+ raise ArgumentError, arithmetic_refusal(operator)
705
+ end
706
+ end
707
+
708
+ def ~
709
+ raise ArgumentError, arithmetic_refusal(:~)
710
+ end
543
711
 
544
712
  private
713
+ # nil is left to the comparison itself, which says to use null?, and
714
+ # so is anything the block built rather than wrote as a literal.
715
+ def comparison_value(other)
716
+ return other if other.nil? || other.is_a?(Node) || other.is_a?(::Symbol) ||
717
+ other.is_a?(Arel::Nodes::Node) ||
718
+ other.is_a?(Arel::Attributes::Attribute) ||
719
+ other.is_a?(ActiveRecord::Relation)
720
+ return json_literal(other) if json_value?
721
+ return other if other.is_a?(::String)
722
+
723
+ raise ArgumentError,
724
+ "dig_text gives text, and comparing it with #{other.inspect} means " \
725
+ "something different on every adapter; cast it to the type meant"
726
+ end
727
+
728
+ def json_literal(other)
729
+ case other
730
+ when ::String, ::Integer, ::Float, ::BigDecimal, true, false, ::Hash, ::Array
731
+ JsonLiteral.new(other)
732
+ when ::Rational
733
+ raise ArgumentError,
734
+ "a Rational has no exact SQL spelling; to_d says the decimal meant"
735
+ else
736
+ raise ArgumentError,
737
+ "#{json_source} gives JSON, and #{other.inspect} has no JSON " \
738
+ "spelling; dig_text gives the value"
739
+ end
740
+ end
545
741
 
546
- # nil is left to the comparison itself, which says to use null?, and
547
- # so is anything the block built rather than wrote as a literal.
548
- def check_comparable(other)
549
- return if other.nil? || other.is_a?(Node) || other.is_a?(::Symbol) ||
550
- other.is_a?(Arel::Nodes::Node) ||
551
- other.is_a?(Arel::Attributes::Attribute) ||
552
- other.is_a?(ActiveRecord::Relation)
553
- return if other.is_a?(::String) && !as_json
742
+ def comparison_set(values)
743
+ case values
744
+ when ActiveRecord::Relation then values
745
+ when ::Range
746
+ In::QuotedRange.new(comparison_value(values.begin),
747
+ comparison_value(values.end), values.exclude_end?)
748
+ else values.map { |value| comparison_value(value) }
749
+ end
750
+ end
751
+
752
+ def arithmetic_refusal(operator)
753
+ json_value? ?
754
+ "#{json_source} gives JSON, and #{operator} on it means something " \
755
+ "different on every adapter; cast dig_text to the type meant" :
756
+ "dig_text gives text, and #{operator} on it means something " \
757
+ "different on every adapter; cast it to the type meant"
758
+ end
759
+ end
554
760
 
555
- raise ArgumentError, as_json ?
556
- "dig gives JSON, and comparing it with #{other.inspect} " \
557
- "means something different on every adapter; dig_text gives the value" :
558
- "dig_text gives text, and comparing it with #{other.inspect} means " \
559
- "something different on every adapter; cast it to the type meant"
761
+ # A Ruby value on the JSON side of a comparison, which jsonb and
762
+ # MySQL's JSON type answer alike: numbers compare as numbers and
763
+ # documents structurally. SQLite and MariaDB have only the text of
764
+ # each -- spelling and key order deciding what equality means -- and
765
+ # refuse here. PostgreSQL needs no cast, an untyped literal beside a
766
+ # jsonb operand coercing to jsonb; MySQL is told CAST(... AS JSON),
767
+ # since a bare string beside JSON would be a JSON string, and every
768
+ # string outranks every number in its ordering.
769
+ class JsonLiteral < Node
770
+ attr_reader :value
771
+
772
+ def initialize(value)
773
+ @value = value
560
774
  end
561
775
 
562
- def check_each(values)
563
- case values
564
- when ActiveRecord::Relation then values
565
- when ::Range then [values.begin, values.end].each {|v| check_comparable(v) }
566
- else values.each {|value| check_comparable(value) }
776
+ def to_arel(_table, model)
777
+ json = Arel::Nodes.build_quoted(JSON.generate(value))
778
+ case AST.adapter_family(model)
779
+ when :postgresql then json
780
+ when :mysql then mysql_literal(json, model)
781
+ else refuse(model.connection_db_config.adapter)
567
782
  end
568
- values
569
783
  end
784
+
785
+ private
786
+ # MariaDB answers to the same adapter and has no JSON type at all.
787
+ def mysql_literal(json, model)
788
+ refuse("MariaDB") if model.with_connection { |c| c.mariadb? }
789
+ Arel::Nodes::NamedFunction.new(
790
+ "CAST", [Arel::Nodes::As.new(json, Arel::Nodes::SqlLiteral.new("JSON"))])
791
+ end
792
+
793
+ def refuse(database)
794
+ raise NotImplementedError,
795
+ "a JSON comparison has no equivalent on #{database}; " \
796
+ "dig_text gives the value"
797
+ end
570
798
  end
571
799
 
572
800
  # The JSON operations read a document, and what dig gives is one:
@@ -576,9 +804,9 @@ module ActiveRecord
576
804
  # again is where they part company: SQLite parses it back and MySQL
577
805
  # takes it as written, where PostgreSQL has no such function for text.
578
806
  module JsonDocument
579
- %i[dig dig_text key? contains? bury except].each do |name|
807
+ %i[dig dig_text key? keys contains? bury except].each do |name|
580
808
  define_method(name) do |*args|
581
- unless as_json
809
+ unless json_value?
582
810
  raise ArgumentError,
583
811
  "dig_text gives text, and #{name} reads JSON; dig keeps it"
584
812
  end
@@ -587,6 +815,22 @@ module ActiveRecord
587
815
  end
588
816
  end
589
817
 
818
+ # JSON the query computes rather than reads out of a document: always
819
+ # a JSON value, with no dig_text counterpart for JsonComparable's
820
+ # advice to name. Included after JsonComparable, whose own
821
+ # arithmetic_refusal it overrides.
822
+ module ComputedJson
823
+ def json_value?
824
+ true
825
+ end
826
+
827
+ private
828
+ def arithmetic_refusal(operator)
829
+ "#{json_source} gives JSON, and #{operator} on it means " \
830
+ "something different on every adapter"
831
+ end
832
+ end
833
+
590
834
  class JsonPath < Node
591
835
  include Predications
592
836
  include Arithmetics
@@ -594,12 +838,16 @@ module ActiveRecord
594
838
  include JsonComparable
595
839
  include JsonDocument
596
840
 
597
- attr_reader :operand, :path, :as_json
841
+ attr_reader :operand, :path
598
842
 
599
- def initialize(operand, path, as_json: false)
843
+ def initialize(operand, path, json_value: true)
600
844
  @operand = operand
601
- @path = check_steps(path, 'dig')
602
- @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
603
851
  end
604
852
 
605
853
  def to_arel(table, model)
@@ -607,22 +855,26 @@ module ActiveRecord
607
855
  case AST.adapter_family(model)
608
856
  when :postgresql
609
857
  Arel::Nodes::InfixOperation.new(
610
- as_json ? :"#>" : :"#>>", document, Arel::Nodes.build_quoted(steps_array))
858
+ json_value? ? :"#>" : :"#>>", document, Arel::Nodes.build_quoted(steps_array))
611
859
  when :mysql
612
860
  extracted = Arel::Nodes::NamedFunction.new(
613
- 'JSON_EXTRACT', [document, Arel::Nodes.build_quoted(dollar_path)])
614
- 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])
615
863
  else
616
864
  extracted = Arel::Nodes::InfixOperation.new(
617
- as_json ? :"->" : :"->>", document, Arel::Nodes.build_quoted(dollar_path))
865
+ json_value? ? :"->" : :"->>", document, Arel::Nodes.build_quoted(dollar_path))
618
866
  # SQLite's ->> gives back the value with its type, where the other
619
867
  # two give text. Cast so that `dig_text(:n) == '5'` means the
620
868
  # same thing everywhere, and a number wants a cast everywhere too.
621
- as_json ? extracted : Arel::Nodes::NamedFunction.new(
622
- '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"))])
623
871
  end
624
872
  end
625
873
 
874
+ private
875
+ def json_source
876
+ "dig"
877
+ end
626
878
  end
627
879
 
628
880
  # Setting a value inside a JSON document, which is what bury does to what
@@ -631,54 +883,63 @@ module ActiveRecord
631
883
  class JsonSet < Node
632
884
  include Predications
633
885
  include JsonSteps
886
+ include JsonComparable
634
887
 
635
888
  attr_reader :operand, :path, :value
636
889
 
637
890
  def initialize(operand, path, value)
638
891
  @operand = operand
639
- @path = check_steps(path, 'bury')
892
+ @path = check_steps(path, "bury")
640
893
  @value = value
641
894
  end
642
895
 
896
+ # Always JSON, which is what the comparison guard asks.
897
+ def json_value?
898
+ true
899
+ end
900
+
643
901
  def to_arel(table, model)
644
902
  document = to_arel_operand(operand, table, model)
645
903
  if AST.adapter_family(model) == :postgresql
646
904
  Arel::Nodes::NamedFunction.new(
647
- 'jsonb_set',
905
+ "jsonb_set",
648
906
  [document, Arel::Nodes.build_quoted(steps_array), postgresql_value(table, model)])
649
907
  else
650
908
  Arel::Nodes::NamedFunction.new(
651
- 'JSON_SET',
909
+ "JSON_SET",
652
910
  [document, Arel::Nodes.build_quoted(dollar_path), other_value(table, model)])
653
911
  end
654
912
  end
655
913
 
656
914
  private
915
+ # jsonb_set takes jsonb, so an expression is turned into it and a Ruby
916
+ # value goes in as the JSON that says it -- '"x"' rather than 'x',
917
+ # which is not a document at all.
918
+ def postgresql_value(table, model)
919
+ return Arel::Nodes::NamedFunction.new(
920
+ "to_jsonb", [to_arel_operand(value, table, model)]) if expression?
921
+ Arel::Nodes.build_quoted(JSON.generate(value))
922
+ end
657
923
 
658
- # jsonb_set takes jsonb, so an expression is turned into it and a Ruby
659
- # value goes in as the JSON that says it -- '"x"' rather than 'x',
660
- # which is not a document at all.
661
- def postgresql_value(table, model)
662
- return Arel::Nodes::NamedFunction.new(
663
- 'to_jsonb', [to_arel_operand(value, table, model)]) if expression?
664
- Arel::Nodes.build_quoted(JSON.generate(value))
665
- 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
666
932
 
667
- # The others take the value as it is, except a whole document, which
668
- # they read out of a literal rather than take as a string. MySQL casts
669
- # to JSON where MariaDB, which answers to the same adapter, does not.
670
- def other_value(table, model)
671
- return to_arel_operand(value, table, model) if expression?
672
- return Arel::Nodes.build_quoted(value) unless value.is_a?(::Hash) || value.is_a?(::Array)
933
+ AST.json_argument(value, model)
934
+ end
673
935
 
674
- Arel::Nodes::NamedFunction.new(
675
- 'JSON_EXTRACT',
676
- [Arel::Nodes.build_quoted(JSON.generate(value)), Arel::Nodes.build_quoted('$')])
677
- end
936
+ def expression?
937
+ value.is_a?(Node) || value.is_a?(::Symbol)
938
+ end
678
939
 
679
- def expression?
680
- value.is_a?(Node) || value.is_a?(::Symbol)
681
- end
940
+ def json_source
941
+ "bury"
942
+ end
682
943
  end
683
944
 
684
945
  # Keys taken out of a JSON document. PostgreSQL subtracts them, the
@@ -686,6 +947,7 @@ module ActiveRecord
686
947
  class JsonExcept < Node
687
948
  include Predications
688
949
  include JsonSteps
950
+ include JsonComparable
689
951
 
690
952
  attr_reader :operand, :keys
691
953
 
@@ -694,40 +956,51 @@ module ActiveRecord
694
956
  @keys = check_keys(keys)
695
957
  end
696
958
 
959
+ def json_value?
960
+ true
961
+ end
962
+
697
963
  def to_arel(table, model)
698
964
  document = to_arel_operand(operand, table, model)
699
- return Arel::Nodes::InfixOperation.new(:-, document, key_array) if
700
- AST.adapter_family(model) == :postgresql
965
+ if AST.adapter_family(model) == :postgresql
966
+ # Grouped because - binds tighter than #>: dug out of a document,
967
+ # the subtraction would otherwise take the path literal first.
968
+ return Arel::Nodes::InfixOperation.new(
969
+ :-, Arel::Nodes::Grouping.new(document), key_array)
970
+ end
701
971
 
702
972
  Arel::Nodes::NamedFunction.new(
703
- 'JSON_REMOVE',
704
- [document, *keys.map {|key| Arel::Nodes.build_quoted("$#{dollar_step(key)}") }])
973
+ "JSON_REMOVE",
974
+ [document, *keys.map { |key| Arel::Nodes.build_quoted("$#{dollar_step(key)}") }])
705
975
  end
706
976
 
707
977
  private
978
+ # jsonb has three subtractions -- a key, an array of keys, an element
979
+ # by index -- and an array literal written without a type is read as
980
+ # the first of them: `meta - '{draft}'` takes out the key spelled
981
+ # {draft}, which is nothing, and says nothing about it.
982
+ def key_array
983
+ Arel::Nodes::NamedFunction.new(
984
+ "CAST",
985
+ [Arel::Nodes::As.new(Arel::Nodes.build_quoted(steps_array(keys)),
986
+ Arel::Nodes::SqlLiteral.new("text[]"))])
987
+ end
708
988
 
709
- # jsonb has three subtractions -- a key, an array of keys, an element
710
- # by index -- and an array literal written without a type is read as
711
- # the first of them: `meta - '{draft}'` takes out the key spelled
712
- # {draft}, which is nothing, and says nothing about it.
713
- def key_array
714
- Arel::Nodes::NamedFunction.new(
715
- 'CAST',
716
- [Arel::Nodes::As.new(Arel::Nodes.build_quoted(steps_array(keys)),
717
- Arel::Nodes::SqlLiteral.new('text[]'))])
718
- end
989
+ # Keys, as Hash#except takes them: an index into an array is not what
990
+ # the name says anywhere, and is bury's business through a path.
991
+ def check_keys(keys)
992
+ raise ArgumentError, "except needs a key" if keys.empty?
993
+ keys.each do |key|
994
+ next if key.is_a?(::String) || key.is_a?(::Symbol)
995
+ raise ArgumentError,
996
+ "except takes keys of the document, not #{key.inspect}"
997
+ end
998
+ keys
999
+ end
719
1000
 
720
- # Keys, as Hash#except takes them: an index into an array is not what
721
- # the name says anywhere, and is bury's business through a path.
722
- def check_keys(keys)
723
- raise ArgumentError, 'except needs a key' if keys.empty?
724
- keys.each do |key|
725
- next if key.is_a?(::String) || key.is_a?(::Symbol)
726
- raise ArgumentError,
727
- "except takes keys of the document, not #{key.inspect}"
1001
+ def json_source
1002
+ "except"
728
1003
  end
729
- keys
730
- end
731
1004
  end
732
1005
 
733
1006
  # JSON containment: whether the document holds what is given.
@@ -745,7 +1018,7 @@ module ActiveRecord
745
1018
  case AST.adapter_family(model)
746
1019
  when :postgresql then Arel::Nodes::Contains.new(document, json)
747
1020
  when :mysql
748
- Arel::Nodes::NamedFunction.new('JSON_CONTAINS', [document, json])
1021
+ Arel::Nodes::NamedFunction.new("JSON_CONTAINS", [document, json])
749
1022
  else
750
1023
  # Later than the others, since the adapter is only known here.
751
1024
  raise NotImplementedError,
@@ -754,9 +1027,11 @@ module ActiveRecord
754
1027
  end
755
1028
  end
756
1029
 
757
- # Whether a key is in the document. PostgreSQL has an operator for it,
758
- # ?, which is also what a bind parameter looks like to several drivers;
759
- # 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.
760
1035
  class JsonHasKey < Predicate
761
1036
  attr_reader :operand, :key
762
1037
 
@@ -771,19 +1046,150 @@ module ActiveRecord
771
1046
  path = Arel::Nodes.build_quoted("$.#{key}")
772
1047
  case AST.adapter_family(model)
773
1048
  when :postgresql
774
- Arel::Nodes::NamedFunction.new('jsonb_exists', [document, name])
1049
+ Arel::Nodes::InfixOperation.new(:"?", document, name)
775
1050
  when :mysql
776
1051
  Arel::Nodes::NamedFunction.new(
777
- 'JSON_CONTAINS_PATH', [document, Arel::Nodes.build_quoted('one'), path])
1052
+ "JSON_CONTAINS_PATH", [document, Arel::Nodes.build_quoted("one"), path])
778
1053
  else
779
- Arel::Nodes::NamedFunction.new('json_type', [document, path]).not_eq(nil)
1054
+ Arel::Nodes::NamedFunction.new("json_type", [document, path]).not_eq(nil)
780
1055
  end
781
1056
  end
782
1057
  end
783
1058
 
1059
+ # The keys of a JSON document, as Hash#keys gives them: a JSON array.
1060
+ # Only the MySQL family has a function for it; the other two reach the
1061
+ # same array through a subquery over their key-listing functions. The
1062
+ # type guard is what makes all four answer alike: the keys of what is
1063
+ # not an object are NULL rather than SQLite's array indices or
1064
+ # PostgreSQL's error, and the keys of {} are [] rather than
1065
+ # PostgreSQL's NULL, jsonb_agg over no rows.
1066
+ class JsonKeys < Node
1067
+ include Predications
1068
+ include JsonComparable
1069
+ include ComputedJson
1070
+
1071
+ attr_reader :operand
1072
+
1073
+ def initialize(operand)
1074
+ @operand = operand
1075
+ end
1076
+
1077
+ def to_arel(table, model)
1078
+ document = to_arel_operand(operand, table, model)
1079
+ case AST.adapter_family(model)
1080
+ when :sqlite
1081
+ sql = compile(document, model)
1082
+ Arel.sql("CASE WHEN json_type(#{sql}) = 'object' " \
1083
+ "THEN (SELECT json_group_array(key) FROM json_each(#{sql})) END")
1084
+ when :postgresql
1085
+ sql = compile(document, model)
1086
+ Arel.sql("CASE WHEN jsonb_typeof(#{sql}) = 'object' " \
1087
+ "THEN COALESCE((SELECT jsonb_agg(k) FROM jsonb_object_keys(#{sql}) k), " \
1088
+ "CAST('[]' AS jsonb)) END")
1089
+ else
1090
+ Arel::Nodes::NamedFunction.new("JSON_KEYS", [document])
1091
+ end
1092
+ end
1093
+
1094
+ private
1095
+ # The document appears more than once, the way SQLite's XOR names
1096
+ # its operands twice; the connection's own visitor compiles it, so
1097
+ # its quoting is the adapter's.
1098
+ def compile(document, model)
1099
+ model.with_connection { |connection| connection.visitor.compile(document) }
1100
+ end
1101
+
1102
+ def json_source
1103
+ "keys"
1104
+ end
1105
+ end
1106
+
1107
+ # A JSON document built in the row: json_array from the values given,
1108
+ # json_object from a Ruby hash. SQLite and the MySQL family both say
1109
+ # the standard names; PostgreSQL is asked to build jsonb, whose
1110
+ # documents the other JSON operations here read.
1111
+ class JsonBuild < Node
1112
+ include Predications
1113
+ include JsonComparable
1114
+ include ComputedJson
1115
+
1116
+ NAMES = {
1117
+ array: { postgresql: "jsonb_build_array" },
1118
+ object: { postgresql: "jsonb_build_object" },
1119
+ }.freeze
1120
+
1121
+ attr_reader :kind, :values
1122
+
1123
+ def initialize(kind, values)
1124
+ @kind = kind
1125
+ @values = kind == :object ? check_pairs(values) : values
1126
+ end
1127
+
1128
+ def to_arel(table, model)
1129
+ Arel::Nodes::NamedFunction.new(
1130
+ NAMES.fetch(kind).fetch(AST.adapter_family(model)) { "JSON_#{kind.to_s.upcase}" },
1131
+ arguments(table, model))
1132
+ end
1133
+
1134
+ private
1135
+ def arguments(table, model)
1136
+ if kind == :array
1137
+ values.map { |value| build_argument(value, table, model) }
1138
+ else
1139
+ values.flat_map do |key, value|
1140
+ [Arel::Nodes.build_quoted(key.to_s),
1141
+ build_argument(value, table, model)]
1142
+ end
1143
+ end
1144
+ end
1145
+
1146
+ # An expression is itself and a document or a boolean goes in as
1147
+ # JSON, as bury takes them. PostgreSQL builds from typed
1148
+ # arguments, so its JSON literal is cast -- left untyped it would
1149
+ # be text, and land as a string.
1150
+ def build_argument(value, table, model)
1151
+ case value
1152
+ when Node, ::Symbol then to_arel_operand(value, table, model)
1153
+ when ::Hash, ::Array, true, false
1154
+ if AST.adapter_family(model) == :postgresql
1155
+ Arel::Nodes::NamedFunction.new(
1156
+ "CAST", [Arel::Nodes::As.new(
1157
+ Arel::Nodes.build_quoted(JSON.generate(value)),
1158
+ Arel::Nodes::SqlLiteral.new("jsonb"))])
1159
+ else
1160
+ AST.json_argument(value, model)
1161
+ end
1162
+ when ::Rational then quote_number(value)
1163
+ else Arel::Nodes.build_quoted(value)
1164
+ end
1165
+ end
1166
+
1167
+ # The keys come from Ruby as Hash keys rather than alternating with
1168
+ # the values as SQL has them, which is what keeps a bare symbol
1169
+ # free to mean a column on the value side. Anything but a name is
1170
+ # refused here, before the adapters answer a NULL key three ways.
1171
+ def check_pairs(pairs)
1172
+ unless pairs.is_a?(::Hash)
1173
+ raise ArgumentError,
1174
+ "json_object takes a hash of keys to values, not #{pairs.inspect}"
1175
+ end
1176
+ pairs.each_key do |key|
1177
+ next if key.is_a?(::String) || key.is_a?(::Symbol)
1178
+ raise ArgumentError,
1179
+ "a key of json_object is a string or a symbol, not #{key.inspect}"
1180
+ end
1181
+ pairs
1182
+ end
1183
+
1184
+ def json_source
1185
+ "json_#{kind}"
1186
+ end
1187
+ end
1188
+
784
1189
  # GROUP BY GROUPING SETS / ROLLUP / CUBE: several groupings asked for at
785
1190
  # once, the totals of each coming back beside the rows. PostgreSQL has
786
- # all three; the block raises for the 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.
787
1193
  #
788
1194
  # Each set is a list of its own, so grouping_sets takes lists and rollup
789
1195
  # and cube take the columns themselves.
@@ -803,16 +1209,32 @@ module ActiveRecord
803
1209
  end
804
1210
 
805
1211
  def to_arel(table, model)
1212
+ return with_rollup(table, model) if AST.adapter_family(model) == :mysql
1213
+
806
1214
  KINDS.fetch(kind).new(
807
1215
  if kind == :grouping_sets
808
1216
  sets.map do |set|
809
1217
  Arel::Nodes::GroupingElement.new(
810
- Array(set).map {|column| to_arel_operand(column, table, model) })
1218
+ Array(set).map { |column| to_arel_operand(column, table, model) })
811
1219
  end
812
1220
  else
813
- sets.map {|column| to_arel_operand(column, table, model) }
1221
+ sets.map { |column| to_arel_operand(column, table, model) }
814
1222
  end)
815
1223
  end
1224
+
1225
+ private
1226
+ # The MySQL family spells rollup WITH ROLLUP, trailing the whole
1227
+ # group list rather than wrapping a list of its own -- which is also
1228
+ # why a rollup cannot stand beside other group entries there. The
1229
+ # columns are compiled by the connection's own visitor, so their
1230
+ # quoting is the adapter's.
1231
+ def with_rollup(table, model)
1232
+ columns = sets.map { |column| to_arel_operand(column, table, model) }
1233
+ sql = model.with_connection do |connection|
1234
+ columns.map { |column| connection.visitor.compile(column) }.join(", ")
1235
+ end
1236
+ Arel.sql("#{sql} WITH ROLLUP")
1237
+ end
816
1238
  end
817
1239
 
818
1240
  class Column < Node
@@ -847,8 +1269,11 @@ module ActiveRecord
847
1269
  end
848
1270
 
849
1271
  def to_arel(table, model)
850
- to_arel_operand(left, table, model).
851
- 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))
852
1277
  end
853
1278
  end
854
1279
 
@@ -859,23 +1284,22 @@ module ActiveRecord
859
1284
  # PostgreSQL has no such operator and would say so.
860
1285
  module BitwiseOperands
861
1286
  private
1287
+ def check_operand(operand, operator)
1288
+ return operand unless operand.is_a?(Predicate)
1289
+ raise ArgumentError,
1290
+ "a condition cannot be an operand of #{operator}; " \
1291
+ "& and | between conditions are AND and OR"
1292
+ end
862
1293
 
863
- def check_operand(operand, operator)
864
- return operand unless operand.is_a?(Predicate)
865
- raise ArgumentError,
866
- "a condition cannot be an operand of #{operator}; " \
867
- "& and | between conditions are AND and OR"
868
- end
869
-
870
- # Only the unqualified column can be checked, since that is the one
871
- # the model is known to have.
872
- def check_not_boolean(operand, operator, model)
873
- return unless operand.is_a?(::Symbol)
874
- return unless model.type_for_attribute(operand).type == :boolean
875
- raise ArgumentError,
876
- "#{operand.inspect} is a boolean column, which #{operator} does " \
877
- "not take; #{operand.inspect}.true? is the condition"
878
- end
1294
+ # Only the unqualified column can be checked, since that is the one
1295
+ # the model is known to have.
1296
+ def check_not_boolean(operand, operator, model)
1297
+ return unless operand.is_a?(::Symbol)
1298
+ return unless model.type_for_attribute(operand).type == :boolean
1299
+ raise ArgumentError,
1300
+ "#{operand.inspect} is a boolean column, which #{operator} does " \
1301
+ "not take; #{operand.inspect}.true? is the condition"
1302
+ end
879
1303
  end
880
1304
 
881
1305
  # SQL's bitwise operators. Each parenthesises itself, which is what
@@ -915,22 +1339,21 @@ module ActiveRecord
915
1339
  end
916
1340
 
917
1341
  private
918
-
919
- # Arel has a node for XOR, but it writes ^ on every adapter, and ^ is
920
- # exponentiation to PostgreSQL -- a wrong answer rather than an error.
921
- # PostgreSQL's own spelling, #, is where a comment starts on MySQL, so
922
- # it cannot be the portable one either. SQLite has no XOR at all;
923
- # (a | b) - (a & b) is it, at the cost of naming each operand twice.
924
- def xor(left, right, model)
925
- case AST.adapter_family(model)
926
- when :postgresql then Arel::Nodes::InfixOperation.new('#', left, right)
927
- when :mysql then Arel::Nodes::BitwiseXor.new(left, right)
928
- else
929
- Arel::Nodes::Subtraction.new(
930
- Arel::Nodes::Grouping.new(Arel::Nodes::BitwiseOr.new(left, right)),
931
- 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
932
1356
  end
933
- end
934
1357
  end
935
1358
 
936
1359
  # ~, which every adapter has. MySQL answers with the unsigned 64-bit
@@ -998,10 +1421,14 @@ module ActiveRecord
998
1421
 
999
1422
  def to_arel(table, model)
1000
1423
  window = Arel::Nodes::Window.new
1001
- partitions.each {|expr| window.partition(to_arel_operand(expr, table, model)) }
1002
- orders.each {|expr| window.order(to_arel_operand(expr, table, model)) }
1424
+ partitions.each { |expr| window.partition(to_arel_operand(expr, table, model)) }
1425
+ orders.each { |expr| window.order(to_arel_operand(expr, table, model)) }
1003
1426
  frame_arel(window) if frame
1004
1427
 
1428
+ # The JSON aggregates cannot ride a window everywhere; the node
1429
+ # itself says where, once the adapter is known.
1430
+ function.check_window(model) if function.is_a?(JsonAggregate)
1431
+
1005
1432
  # A window-only function refuses to build on its own; here is where
1006
1433
  # it is asked for the call itself.
1007
1434
  arel_function =
@@ -1011,43 +1438,42 @@ module ActiveRecord
1011
1438
  end
1012
1439
 
1013
1440
  private
1014
-
1015
- # The frame is a range of rows counted from the current one: negative
1016
- # before it, positive after, 0 the row itself, and an open end for
1017
- # unbounded. `rows(..0)` is what a running total wants.
1018
- def framing(kind, bounds)
1019
- raise ArgumentError, "a window has one frame" if frame
1020
- unless bounds.is_a?(::Range)
1021
- raise ArgumentError, "#{kind} takes a range of rows, as in rows(..0)"
1022
- end
1023
- if bounds.exclude_end?
1024
- raise ArgumentError, "a frame ends on a row rather than before one; use .."
1025
- end
1026
- [bounds.begin, bounds.end].each do |bound|
1027
- next if bound.nil? || bound.is_a?(::Integer)
1028
- raise ArgumentError,
1029
- "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]
1030
1458
  end
1031
- [kind, bounds.begin, bounds.end]
1032
- end
1033
1459
 
1034
- # Arel wants the keyword itself on the left of the BETWEEN, which is
1035
- # what window.rows with no argument hands back.
1036
- def frame_arel(window)
1037
- kind, from, to = frame
1038
- window.frame(
1039
- Arel::Nodes::Between.new(
1040
- window.public_send(kind),
1041
- Arel::Nodes::And.new([bound(from, Arel::Nodes::Preceding.new),
1042
- bound(to, Arel::Nodes::Following.new)])))
1043
- 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
1044
1470
 
1045
- def bound(rows, unbounded)
1046
- return unbounded if rows.nil?
1047
- return Arel::Nodes::CurrentRow.new if rows.zero?
1048
- rows.negative? ? Arel::Nodes::Preceding.new(-rows)
1049
- : Arel::Nodes::Following.new(rows)
1050
- 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
1051
1477
  end
1052
1478
 
1053
1479
  class Aggregate < Node
@@ -1093,15 +1519,80 @@ module ActiveRecord
1093
1519
  end
1094
1520
 
1095
1521
  private
1522
+ def aggregate(over, table, model)
1523
+ arel_operand = to_arel_operand(over, table, model)
1524
+ if function == :count
1525
+ arel_operand.count(distinct)
1526
+ else
1527
+ arel_operand.public_send(function)
1528
+ end
1529
+ end
1530
+ end
1096
1531
 
1097
- def aggregate(over, table, model)
1098
- arel_operand = to_arel_operand(over, table, model)
1099
- if function == :count
1100
- arel_operand.count(distinct)
1101
- else
1102
- 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"
1103
1588
  end
1589
+ call.filter(condition.to_arel(table, model))
1104
1590
  end
1591
+
1592
+ private
1593
+ def json_source
1594
+ "json_#{kind}"
1595
+ end
1105
1596
  end
1106
1597
 
1107
1598
  # A column alias, quoted by the adapter, so that the name asked for is
@@ -1130,12 +1621,11 @@ module ActiveRecord
1130
1621
  end
1131
1622
 
1132
1623
  private
1133
-
1134
- def alias_sql(model)
1135
- name = alias_name.to_s
1136
- return name unless quote
1137
- model.with_connection {|connection| connection.quote_column_name(name) }
1138
- end
1624
+ def alias_sql(model)
1625
+ name = alias_name.to_s
1626
+ return name unless quote
1627
+ model.with_connection { |connection| connection.quote_column_name(name) }
1628
+ end
1139
1629
  end
1140
1630
 
1141
1631
  class Ordering < Node
@@ -1176,11 +1666,54 @@ module ActiveRecord
1176
1666
  end
1177
1667
 
1178
1668
  def to_arel(table, model)
1179
- arel_args = args.map {|arg| to_arel_argument(arg, table, model) }
1669
+ arel_args = args.map { |arg| to_arel_argument(arg, table, model) }
1180
1670
  Arel::Nodes::NamedFunction.new(name, arel_args)
1181
1671
  end
1182
1672
  end
1183
1673
 
1674
+ # Escape hatch for operators without a spelling of their own, the way
1675
+ # fn is for functions. The operator is emitted as written -- whether
1676
+ # the adapter has it is the caller's assertion, as fn's names are --
1677
+ # and the values ride as quoted literals, so on PostgreSQL an untyped
1678
+ # one takes the type of the operand beside it.
1679
+ class Operation < Node
1680
+ include Predications
1681
+ include Arithmetics
1682
+
1683
+ attr_reader :operator, :left, :right
1684
+
1685
+ def initialize(operator, left, right)
1686
+ @operator = AST.check_name(operator, OPERATOR, "operator").to_s
1687
+ @left = check_side(left)
1688
+ @right = check_side(right)
1689
+ end
1690
+
1691
+ def to_arel(table, model)
1692
+ Arel::Nodes::Grouping.new(
1693
+ Arel::Nodes::InfixOperation.new(
1694
+ operator, side(left, table, model), side(right, table, model)))
1695
+ end
1696
+
1697
+ private
1698
+ # An expression operand is parenthesized: an unknown operator's
1699
+ # precedence is unknown too, and PostgreSQL reads its named
1700
+ # operators from the left, so a bare infix on the right would take
1701
+ # the new operator's left side into its own.
1702
+ def side(operand, table, model)
1703
+ arel = to_arel_argument(operand, table, model)
1704
+ operand.is_a?(Node) ? Arel::Nodes::Grouping.new(arel) : arel
1705
+ end
1706
+
1707
+ def check_side(operand)
1708
+ if operand.is_a?(::Hash) || operand.is_a?(::Array) || operand.is_a?(::Set)
1709
+ raise ArgumentError,
1710
+ "#{operand.inspect} has no one SQL spelling; a string says it " \
1711
+ "in the adapter's own, to_json for a document"
1712
+ end
1713
+ operand
1714
+ end
1715
+ end
1716
+
1184
1717
  # ROW_NUMBER and its kind: functions that say nothing without a window.
1185
1718
  # On its own this refuses rather than reaching the database as an error
1186
1719
  # there; over asks it for call_arel instead.
@@ -1283,24 +1816,24 @@ module ActiveRecord
1283
1816
  arel_value =
1284
1817
  case value
1285
1818
  when Node then value.to_arel(table, model)
1819
+ when ::Symbol then column_operand(value, table, model)
1286
1820
  when ActiveRecord::Relation then scalar_subquery(value)
1287
- else value
1821
+ else quote_number(value)
1288
1822
  end
1289
1823
  arel_column.public_send(OPERATOR_MAP.fetch(operator), arel_value)
1290
1824
  end
1291
1825
 
1292
1826
  private
1293
-
1294
- # A relation compared against a column has to yield a single value, so
1295
- # unlike In there is no sensible default select list to fall back on.
1296
- def scalar_subquery(relation)
1297
- if relation.select_values.empty?
1298
- raise ArgumentError,
1299
- "#{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
1300
1836
  end
1301
- relation = relation.send(:apply_join_dependency) if relation.eager_loading?
1302
- relation.arel
1303
- end
1304
1837
  end
1305
1838
 
1306
1839
  # IS TRUE, IS FALSE and their negations, which every adapter spells the
@@ -1316,7 +1849,7 @@ module ActiveRecord
1316
1849
 
1317
1850
  def to_arel(table, model)
1318
1851
  literal = value ? Arel::Nodes::True.new : Arel::Nodes::False.new
1319
- Arel::Nodes::InfixOperation.new(negated ? 'IS NOT' : 'IS',
1852
+ Arel::Nodes::InfixOperation.new(negated ? "IS NOT" : "IS",
1320
1853
  to_arel_operand(operand, table, model), literal)
1321
1854
  end
1322
1855
  end
@@ -1327,19 +1860,18 @@ module ActiveRecord
1327
1860
  # selects the model's primary key.
1328
1861
  module SetSubquery
1329
1862
  private
1330
-
1331
- def set_subquery(relation, spelling)
1332
- relation = relation.send(:apply_join_dependency) if relation.eager_loading?
1333
- if relation.select_values.empty?
1334
- model = relation.model
1335
- if model.composite_primary_key?
1336
- raise ArgumentError,
1337
- "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])
1338
1872
  end
1339
- relation = relation.select(relation.table[model.primary_key])
1873
+ relation.arel
1340
1874
  end
1341
- relation.arel
1342
- end
1343
1875
  end
1344
1876
 
1345
1877
  # IN for a list of values, BETWEEN for a range, IN (SELECT ...) for a
@@ -1347,6 +1879,12 @@ module ActiveRecord
1347
1879
  class In < Predicate
1348
1880
  include SetSubquery
1349
1881
 
1882
+ # Range holds its endpoints to Comparable, which a quoted node is
1883
+ # not, so this quacks the three methods Arel's between reads.
1884
+ QuotedRange = Struct.new(:begin, :end, :exclude_end) do
1885
+ def exclude_end? = exclude_end
1886
+ end
1887
+
1350
1888
  attr_reader :operand, :values, :negated
1351
1889
 
1352
1890
  def initialize(operand, values, negated: false)
@@ -1357,13 +1895,67 @@ module ActiveRecord
1357
1895
 
1358
1896
  def to_arel(table, model)
1359
1897
  arel_operand = to_arel_operand(operand, table, model)
1360
- if values.is_a?(Range)
1361
- 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"))
1362
1910
  else
1363
- 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
1364
1916
  arel_operand.public_send(negated ? :not_in : :in, arg)
1365
1917
  end
1366
1918
  end
1919
+
1920
+ private
1921
+ # An element that is already an expression resolves, a symbol is a
1922
+ # column here as everywhere, a number is quoted as itself, and the
1923
+ # rest ride for Arel to cast by the column.
1924
+ def quote_value(value, table, model)
1925
+ case value
1926
+ when Node then value.to_arel(table, model)
1927
+ when ::Symbol then column_operand(value, table, model)
1928
+ else quote_number(value)
1929
+ end
1930
+ end
1931
+
1932
+ # MySQL leaves IN and BETWEEN out of its JSON comparisons -- they
1933
+ # fall back to another comparison entirely -- so on it a JSON set is
1934
+ # spelled as the comparisons it means: the closed range as its two
1935
+ # bounds, the list as one equality per element. That names the dug
1936
+ # value once per element, the price SQLite's XOR pays per operand;
1937
+ # a negated range needs nothing, Arel writing it as two comparisons
1938
+ # everywhere. MariaDB never gets this far: the endpoints refuse as
1939
+ # they resolve.
1940
+ def json_between?(model)
1941
+ (values.begin.is_a?(JsonLiteral) || values.end.is_a?(JsonLiteral)) &&
1942
+ AST.adapter_family(model) == :mysql
1943
+ end
1944
+
1945
+ def json_list?(model)
1946
+ values.any? { |value| value.is_a?(JsonLiteral) } &&
1947
+ AST.adapter_family(model) == :mysql
1948
+ end
1949
+
1950
+ def json_list(arel_operand, elements)
1951
+ comparisons = elements.map do |element|
1952
+ negated ? arel_operand.not_eq(element) : arel_operand.eq(element)
1953
+ end
1954
+ joined = comparisons.inject do |so_far, piece|
1955
+ negated ? so_far.and(piece) : so_far.or(piece)
1956
+ end
1957
+ negated ? Arel::Nodes::Grouping.new(joined) : joined
1958
+ end
1367
1959
  end
1368
1960
 
1369
1961
  # ANY and ALL, which stand on the right of a comparison and say how many
@@ -1411,7 +2003,7 @@ module ActiveRecord
1411
2003
  end
1412
2004
 
1413
2005
  class Like < Predicate
1414
- ESCAPE = "\\".freeze
2006
+ ESCAPE = "\\"
1415
2007
 
1416
2008
  # Escapes % and _ so that they match literally. The pattern built from
1417
2009
  # the result must be used with ESCAPE, since SQLite has no default
@@ -1423,8 +2015,8 @@ module ActiveRecord
1423
2015
  # ORs one LIKE per pattern, for the shortcuts that accept several
1424
2016
  # literals the way String#start_with? does.
1425
2017
  def self.any(operand, patterns)
1426
- patterns.map {|pattern| new(operand, pattern, ESCAPE) }.
1427
- inject {|left, right| Or.new(left, right) }
2018
+ patterns.map { |pattern| new(operand, pattern, ESCAPE) }.
2019
+ inject { |left, right| Or.new(left, right) }
1428
2020
  end
1429
2021
 
1430
2022
  attr_reader :operand, :pattern, :escape, :case_sensitive, :negated
@@ -1508,21 +2100,20 @@ module ActiveRecord
1508
2100
  end
1509
2101
 
1510
2102
  private
1511
-
1512
- # PostgreSQL array input syntax: elements joined by commas inside
1513
- # braces, and an element is double-quoted whenever it is empty, spells
1514
- # NULL, or contains a character the parser treats specially.
1515
- def array_literal
1516
- encoded = elements.map do |value|
1517
- s = value.to_s
1518
- if s.empty? || s.casecmp?("null") || s.match?(/[\s{},"\\]/)
1519
- "\"#{s.gsub(/["\\]/) {|c| "\\#{c}" }}\""
1520
- else
1521
- 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
1522
2114
  end
2115
+ "{#{encoded.join(',')}}"
1523
2116
  end
1524
- "{#{encoded.join(',')}}"
1525
- end
1526
2117
  end
1527
2118
 
1528
2119
  # Regular expression match: REGEXP on MySQL, ~ on PostgreSQL. SQLite has
@@ -1546,19 +2137,18 @@ module ActiveRecord
1546
2137
  end
1547
2138
 
1548
2139
  private
1549
-
1550
- # A Regexp literal reads naturally with =~, but only its source crosses
1551
- # over; the database has its own dialect and no notion of Ruby's flags.
1552
- # Dropping a flag would silently change what the query matches, so
1553
- # anything beyond a plain literal is refused rather than ignored.
1554
- def regexp_source(regexp)
1555
- unless regexp.options.zero?
1556
- raise ArgumentError,
1557
- "#{regexp.inspect} has options that SQL cannot express; " \
1558
- "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
1559
2151
  end
1560
- regexp.source
1561
- end
1562
2152
  end
1563
2153
 
1564
2154
  class And < Predicate