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 +4 -4
- data/README.md +20 -8
- data/lib/active_record/refined/ast.rb +72 -11
- data/lib/activerecord-refined/version.rb +1 -1
- data/test/test_block_syntax.rb +52 -2
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3ea652837dabe0136a80b1603e2efd7a86a5ca18f966a39a112c10bb05b8f7e4
|
|
4
|
+
data.tar.gz: 791a680c4ed2d87ed61581c8c232b792ab493a9ef5bd30ddc0d1d0ca9c437ec7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
759
|
-
|
|
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.
|
|
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
|
|
776
|
-
|
|
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, `
|
|
822
|
-
`
|
|
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
|
-
"
|
|
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
|
|
668
|
-
#
|
|
669
|
-
#
|
|
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
|
-
|
|
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
|
|
675
|
-
|
|
676
|
-
|
|
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
|
-
|
|
700
|
-
|
|
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.
|
data/test/test_block_syntax.rb
CHANGED
|
@@ -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(
|
|
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') } })
|