activerecord-refined 0.7.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9dec7847f9abff657ab248a17613b08464164fcef0780db94ab5f97de8727dd5
4
- data.tar.gz: a544f3202bd459206ad8e94827b18f09f66d8da912c9ba1f130d75b5d1646e02
3
+ metadata.gz: 3ea652837dabe0136a80b1603e2efd7a86a5ca18f966a39a112c10bb05b8f7e4
4
+ data.tar.gz: 791a680c4ed2d87ed61581c8c232b792ab493a9ef5bd30ddc0d1d0ca9c437ec7
5
5
  SHA512:
6
- metadata.gz: 821537f17ddd8cba223cb45b4e9b6f1bd1bc19cccb4c615686f0f16959cc615c8b3d10225b1a9e632dff291abb87291827bed1b62857648378f46b7d2e947463
7
- data.tar.gz: ee7269d0bb16a09a7ec938557303e00133e96723c9341e8bbf58d890c2fd5dbffc6fa016a88bfd8383f78a32448a4522c982cefd68dde0e478e64de10c9b751a
6
+ metadata.gz: 68bcd884eb01ab5bbc72711996e2ed96f3299b00525295c8688734f3f7110cffd2d15775b2d71c9cb78b02eb52983ddd58675ba4d80a73f76aad9042f719c479
7
+ data.tar.gz: f47a5b3f3f43242399e3d964c6630d08c124acdff206a9cec0cd7919e88baf9b399fe2e505e9f2296da7ddd123f0be34404646e052abad1d6a963b189d2424dd
data/README.md CHANGED
@@ -706,11 +706,14 @@ Author.select { sum(case_when { :age >= 60 }.then(1).else(0)).as(:seniors) }
706
706
  ### JSON
707
707
 
708
708
  `dig` reads inside a JSON document, by the name of what `Hash` does. A string
709
- or symbol steps into an object, an integer into an array:
709
+ or symbol steps into an object, an integer into an array, and what comes back
710
+ is still JSON — the way `Hash#dig` hands back the structure itself — for a
711
+ document to be dug into further or asked the JSON questions. `dig_text` gives
712
+ the value as text instead, which is what a comparison wants:
710
713
 
711
714
  ```ruby
712
- Post.where { :meta.dig(:author, :name) == 'alice' }
713
- Post.select { :meta.dig(:tags, 0).as(:first_tag) }
715
+ Post.where { :meta.dig_text(:author, :name) == 'alice' }
716
+ Post.select { :meta.dig(:author).as(:author) }
714
717
  Post.where { :meta.key?(:draft) }
715
718
  Post.where { :meta.contains?(status: 'open') }
716
719
  ```
@@ -720,34 +723,43 @@ three:
720
723
 
721
724
  | | PostgreSQL | SQLite | MySQL |
722
725
  | --- | --- | --- | --- |
723
- | `dig(:a, :b)` | `#>> '{a,b}'` | `->> '$.a.b'` | `JSON_UNQUOTE(JSON_EXTRACT(…, '$.a.b'))` |
724
- | `dig_json(:a)` | `#> '{a}'` | `-> '$.a'` | `JSON_EXTRACT(…, '$.a')` |
726
+ | `dig(:a, :b)` | `#> '{a,b}'` | `-> '$.a.b'` | `JSON_EXTRACT(…, '$.a.b')` |
727
+ | `dig_text(:a, :b)` | `#>> '{a,b}'` | `->> '$.a.b'` | `JSON_UNQUOTE(JSON_EXTRACT(…, '$.a.b'))` |
725
728
  | `key?(:a)` | `jsonb_exists(…, 'a')` | `json_type(…, '$.a') IS NOT NULL` | `JSON_CONTAINS_PATH(…, 'one', '$.a')` |
726
729
  | `contains?(…)` | `@>` | — | `JSON_CONTAINS` |
727
730
 
728
731
  MariaDB answers to the `mysql2` adapter and has none of `->` or `->>`, so the
729
732
  MySQL family goes through the functions, which both have.
730
733
 
731
- `dig` gives text everywhere. SQLite's `->>` would otherwise hand back the value
732
- with its type, so a comparison that worked there would fail on the other two;
733
- a number is compared through a `cast` on all three:
734
+ `dig_text` gives text everywhere. SQLite's `->>` would otherwise hand back the
735
+ value with its type, so a comparison that worked there would fail on the other
736
+ two; a number is compared through a `cast` on all three:
734
737
 
735
738
  ```ruby
736
- Post.where { :meta.dig(:n) == '5' }
737
- Post.where { cast(:meta.dig(:n), 'integer') > 6 } # 'signed' on MySQL
739
+ Post.where { :meta.dig_text(:n) == '5' }
740
+ Post.where { cast(:meta.dig_text(:n), 'integer') > 6 } # 'signed' on MySQL
738
741
  ```
739
742
 
740
743
  The type is the adapter's own name for it, here as everywhere `cast` is used.
741
744
 
745
+ Strings and numbers are where the adapters agree. A JSON boolean comes back as
746
+ `"1"` on SQLite, which turns `true` into SQL's `1` before the text cast, and
747
+ as `"true"` on the other two; a JSON `null` is SQL `NULL` everywhere but
748
+ MariaDB, which spells it `"null"`. A key that is not there is `NULL` on all
749
+ three.
750
+
742
751
  Comparing a dug value with anything but a string raises `ArgumentError` rather
743
- than being left to the adapters, which answer it three ways: `dig(:n) == 5` is
744
- true on SQLite, an error on PostgreSQL and true on MySQL, and
745
- `dig(:flag) == true` is true, an error and false. `cast` is what says which
746
- type was meant, and then all three agree. `dig_json` is refused the other way
747
- about — the JSON for a string carries its quotes, so `dig_json(:name) ==
748
- 'alice'` is false, an error and true — and `dig` is the one that gives the
749
- value. A column, a function or another dug value on the right goes through
750
- untouched; only a Ruby literal is refused.
752
+ than being left to the adapters, which answer it three ways: `dig_text(:n) ==
753
+ 5` is true on SQLite, an error on PostgreSQL and true on MySQL, and
754
+ `dig_text(:flag) == true` is true, an error and false. `cast` is what says
755
+ which type was meant, and then all three agree. `dig` is refused the other way
756
+ about — the JSON for a string carries its quotes, so `dig(:name) == 'alice'`
757
+ is false, an error and true — and `dig_text` is the one that gives the value.
758
+ What `bury` and `except` give back is JSON as `dig`'s is, and is refused the
759
+ same way. A column, a function or another dug value on the right goes through
760
+ untouched; only a Ruby literal is refused. Arithmetic and the bit operators
761
+ are refused outright on both sides — `dig_text(:n) + 1` is 6 on SQLite, an
762
+ error on PostgreSQL and 6.0 on MariaDB — and `cast` settles those too.
751
763
 
752
764
  `bury` sets what `dig` reads: the last argument is the value and the rest are
753
765
  the path to it. The document comes back changed rather than being written
@@ -763,8 +775,10 @@ Post.update_all { { meta: :meta.bury(:copy, :meta.dig(:n)) } }
763
775
  ```
764
776
 
765
777
  A whole document goes in as one — an object or an array rather than the string
766
- that spells it — which each adapter takes its own way round. `bury` is not a
767
- Ruby method; it is the name Ruby considered for the other end of `dig`, and
778
+ that spells it — which each adapter takes its own way round, and a boolean
779
+ goes in as JSON too, which SQLite would otherwise write as its `1`. `bury` is
780
+ not a Ruby method; it is the name Ruby considered for the other end of `dig`,
781
+ and
768
782
  SQL has no one name to borrow here, since PostgreSQL says `jsonb_set` where
769
783
  the others say `JSON_SET`.
770
784
 
@@ -787,31 +801,37 @@ keys, an element by index — and an array literal written without a type is
787
801
  read as the first of them, so `"meta" - '{draft}'` takes out the key spelled
788
802
  `{draft}`, which is nothing, and says nothing about it.
789
803
 
790
- `dig_json` keeps the JSON, for a document to be dug into further or compared
791
- whole so the JSON operations read what it gives, which is the same question
792
- asked of a part of the document rather than of all of it:
804
+ A key deeper in is reached through the chain: `dig` reads the part out,
805
+ `except` takes the key from it, and `bury` puts it back:
806
+
807
+ ```ruby
808
+ Post.update_all { { meta: :meta.bury(:author, :meta.dig(:author).except(:email)) } }
809
+ ```
810
+
811
+ What `dig` gives is a document, so the JSON operations read it — the same
812
+ question asked of a part of the document rather than of all of it:
793
813
 
794
814
  ```ruby
795
- Post.where { :meta.dig_json(:author).key?(:email) }
796
- Post.where { :meta.dig_json(:author).dig(:name) == 'alice' }
797
- Post.update_all { { meta: :meta.dig_json(:author).bury(:name, 'alice') } }
815
+ Post.where { :meta.dig(:author).key?(:email) }
816
+ Post.where { :meta.dig(:author).dig_text(:name) == 'alice' }
817
+ Post.update_all { { meta: :meta.dig(:author).bury(:name, 'alice') } }
798
818
  ```
799
819
 
800
820
  Containment reads it too, on the adapters that have containment at all:
801
821
 
802
822
  ```ruby
803
- Post.where { :meta.dig_json(:tags).contains?(['ruby']) }
823
+ Post.where { :meta.dig(:tags).contains?(['ruby']) }
804
824
  ```
805
825
 
806
- Asking the same of `dig` raises `ArgumentError`: what it gives is text, and
807
- reading text back as a document is where the adapters part company — SQLite
808
- parses it, MySQL takes it as written, and PostgreSQL has no such function for
809
- text at all.
826
+ Asking the same of `dig_text` raises `ArgumentError`: what it gives is text,
827
+ and reading text back as a document is where the adapters part company —
828
+ SQLite parses it, MySQL takes it as written, and PostgreSQL has no such
829
+ function for text at all.
810
830
 
811
831
  `contains?` has no equivalent on SQLite and raises `NotImplementedError`
812
832
  there — later than the rest, since the adapter is only known when the SQL is
813
- built. On PostgreSQL, `contains?` and `key?` want a `jsonb` column; the
814
- `json` type carries neither operator.
833
+ built. On PostgreSQL, `dig` and `dig_text` are all the `json` type carries;
834
+ `key?`, `contains?`, `bury` and `except` want a `jsonb` column.
815
835
 
816
836
  A key that is not a plain name travels as itself rather than being refused:
817
837
  `dig(:'odd key')` becomes `'{odd key}'` or `$."odd key"`.
data/examples/json.rb CHANGED
@@ -30,45 +30,45 @@ def show(title, relation, rows = nil)
30
30
  puts
31
31
  end
32
32
 
33
- # 1. Reading. dig takes the path Hash#dig takes: a string or symbol steps
34
- # into an object, an integer into an array. What comes back is the value
35
- # rather than the JSON around it, which is what a comparison wants.
36
- show('dig reads a value out of the document',
37
- Document.select { [:name, :meta.dig(:author, :name).as(:author)] },
38
- Document.select { [:name, :meta.dig(:author, :name).as(:author)] }.
33
+ # 1. Reading. dig_text takes the path Hash#dig takes: a string or symbol
34
+ # steps into an object, an integer into an array. What comes back is the
35
+ # value rather than the JSON around it, which is what a comparison wants.
36
+ show('dig_text reads a value out of the document',
37
+ Document.select { [:name, :meta.dig_text(:author, :name).as(:author)] },
38
+ Document.select { [:name, :meta.dig_text(:author, :name).as(:author)] }.
39
39
  map {|d| [d.name, d.author] })
40
40
 
41
41
  show('an integer steps into an array',
42
- Document.select { [:name, :meta.dig(:tags, 0).as(:first_tag)] },
43
- Document.select { [:name, :meta.dig(:tags, 0).as(:first_tag)] }.
42
+ Document.select { [:name, :meta.dig_text(:tags, 0).as(:first_tag)] },
43
+ Document.select { [:name, :meta.dig_text(:tags, 0).as(:first_tag)] }.
44
44
  map {|d| [d.name, d.first_tag] })
45
45
 
46
46
  # A dug value is an expression like any other, so it compares and orders.
47
- show('dig in a condition',
48
- Document.where { :meta.dig(:author, :country) == 'JP' },
49
- Document.where { :meta.dig(:author, :country) == 'JP' }.pluck(:name))
47
+ show('dig_text in a condition',
48
+ Document.where { :meta.dig_text(:author, :country) == 'JP' },
49
+ Document.where { :meta.dig_text(:author, :country) == 'JP' }.pluck(:name))
50
50
 
51
51
  # A dug value is text, so a number goes through a cast. Comparing it with
52
52
  # one instead is refused rather than left to the adapters, which answer that
53
53
  # three ways: true here, an error on PostgreSQL, true on MySQL.
54
54
  show('a number wants a cast',
55
- Document.where { cast(:meta.dig(:views), 'integer') > 100 },
56
- Document.where { cast(:meta.dig(:views), 'integer') > 100 }.pluck(:name))
55
+ Document.where { cast(:meta.dig_text(:views), 'integer') > 100 },
56
+ Document.where { cast(:meta.dig_text(:views), 'integer') > 100 }.pluck(:name))
57
57
 
58
58
  begin
59
- Document.where { :meta.dig(:views) > 100 }
59
+ Document.where { :meta.dig_text(:views) > 100 }
60
60
  rescue ArgumentError => e
61
61
  puts '--- and without one it says so ---'
62
62
  puts " #{e.message}"
63
63
  puts
64
64
  end
65
65
 
66
- # dig_json keeps the JSON, for a part of the document to be dug into further
67
- # or compared whole. The quotes around the string are the sign of it -- and
68
- # the reason a Ruby value is refused on this side too.
69
- show('dig_json keeps the JSON',
70
- Document.select { [:name, :meta.dig_json(:author).as(:author)] },
71
- Document.select { [:name, :meta.dig_json(:author).as(:author)] }.
66
+ # dig keeps the JSON, for a part of the document to be dug into further
67
+ # or asked the JSON questions. The quotes around the string are the sign of
68
+ # it -- and the reason a Ruby value is refused on this side too.
69
+ show('dig keeps the JSON',
70
+ Document.select { [:name, :meta.dig(:author).as(:author)] },
71
+ Document.select { [:name, :meta.dig(:author).as(:author)] }.
72
72
  map {|d| [d.name, d.author] })
73
73
 
74
74
  # 2. Asking whether a key is there at all, which is not the same as asking
@@ -217,15 +217,16 @@ module ActiveRecord
217
217
 
218
218
  # Reading inside a JSON document, by the name of what Hash does. A
219
219
  # string or symbol steps into an object, an integer into an array, and
220
- # what comes back is the value rather than the JSON, since that is what
221
- # a comparison wants. dig_json keeps it JSON, for a document to be dug
222
- # into further or compared whole.
220
+ # what comes back is still JSON, the way Hash#dig hands back the
221
+ # structure itself -- for a document to be dug into further or asked
222
+ # the JSON questions. dig_text gives the value as text instead,
223
+ # which is what a comparison wants.
223
224
  def dig(*path)
224
- JsonPath.new(self, path)
225
+ JsonPath.new(self, path, as_json: true)
225
226
  end
226
227
 
227
- def dig_json(*path)
228
- JsonPath.new(self, path, as_json: true)
228
+ def dig_text(*path)
229
+ JsonPath.new(self, path)
229
230
  end
230
231
 
231
232
  # Keys taken out of a JSON document, by the name of what Hash does,
@@ -513,20 +514,26 @@ module ActiveRecord
513
514
  #
514
515
  # The path is turned into a string either way, so a key with a space or
515
516
  # a quote in it travels as itself rather than having to be refused.
516
- # What a dug value may be compared with. dig gives text on every
517
+ # What a dug value may be compared with. dig_text gives text on every
517
518
  # adapter, and what a text value compared with a number means is a
518
- # question the three answer three ways: `dig(:n) == 5` is true on
519
+ # question the three answer three ways: `dig_text(:n) == 5` is true on
519
520
  # SQLite, an error on PostgreSQL and true on MySQL, while
520
- # `dig(:flag) == true` is true, an error, and false. cast is what says
521
- # which type was meant, and then all three agree.
521
+ # `dig_text(:flag) == true` is true, an error, and false. cast is what
522
+ # says which type was meant, and then all three agree.
523
+ #
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.
522
529
  #
523
- # dig_json is refused the other way about: the JSON for a string carries
524
- # its quotes, so `dig_json(:name) == 'alice'` is false on SQLite, an
525
- # error on PostgreSQL and true on MySQL. dig is the one that gives the
526
- # value.
530
+ # A string against dig_text, and anything the block itself built -- a
531
+ # column, a function, another dug value -- go through untouched.
527
532
  #
528
- # A string against dig, and anything the block itself built -- a column,
529
- # a function, another dug value -- go through untouched.
533
+ # Arithmetic and the bit operators are refused outright on both sides:
534
+ # `dig_text(:n) + 1` is 6 on SQLite, an error on PostgreSQL and 6.0 on
535
+ # MariaDB, and an expression on the right does not change what the
536
+ # dug side is.
530
537
  module JsonComparable
531
538
  %i[== != < <= > >=].each do |operator|
532
539
  define_method(operator) do |other|
@@ -540,6 +547,16 @@ module ActiveRecord
540
547
  def between?(min, max) = super(*check_each([min, max]))
541
548
  def not_between?(min, max) = super(*check_each([min, max]))
542
549
 
550
+ %i[+ - * / & | ^ << >>].each do |operator|
551
+ define_method(operator) do |_other|
552
+ raise ArgumentError, arithmetic_refusal(operator)
553
+ end
554
+ end
555
+
556
+ def ~
557
+ raise ArgumentError, arithmetic_refusal(:~)
558
+ end
559
+
543
560
  private
544
561
 
545
562
  # nil is left to the comparison itself, which says to use null?, and
@@ -552,9 +569,9 @@ module ActiveRecord
552
569
  return if other.is_a?(::String) && !as_json
553
570
 
554
571
  raise ArgumentError, as_json ?
555
- "dig_json gives JSON, and comparing it with #{other.inspect} " \
556
- "means something different on every adapter; dig gives the value" :
557
- "dig gives text, and comparing it with #{other.inspect} means " \
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 " \
558
575
  "something different on every adapter; cast it to the type meant"
559
576
  end
560
577
 
@@ -566,20 +583,28 @@ module ActiveRecord
566
583
  end
567
584
  values
568
585
  end
586
+
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"
593
+ end
569
594
  end
570
595
 
571
- # The JSON operations read a document, and what dig_json gives is one:
572
- # `dig_json(:author).key?(:email)` and `dig_json(:tags).contains?(...)`
573
- # are the same question asked of a part of it, and the adapters answer
574
- # them alike. What dig gives is text, and reading that as a document
596
+ # The JSON operations read a document, and what dig gives is one:
597
+ # `dig(:author).key?(:email)` and `dig(:tags).contains?(...)` are the
598
+ # same question asked of a part of it, and the adapters answer them
599
+ # alike. What dig_text gives is text, and reading that as a document
575
600
  # again is where they part company: SQLite parses it back and MySQL
576
601
  # takes it as written, where PostgreSQL has no such function for text.
577
602
  module JsonDocument
578
- %i[dig dig_json key? contains? bury except].each do |name|
603
+ %i[dig dig_text key? contains? bury except].each do |name|
579
604
  define_method(name) do |*args|
580
605
  unless as_json
581
606
  raise ArgumentError,
582
- "dig gives text, and #{name} reads JSON; dig_json keeps it"
607
+ "dig_text gives text, and #{name} reads JSON; dig keeps it"
583
608
  end
584
609
  super(*args)
585
610
  end
@@ -615,13 +640,18 @@ module ActiveRecord
615
640
  extracted = Arel::Nodes::InfixOperation.new(
616
641
  as_json ? :"->" : :"->>", document, Arel::Nodes.build_quoted(dollar_path))
617
642
  # SQLite's ->> gives back the value with its type, where the other
618
- # two give text. Cast so that `dig(:n) == '5'` means the same
619
- # thing everywhere, and a number wants a cast everywhere too.
643
+ # two give text. Cast so that `dig_text(:n) == '5'` means the
644
+ # same thing everywhere, and a number wants a cast everywhere too.
620
645
  as_json ? extracted : Arel::Nodes::NamedFunction.new(
621
646
  'CAST', [Arel::Nodes::As.new(extracted, Arel::Nodes::SqlLiteral.new('text'))])
622
647
  end
623
648
  end
624
649
 
650
+ private
651
+
652
+ def json_source
653
+ 'dig'
654
+ end
625
655
  end
626
656
 
627
657
  # Setting a value inside a JSON document, which is what bury does to what
@@ -630,6 +660,7 @@ module ActiveRecord
630
660
  class JsonSet < Node
631
661
  include Predications
632
662
  include JsonSteps
663
+ include JsonComparable
633
664
 
634
665
  attr_reader :operand, :path, :value
635
666
 
@@ -639,6 +670,11 @@ module ActiveRecord
639
670
  @value = value
640
671
  end
641
672
 
673
+ # Always JSON, which is what the comparison guard asks.
674
+ def as_json
675
+ true
676
+ end
677
+
642
678
  def to_arel(table, model)
643
679
  document = to_arel_operand(operand, table, model)
644
680
  if AST.adapter_family(model) == :postgresql
@@ -663,21 +699,34 @@ module ActiveRecord
663
699
  Arel::Nodes.build_quoted(JSON.generate(value))
664
700
  end
665
701
 
666
- # The others take the value as it is, except a whole document, which
667
- # they read out of a literal rather than take as a string. MySQL casts
668
- # to JSON where MariaDB, which answers to the same adapter, does not.
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.
669
707
  def other_value(table, model)
670
708
  return to_arel_operand(value, table, model) if expression?
671
- return Arel::Nodes.build_quoted(value) unless value.is_a?(::Hash) || value.is_a?(::Array)
709
+ unless value.is_a?(::Hash) || value.is_a?(::Array) ||
710
+ value == true || value == false
711
+ return Arel::Nodes.build_quoted(value)
712
+ end
672
713
 
673
- Arel::Nodes::NamedFunction.new(
674
- 'JSON_EXTRACT',
675
- [Arel::Nodes.build_quoted(JSON.generate(value)), Arel::Nodes.build_quoted('$')])
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('$')])
720
+ end
676
721
  end
677
722
 
678
723
  def expression?
679
724
  value.is_a?(Node) || value.is_a?(::Symbol)
680
725
  end
726
+
727
+ def json_source
728
+ 'bury'
729
+ end
681
730
  end
682
731
 
683
732
  # Keys taken out of a JSON document. PostgreSQL subtracts them, the
@@ -685,6 +734,7 @@ module ActiveRecord
685
734
  class JsonExcept < Node
686
735
  include Predications
687
736
  include JsonSteps
737
+ include JsonComparable
688
738
 
689
739
  attr_reader :operand, :keys
690
740
 
@@ -693,10 +743,18 @@ module ActiveRecord
693
743
  @keys = check_keys(keys)
694
744
  end
695
745
 
746
+ def as_json
747
+ true
748
+ end
749
+
696
750
  def to_arel(table, model)
697
751
  document = to_arel_operand(operand, table, model)
698
- return Arel::Nodes::InfixOperation.new(:-, document, key_array) if
699
- AST.adapter_family(model) == :postgresql
752
+ if AST.adapter_family(model) == :postgresql
753
+ # Grouped because - binds tighter than #>: dug out of a document,
754
+ # the subtraction would otherwise take the path literal first.
755
+ return Arel::Nodes::InfixOperation.new(
756
+ :-, Arel::Nodes::Grouping.new(document), key_array)
757
+ end
700
758
 
701
759
  Arel::Nodes::NamedFunction.new(
702
760
  'JSON_REMOVE',
@@ -727,6 +785,10 @@ module ActiveRecord
727
785
  end
728
786
  keys
729
787
  end
788
+
789
+ def json_source
790
+ 'except'
791
+ end
730
792
  end
731
793
 
732
794
  # JSON containment: whether the document holds what is given.
@@ -1,5 +1,5 @@
1
1
  module Activerecord
2
2
  module Refined
3
- VERSION = '0.7.0'
3
+ VERSION = '0.8.1'
4
4
  end
5
5
  end
@@ -1891,115 +1891,144 @@ class TestBlockSyntax < Minitest::Test
1891
1891
  Doc.create!(name: 'two', meta: json_document({ 'n' => 9 }))
1892
1892
  end
1893
1893
 
1894
- def test_dig_a_key
1894
+ def test_dig_text_a_key
1895
1895
  seed_docs
1896
- assert_equal(%w[5 9], Doc.order(:name).select { :meta.dig(:n).as(:v) }.map(&:v))
1896
+ assert_equal(%w[5 9], Doc.order(:name).select { :meta.dig_text(:n).as(:v) }.map(&:v))
1897
1897
  end
1898
1898
 
1899
- def test_dig_a_path
1899
+ def test_dig_text_a_path
1900
1900
  seed_docs
1901
1901
  assert_equal(['deep', nil],
1902
- Doc.order(:name).select { :meta.dig(:a, :b).as(:v) }.map(&:v))
1902
+ Doc.order(:name).select { :meta.dig_text(:a, :b).as(:v) }.map(&:v))
1903
1903
  end
1904
1904
 
1905
- def test_dig_an_array_index
1905
+ def test_dig_text_an_array_index
1906
1906
  seed_docs
1907
1907
  assert_equal(['x', nil],
1908
- Doc.order(:name).select { :meta.dig(:tags, 0).as(:v) }.map(&:v))
1908
+ Doc.order(:name).select { :meta.dig_text(:tags, 0).as(:v) }.map(&:v))
1909
1909
  end
1910
1910
 
1911
1911
  # A key that is not a plain name travels as itself rather than being refused.
1912
- def test_dig_a_key_that_needs_quoting
1912
+ def test_dig_text_a_key_that_needs_quoting
1913
1913
  seed_docs
1914
1914
  assert_equal(['1', nil],
1915
- Doc.order(:name).select { :meta.dig(:'odd key').as(:v) }.map(&:v))
1915
+ Doc.order(:name).select { :meta.dig_text(:'odd key').as(:v) }.map(&:v))
1916
1916
  end
1917
1917
 
1918
- # dig gives text on every adapter -- SQLite's ->> would otherwise give the
1918
+ # dig_text gives text on every adapter -- SQLite's ->> would otherwise give the
1919
1919
  # value with its type -- so a number is compared through a cast.
1920
- def test_dig_is_text_everywhere
1920
+ def test_dig_text_is_text_everywhere
1921
1921
  seed_docs
1922
- assert_equal(['one'], Doc.where { :meta.dig(:n) == '5' }.pluck(:name))
1922
+ assert_equal(['one'], Doc.where { :meta.dig_text(:n) == '5' }.pluck(:name))
1923
1923
  type = integer_type
1924
- assert_equal(['two'], Doc.where { cast(:meta.dig(:n), type) > 6 }.pluck(:name))
1924
+ assert_equal(['two'], Doc.where { cast(:meta.dig_text(:n), type) > 6 }.pluck(:name))
1925
1925
  end
1926
1926
 
1927
- def test_dig_json_keeps_the_json
1927
+ def test_dig_keeps_the_json
1928
1928
  seed_docs
1929
- value = Doc.where { :name == 'one' }.select { :meta.dig_json(:tags).as(:v) }.first.v
1929
+ value = Doc.where { :name == 'one' }.select { :meta.dig(:tags).as(:v) }.first.v
1930
1930
  assert_equal(%w[x y], value.is_a?(String) ? JSON.parse(value) : value)
1931
1931
  end
1932
1932
 
1933
1933
  # What text compared with a number means is a question the three adapters
1934
- # answer three ways -- `dig(:n) == 5` is true on SQLite, an error on
1935
- # PostgreSQL and true on MySQL, and `dig(:flag) == true` is true, an error
1934
+ # answer three ways -- `dig_text(:n) == 5` is true on SQLite, an error on
1935
+ # PostgreSQL and true on MySQL, and `dig_text(:flag) == true` is true, an error
1936
1936
  # and false -- so the comparison is refused rather than left to them.
1937
- def test_dig_refuses_a_comparison_with_anything_but_text
1938
- e = assert_raises(ArgumentError) { Doc.where { :meta.dig(:n) == 5 } }
1937
+ def test_dig_text_refuses_a_comparison_with_anything_but_text
1938
+ e = assert_raises(ArgumentError) { Doc.where { :meta.dig_text(:n) == 5 } }
1939
1939
  assert_match(/cast/, e.message)
1940
- assert_raises(ArgumentError) { Doc.where { :meta.dig(:n) != 5 } }
1941
- assert_raises(ArgumentError) { Doc.where { :meta.dig(:n) > 6 } }
1942
- assert_raises(ArgumentError) { Doc.where { :meta.dig(:flag) == true } }
1943
- assert_raises(ArgumentError) { Doc.where { :meta.dig(:n).in?([1, 2]) } }
1944
- assert_raises(ArgumentError) { Doc.where { :meta.dig(:n).between?(1, 9) } }
1940
+ assert_raises(ArgumentError) { Doc.where { :meta.dig_text(:n) != 5 } }
1941
+ assert_raises(ArgumentError) { Doc.where { :meta.dig_text(:n) > 6 } }
1942
+ assert_raises(ArgumentError) { Doc.where { :meta.dig_text(:flag) == true } }
1943
+ assert_raises(ArgumentError) { Doc.where { :meta.dig_text(:n).in?([1, 2]) } }
1944
+ assert_raises(ArgumentError) { Doc.where { :meta.dig_text(:n).between?(1, 9) } }
1945
1945
  end
1946
1946
 
1947
1947
  # A string is what a dug value compares to; so is anything the block built
1948
1948
  # rather than wrote as a literal, since that is nobody's guess to make.
1949
- def test_dig_compares_with_text_and_with_expressions
1949
+ def test_dig_text_compares_with_text_and_with_expressions
1950
1950
  seed_docs
1951
- assert_equal(['one'], Doc.where { :meta.dig(:n) == '5' }.pluck(:name))
1952
- assert_equal([], Doc.where { :meta.dig(:n) == :name }.pluck(:name))
1953
- assert_equal(['one'], Doc.where { :meta.dig(:n) == upper('5') }.pluck(:name))
1951
+ assert_equal(['one'], Doc.where { :meta.dig_text(:n) == '5' }.pluck(:name))
1952
+ assert_equal([], Doc.where { :meta.dig_text(:n) == :name }.pluck(:name))
1953
+ assert_equal(['one'], Doc.where { :meta.dig_text(:n) == upper('5') }.pluck(:name))
1954
1954
  type = integer_type
1955
- assert_equal(['one'], Doc.where { cast(:meta.dig(:n), type) == 5 }.pluck(:name))
1955
+ assert_equal(['one'], Doc.where { cast(:meta.dig_text(:n), type) == 5 }.pluck(:name))
1956
1956
  end
1957
1957
 
1958
1958
  # The JSON for a string carries its quotes, so the same comparison is
1959
1959
  # refused the other way about: false on SQLite, an error on PostgreSQL and
1960
1960
  # true on MySQL.
1961
- def test_dig_json_refuses_a_comparison_with_a_ruby_value
1962
- e = assert_raises(ArgumentError) { Doc.where { :meta.dig_json(:a) == 'deep' } }
1963
- assert_match(/dig gives the value/, e.message)
1964
- assert_raises(ArgumentError) { Doc.where { :meta.dig_json(:n) == 5 } }
1961
+ def test_dig_refuses_a_comparison_with_a_ruby_value
1962
+ e = assert_raises(ArgumentError) { Doc.where { :meta.dig(:a) == 'deep' } }
1963
+ assert_match(/dig_text gives the value/, e.message)
1964
+ assert_raises(ArgumentError) { Doc.where { :meta.dig(:n) == 5 } }
1965
1965
  end
1966
1966
 
1967
- # What dig_json gives is a document, so the JSON operations read it: the
1967
+ # What dig gives is a document, so the JSON operations read it: the
1968
1968
  # same question asked of a part rather than of the whole.
1969
- def test_the_json_operations_read_what_dig_json_kept
1969
+ def test_the_json_operations_read_what_dig_kept
1970
1970
  seed_docs
1971
- assert_equal(['one'], Doc.where { :meta.dig_json(:a).key?(:b) }.pluck(:name))
1972
- assert_equal(%w[one two], Doc.where { :meta.dig_json(:n).not_null? }.order(:name).pluck(:name))
1971
+ assert_equal(['one'], Doc.where { :meta.dig(:a).key?(:b) }.pluck(:name))
1972
+ assert_equal(%w[one two], Doc.where { :meta.dig(:n).not_null? }.order(:name).pluck(:name))
1973
1973
  assert_equal(['one'],
1974
- Doc.where { :meta.dig_json(:a).dig(:b) == 'deep' }.pluck(:name))
1974
+ Doc.where { :meta.dig(:a).dig_text(:b) == 'deep' }.pluck(:name))
1975
1975
  value = Doc.where { :name == 'one' }.
1976
- select { :meta.dig_json(:a).bury(:b, 'x').as(:v) }.first.v
1976
+ select { :meta.dig(:a).bury(:b, 'x').as(:v) }.first.v
1977
1977
  assert_equal('x', (value.is_a?(String) ? JSON.parse(value) : value)['b'])
1978
1978
  end
1979
1979
 
1980
- def test_containment_reads_what_dig_json_kept
1980
+ def test_containment_reads_what_dig_kept
1981
1981
  skip_without_json_containment
1982
1982
  seed_docs
1983
- assert_equal(['one'], Doc.where { :meta.dig_json(:tags).contains?(['x']) }.pluck(:name))
1984
- assert_equal([], Doc.where { :meta.dig_json(:tags).contains?(['z']) }.pluck(:name))
1983
+ assert_equal(['one'], Doc.where { :meta.dig(:tags).contains?(['x']) }.pluck(:name))
1984
+ assert_equal([], Doc.where { :meta.dig(:tags).contains?(['z']) }.pluck(:name))
1985
1985
  end
1986
1986
 
1987
1987
  # Reading text back as a document is where the adapters part company:
1988
1988
  # SQLite parses it, MySQL takes it as written, PostgreSQL has no such
1989
1989
  # function for text at all.
1990
1990
  def test_the_json_operations_are_refused_on_a_dug_value
1991
- e = assert_raises(ArgumentError) { Doc.where { :meta.dig(:a).key?(:b) } }
1992
- assert_match(/dig_json keeps it/, e.message)
1993
- assert_raises(ArgumentError) { Doc.where { :meta.dig(:a).contains?(b: 1) } }
1994
- assert_raises(ArgumentError) { Doc.select { :meta.dig(:a).dig(:b) } }
1995
- assert_raises(ArgumentError) { Doc.select { :meta.dig(:a).dig_json(:b) } }
1996
- assert_raises(ArgumentError) { Doc.select { :meta.dig(:a).bury(:b, 'x') } }
1997
- assert_raises(ArgumentError) { Doc.select { :meta.dig(:a).except(:b) } }
1991
+ e = assert_raises(ArgumentError) { Doc.where { :meta.dig_text(:a).key?(:b) } }
1992
+ assert_match(/dig keeps it/, e.message)
1993
+ assert_raises(ArgumentError) { Doc.where { :meta.dig_text(:a).contains?(b: 1) } }
1994
+ assert_raises(ArgumentError) { Doc.select { :meta.dig_text(:a).dig_text(:b) } }
1995
+ assert_raises(ArgumentError) { Doc.select { :meta.dig_text(:a).dig(:b) } }
1996
+ assert_raises(ArgumentError) { Doc.select { :meta.dig_text(:a).bury(:b, 'x') } }
1997
+ assert_raises(ArgumentError) { Doc.select { :meta.dig_text(:a).except(:b) } }
1998
+ end
1999
+
2000
+ # What bury and except give back is JSON as dig's is, so the same guard
2001
+ # covers them; an expression on the right still goes through.
2002
+ def test_bury_and_except_refuse_a_comparison_with_a_ruby_value
2003
+ e = assert_raises(ArgumentError) { Doc.where { :meta.bury(:a, 1) == '{"a": 1}' } }
2004
+ assert_match(/bury gives JSON/, e.message)
2005
+ e = assert_raises(ArgumentError) { Doc.where { :meta.except(:a) == '{"b": 2}' } }
2006
+ assert_match(/except gives JSON/, e.message)
2007
+ assert_raises(ArgumentError) { Doc.where { :meta.except(:a).in?(['{}']) } }
2008
+ assert_sql(/ = /, Doc.where { :meta.except(:a) == :meta.except(:b) })
2009
+ end
2010
+
2011
+ # Arithmetic is refused like a literal comparison is: text plus one is 6
2012
+ # on SQLite, an error on PostgreSQL and 6.0 on MariaDB. cast settles it,
2013
+ # and an expression on the right changes nothing about the dug side.
2014
+ def test_arithmetic_is_refused_on_a_dug_value
2015
+ e = assert_raises(ArgumentError) { Doc.select { :meta.dig_text(:n) + 1 } }
2016
+ assert_match(/cast it to the type meant/, e.message)
2017
+ e = assert_raises(ArgumentError) { Doc.select { :meta.dig(:n) * 2 } }
2018
+ assert_match(/dig gives JSON/, e.message)
2019
+ assert_raises(ArgumentError) { Doc.select { :meta.dig_text(:n) + :name } }
2020
+ assert_raises(ArgumentError) { Doc.select { :meta.bury(:a, 1) - 1 } }
2021
+ assert_raises(ArgumentError) { Doc.select { ~:meta.dig(:n) } }
2022
+ seed_docs
2023
+ type = integer_type
2024
+ assert_equal(6,
2025
+ Doc.where { :name == 'one' }.
2026
+ select { (cast(:meta.dig_text(:n), type) + 1).as(:v) }.first.v.to_i)
1998
2027
  end
1999
2028
 
2000
- def test_dig_from_a_qualified_column
2029
+ def test_dig_text_from_a_qualified_column
2001
2030
  seed_docs
2002
- assert_equal(['one'], Doc.where { :docs[:meta].dig(:a, :b) == 'deep' }.pluck(:name))
2031
+ assert_equal(['one'], Doc.where { :docs[:meta].dig_text(:a, :b) == 'deep' }.pluck(:name))
2003
2032
  end
2004
2033
 
2005
2034
  def test_key
@@ -2022,7 +2051,7 @@ class TestBlockSyntax < Minitest::Test
2022
2051
 
2023
2052
  def test_dig_needs_a_path
2024
2053
  assert_raises(ArgumentError) { Doc.select { :meta.dig } }
2025
- e = assert_raises(ArgumentError) { Doc.select { :meta.dig(1.5) } }
2054
+ e = assert_raises(ArgumentError) { Doc.select { :meta.dig_text(1.5) } }
2026
2055
  assert_match(/key or an array index/, e.message)
2027
2056
  end
2028
2057
 
@@ -2277,13 +2306,27 @@ class TestBlockSyntax < Minitest::Test
2277
2306
  assert_equal([1, 2], buried { { meta: :meta.bury(:arr, [1, 2]) } }['arr'])
2278
2307
  end
2279
2308
 
2309
+ # A boolean goes in as JSON too: taken as it is, SQLite would write its 1.
2310
+ def test_bury_a_boolean
2311
+ assert_equal(true, buried { { meta: :meta.bury(:flag, true) } }['flag'])
2312
+ assert_equal(false, buried { { meta: :meta.bury(:flag, false) } }['flag'])
2313
+ end
2314
+
2315
+ def test_bury_a_null
2316
+ document = buried { { meta: :meta.bury(:gone, nil) } }
2317
+ assert(document.key?('gone'))
2318
+ assert_nil(document['gone'])
2319
+ end
2320
+
2280
2321
  def test_bury_an_array_index
2281
2322
  assert_equal(%w[7 y], buried { { meta: :meta.bury(:tags, 0, '7') } }['tags'])
2282
2323
  end
2283
2324
 
2284
- # The value can be read out of the document it is going into.
2325
+ # The value can be read out of the document it is going into: dig keeps
2326
+ # the number a number, dig_text makes it the text of one.
2285
2327
  def test_bury_an_expression
2286
- assert_equal('5', buried { { meta: :meta.bury(:copy, :meta.dig(:n)) } }['copy'].to_s)
2328
+ assert_equal(5, buried { { meta: :meta.bury(:copy, :meta.dig(:n)) } }['copy'])
2329
+ assert_equal('5', buried { { meta: :meta.bury(:copy, :meta.dig_text(:n)) } }['copy'])
2287
2330
  end
2288
2331
 
2289
2332
  # It is an expression, so it does not have to be written anywhere.
@@ -2306,6 +2349,13 @@ class TestBlockSyntax < Minitest::Test
2306
2349
  buried { { meta: :meta.except(:n) } })
2307
2350
  end
2308
2351
 
2352
+ # A key deeper in is reached through the chain: dig reads the part out,
2353
+ # except takes the key from it, and bury puts it back. The dug document
2354
+ # needs its parentheses on PostgreSQL, where - binds tighter than #>.
2355
+ def test_except_a_nested_key_through_the_chain
2356
+ assert_equal({}, buried { { meta: :meta.bury(:a, :meta.dig(:a).except(:b)) } }['a'])
2357
+ end
2358
+
2309
2359
  def test_except_several_keys
2310
2360
  assert_equal({ 'a' => { 'b' => 'deep' } },
2311
2361
  buried { { meta: :meta.except(:n, :tags, :'odd key') } })
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord-refined
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.8.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shugo Maeda