activerecord-refined 0.8.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: aaf1859c70b9ca6e5c245343e664fffbd483d395266a4d442eaa0d5c4107595c
4
- data.tar.gz: da1ddb190628a8a11ced1d105d0a73bf164892cf9e78dfd68db7fdf135047d95
3
+ metadata.gz: 3ea652837dabe0136a80b1603e2efd7a86a5ca18f966a39a112c10bb05b8f7e4
4
+ data.tar.gz: 791a680c4ed2d87ed61581c8c232b792ab493a9ef5bd30ddc0d1d0ca9c437ec7
5
5
  SHA512:
6
- metadata.gz: 5b075b93194651859a7411d2465fec0c2d0a504eaf9da7d4d296ddea1cc91a366af68fea0b37cbea230e44da9e4bf480ecfd047571c1b49601b9bfa7c3dc2780
7
- data.tar.gz: c35194020d37a97de9f5816b965f391a47c5e3d12ef7192bfd2c91ff16ea72689e122fc29daba3c6096aceac22dd7fbb535d562fe0c0ba2f3edbcbbb9904c22c
6
+ metadata.gz: 68bcd884eb01ab5bbc72711996e2ed96f3299b00525295c8688734f3f7110cffd2d15775b2d71c9cb78b02eb52983ddd58675ba4d80a73f76aad9042f719c479
7
+ data.tar.gz: f47a5b3f3f43242399e3d964c6630d08c124acdff206a9cec0cd7919e88baf9b399fe2e505e9f2296da7ddd123f0be34404646e052abad1d6a963b189d2424dd
data/README.md CHANGED
@@ -723,7 +723,7 @@ three:
723
723
 
724
724
  | | PostgreSQL | SQLite | MySQL |
725
725
  | --- | --- | --- | --- |
726
- | `dig(:a)` | `#> '{a}'` | `-> '$.a'` | `JSON_EXTRACT(…, '$.a')` |
726
+ | `dig(:a, :b)` | `#> '{a,b}'` | `-> '$.a.b'` | `JSON_EXTRACT(…, '$.a.b')` |
727
727
  | `dig_text(:a, :b)` | `#>> '{a,b}'` | `->> '$.a.b'` | `JSON_UNQUOTE(JSON_EXTRACT(…, '$.a.b'))` |
728
728
  | `key?(:a)` | `jsonb_exists(…, 'a')` | `json_type(…, '$.a') IS NOT NULL` | `JSON_CONTAINS_PATH(…, 'one', '$.a')` |
729
729
  | `contains?(…)` | `@>` | — | `JSON_CONTAINS` |
@@ -755,8 +755,11 @@ than being left to the adapters, which answer it three ways: `dig_text(:n) ==
755
755
  which type was meant, and then all three agree. `dig` is refused the other way
756
756
  about — the JSON for a string carries its quotes, so `dig(:name) == 'alice'`
757
757
  is false, an error and true — and `dig_text` is the one that gives the value.
758
- A column, a function or another dug value on the right goes through untouched;
759
- only a Ruby literal is refused.
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.
760
763
 
761
764
  `bury` sets what `dig` reads: the last argument is the value and the rest are
762
765
  the path to it. The document comes back changed rather than being written
@@ -768,12 +771,14 @@ Post.update_all { { meta: :meta.bury(:author, :name, 'alice') } }
768
771
  # ... JSON_SET("meta", '$.author.name', 'alice') elsewhere
769
772
 
770
773
  Post.update_all { { meta: :meta.bury(:tags, ['ruby', 'sql']) } }
771
- Post.update_all { { meta: :meta.bury(:copy, :meta.dig_text(:n)) } }
774
+ Post.update_all { { meta: :meta.bury(:copy, :meta.dig(:n)) } }
772
775
  ```
773
776
 
774
777
  A whole document goes in as one — an object or an array rather than the string
775
- that spells it — which each adapter takes its own way round. `bury` is not a
776
- 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
777
782
  SQL has no one name to borrow here, since PostgreSQL says `jsonb_set` where
778
783
  the others say `JSON_SET`.
779
784
 
@@ -796,6 +801,13 @@ keys, an element by index — and an array literal written without a type is
796
801
  read as the first of them, so `"meta" - '{draft}'` takes out the key spelled
797
802
  `{draft}`, which is nothing, and says nothing about it.
798
803
 
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
+
799
811
  What `dig` gives is a document, so the JSON operations read it — the same
800
812
  question asked of a part of the document rather than of all of it:
801
813
 
@@ -818,8 +830,8 @@ function for text at all.
818
830
 
819
831
  `contains?` has no equivalent on SQLite and raises `NotImplementedError`
820
832
  there — later than the rest, since the adapter is only known when the SQL is
821
- built. On PostgreSQL, `contains?` and `key?` want a `jsonb` column; the
822
- `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.
823
835
 
824
836
  A key that is not a plain name travels as itself rather than being refused:
825
837
  `dig(:'odd key')` becomes `'{odd key}'` or `$."odd key"`.
@@ -524,10 +524,16 @@ module ActiveRecord
524
524
  # dig is refused the other way about: the JSON for a string carries
525
525
  # its quotes, so `dig(:name) == 'alice'` is false on SQLite, an
526
526
  # error on PostgreSQL and true on MySQL. dig_text is the one that
527
- # gives the value.
527
+ # gives the value. What bury and except give back is JSON as dig's
528
+ # is, and is refused the same way.
528
529
  #
529
530
  # A string against dig_text, and anything the block itself built -- a
530
531
  # column, a function, another dug value -- go through untouched.
532
+ #
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.
531
537
  module JsonComparable
532
538
  %i[== != < <= > >=].each do |operator|
533
539
  define_method(operator) do |other|
@@ -541,6 +547,16 @@ module ActiveRecord
541
547
  def between?(min, max) = super(*check_each([min, max]))
542
548
  def not_between?(min, max) = super(*check_each([min, max]))
543
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
+
544
560
  private
545
561
 
546
562
  # nil is left to the comparison itself, which says to use null?, and
@@ -553,7 +569,7 @@ module ActiveRecord
553
569
  return if other.is_a?(::String) && !as_json
554
570
 
555
571
  raise ArgumentError, as_json ?
556
- "dig gives JSON, and comparing it with #{other.inspect} " \
572
+ "#{json_source} gives JSON, and comparing it with #{other.inspect} " \
557
573
  "means something different on every adapter; dig_text gives the value" :
558
574
  "dig_text gives text, and comparing it with #{other.inspect} means " \
559
575
  "something different on every adapter; cast it to the type meant"
@@ -567,6 +583,14 @@ module ActiveRecord
567
583
  end
568
584
  values
569
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
570
594
  end
571
595
 
572
596
  # The JSON operations read a document, and what dig gives is one:
@@ -623,6 +647,11 @@ module ActiveRecord
623
647
  end
624
648
  end
625
649
 
650
+ private
651
+
652
+ def json_source
653
+ 'dig'
654
+ end
626
655
  end
627
656
 
628
657
  # Setting a value inside a JSON document, which is what bury does to what
@@ -631,6 +660,7 @@ module ActiveRecord
631
660
  class JsonSet < Node
632
661
  include Predications
633
662
  include JsonSteps
663
+ include JsonComparable
634
664
 
635
665
  attr_reader :operand, :path, :value
636
666
 
@@ -640,6 +670,11 @@ module ActiveRecord
640
670
  @value = value
641
671
  end
642
672
 
673
+ # Always JSON, which is what the comparison guard asks.
674
+ def as_json
675
+ true
676
+ end
677
+
643
678
  def to_arel(table, model)
644
679
  document = to_arel_operand(operand, table, model)
645
680
  if AST.adapter_family(model) == :postgresql
@@ -664,21 +699,34 @@ module ActiveRecord
664
699
  Arel::Nodes.build_quoted(JSON.generate(value))
665
700
  end
666
701
 
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.
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.
670
707
  def other_value(table, model)
671
708
  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)
709
+ unless value.is_a?(::Hash) || value.is_a?(::Array) ||
710
+ value == true || value == false
711
+ return Arel::Nodes.build_quoted(value)
712
+ end
673
713
 
674
- Arel::Nodes::NamedFunction.new(
675
- 'JSON_EXTRACT',
676
- [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
677
721
  end
678
722
 
679
723
  def expression?
680
724
  value.is_a?(Node) || value.is_a?(::Symbol)
681
725
  end
726
+
727
+ def json_source
728
+ 'bury'
729
+ end
682
730
  end
683
731
 
684
732
  # Keys taken out of a JSON document. PostgreSQL subtracts them, the
@@ -686,6 +734,7 @@ module ActiveRecord
686
734
  class JsonExcept < Node
687
735
  include Predications
688
736
  include JsonSteps
737
+ include JsonComparable
689
738
 
690
739
  attr_reader :operand, :keys
691
740
 
@@ -694,10 +743,18 @@ module ActiveRecord
694
743
  @keys = check_keys(keys)
695
744
  end
696
745
 
746
+ def as_json
747
+ true
748
+ end
749
+
697
750
  def to_arel(table, model)
698
751
  document = to_arel_operand(operand, table, model)
699
- return Arel::Nodes::InfixOperation.new(:-, document, key_array) if
700
- 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
701
758
 
702
759
  Arel::Nodes::NamedFunction.new(
703
760
  'JSON_REMOVE',
@@ -728,6 +785,10 @@ module ActiveRecord
728
785
  end
729
786
  keys
730
787
  end
788
+
789
+ def json_source
790
+ 'except'
791
+ end
731
792
  end
732
793
 
733
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.8.0'
3
+ VERSION = '0.8.1'
4
4
  end
5
5
  end
@@ -1997,6 +1997,35 @@ class TestBlockSyntax < Minitest::Test
1997
1997
  assert_raises(ArgumentError) { Doc.select { :meta.dig_text(:a).except(:b) } }
1998
1998
  end
1999
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)
2027
+ end
2028
+
2000
2029
  def test_dig_text_from_a_qualified_column
2001
2030
  seed_docs
2002
2031
  assert_equal(['one'], Doc.where { :docs[:meta].dig_text(:a, :b) == 'deep' }.pluck(:name))
@@ -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_text(: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.8.0
4
+ version: 0.8.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shugo Maeda