predicate 1.1.3 → 1.2.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.
- checksums.yaml +4 -4
- data/lib/predicate/factory.rb +5 -0
- data/lib/predicate/grammar.rb +1 -0
- data/lib/predicate/grammar.sexp.yml +3 -0
- data/lib/predicate/nodes/and.rb +10 -0
- data/lib/predicate/nodes/contradiction.rb +4 -0
- data/lib/predicate/nodes/expr.rb +12 -0
- data/lib/predicate/nodes/intersect.rb +26 -0
- data/lib/predicate/nodes/native.rb +8 -0
- data/lib/predicate/nodes/tautology.rb +4 -0
- data/lib/predicate/processors/qualifier.rb +7 -2
- data/lib/predicate/processors/to_ruby_code.rb +4 -0
- data/lib/predicate/sequel/to_sequel.rb +5 -2
- data/lib/predicate/version.rb +2 -2
- data/lib/predicate.rb +14 -0
- data/spec/factory/test_intersect.rb +12 -0
- data/spec/grammar/test_sexpr.rb +17 -0
- data/spec/nodes/and/test_attr_split.rb +50 -0
- data/spec/predicate/test_attr_split.rb +39 -0
- data/spec/predicate/test_factory_methods.rb +6 -0
- data/spec/predicate/test_qualify.rb +29 -15
- data/spec/sequel/test_to_sequel.rb +31 -0
- data/spec/test_predicate.rb +9 -2
- metadata +5 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 710a45c31d1350beeded48ea522d3e868925e600
|
4
|
+
data.tar.gz: 8e9acddd67b337218c749aa6744940b26ca8568e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e3d92a4dfa7227cbac8dc6b9dc9e4856861983103e2840ad041721269a66ecce74980750293178b3c3cdc9f34c06aa2d5a396880634f47e84a0b03aaac3a202d
|
7
|
+
data.tar.gz: ff1eee7dcc029585926d897eb5f3da17b68dd6f392f6e2c73bb50208ae00d22aedf0422feccea4cf747d869555b37dfa720407aa73746dd74935cfd52384f8de
|
data/lib/predicate/factory.rb
CHANGED
@@ -35,6 +35,11 @@ class Predicate
|
|
35
35
|
end
|
36
36
|
alias :among :in
|
37
37
|
|
38
|
+
def intersect(identifier, values)
|
39
|
+
identifier = sexpr(identifier) if identifier.is_a?(Symbol)
|
40
|
+
_factor_predicate([:intersect, identifier, values])
|
41
|
+
end
|
42
|
+
|
38
43
|
def comp(op, h)
|
39
44
|
from_hash(h, op)
|
40
45
|
end
|
data/lib/predicate/grammar.rb
CHANGED
data/lib/predicate/nodes/and.rb
CHANGED
@@ -18,6 +18,16 @@ class Predicate
|
|
18
18
|
end
|
19
19
|
end
|
20
20
|
|
21
|
+
def attr_split
|
22
|
+
# Similar to and_split above, but the reasonning applies on
|
23
|
+
# attribute names this time.
|
24
|
+
sexpr_body.each_with_object({}) do |term, h|
|
25
|
+
term.attr_split.each_pair do |a,p|
|
26
|
+
h[a] = (h[a] || tautology) & p
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
21
31
|
def constant_variables
|
22
32
|
sexpr_body.inject([]) do |cvars,expr|
|
23
33
|
cvars | expr.constant_variables
|
data/lib/predicate/nodes/expr.rb
CHANGED
@@ -40,6 +40,18 @@ class Predicate
|
|
40
40
|
(free_variables & attr_list).empty? ? [ tautology, self ] : [ self, tautology ]
|
41
41
|
end
|
42
42
|
|
43
|
+
def attr_split
|
44
|
+
# if I have only one variable reference, then I can return
|
45
|
+
# myself mapped to that variable...
|
46
|
+
if (vars = free_variables).size == 1
|
47
|
+
{ vars.first => self }
|
48
|
+
else
|
49
|
+
# I must still map myself to nil to meet the conjunction
|
50
|
+
# specification
|
51
|
+
{ nil => self }
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
43
55
|
def rename(renaming)
|
44
56
|
Renamer.call(self, :renaming => renaming)
|
45
57
|
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
class Predicate
|
2
|
+
module Intersect
|
3
|
+
include Expr
|
4
|
+
|
5
|
+
def priority
|
6
|
+
80
|
7
|
+
end
|
8
|
+
|
9
|
+
def identifier
|
10
|
+
self[1]
|
11
|
+
end
|
12
|
+
|
13
|
+
def values
|
14
|
+
self[2]
|
15
|
+
end
|
16
|
+
|
17
|
+
def free_variables
|
18
|
+
@free_variables ||= identifier.free_variables
|
19
|
+
end
|
20
|
+
|
21
|
+
def constant_variables
|
22
|
+
values.size == 1 ? free_variables : []
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
@@ -10,12 +10,20 @@ class Predicate
|
|
10
10
|
self[1]
|
11
11
|
end
|
12
12
|
|
13
|
+
# overriden because parent relies on free_variables,
|
14
|
+
# which raises an exception
|
13
15
|
def and_split(attr_list)
|
14
16
|
# I possibly make references to those attributes, so
|
15
17
|
# I can't be P2
|
16
18
|
[ self, tautology ]
|
17
19
|
end
|
18
20
|
|
21
|
+
# overriden because parent relies on free_variables,
|
22
|
+
# which raises an exception
|
23
|
+
def attr_split
|
24
|
+
{ nil => self }
|
25
|
+
end
|
26
|
+
|
19
27
|
def to_ruby_code(scope = 't')
|
20
28
|
if proc.respond_to?(:source_code)
|
21
29
|
code = proc.source_code
|
@@ -9,8 +9,13 @@ class Predicate
|
|
9
9
|
attr_reader :qualifier
|
10
10
|
|
11
11
|
def on_identifier(sexpr)
|
12
|
-
|
13
|
-
|
12
|
+
case qualifier
|
13
|
+
when Symbol
|
14
|
+
[:qualified_identifier, qualifier, sexpr.name]
|
15
|
+
else
|
16
|
+
return sexpr unless q = qualifier[sexpr.name]
|
17
|
+
[:qualified_identifier, q, sexpr.name]
|
18
|
+
end
|
14
19
|
end
|
15
20
|
|
16
21
|
def on_native(sexpr)
|
@@ -49,6 +49,10 @@ class Predicate
|
|
49
49
|
"#{to_ruby_literal(sexpr.values)}.include?(#{apply(sexpr.identifier)})"
|
50
50
|
end
|
51
51
|
|
52
|
+
def on_intersect(sexpr)
|
53
|
+
"!(#{apply(sexpr.identifier)} & #{to_ruby_literal(sexpr.values)}).empty?"
|
54
|
+
end
|
55
|
+
|
52
56
|
def on_literal(sexpr)
|
53
57
|
to_ruby_literal(sexpr.last)
|
54
58
|
end
|
@@ -6,7 +6,7 @@ class Predicate
|
|
6
6
|
end
|
7
7
|
|
8
8
|
def on_qualified_identifier(sexpr)
|
9
|
-
::Sequel.
|
9
|
+
::Sequel.identifier(sexpr.name).qualify(sexpr.qualifier)
|
10
10
|
end
|
11
11
|
|
12
12
|
def on_literal(sexpr)
|
@@ -44,6 +44,7 @@ class Predicate
|
|
44
44
|
|
45
45
|
def on_in(sexpr)
|
46
46
|
left, right = apply(sexpr.identifier), sexpr.last
|
47
|
+
right = [right] if !right.is_a?(Array) && right.respond_to?(:sql_literal)
|
47
48
|
::Sequel.expr(left => right)
|
48
49
|
end
|
49
50
|
|
@@ -61,9 +62,11 @@ class Predicate
|
|
61
62
|
body[1..-1].inject(apply(body.first)){|f,t| f | apply(t) }
|
62
63
|
end
|
63
64
|
|
64
|
-
def
|
65
|
+
def on_unsupported(sexpr)
|
65
66
|
raise NotSupportedError
|
66
67
|
end
|
68
|
+
alias :on_native :on_unsupported
|
69
|
+
alias :on_intersect :on_unsupported
|
67
70
|
|
68
71
|
end # class ToSequel
|
69
72
|
end # class Predicate
|
data/lib/predicate/version.rb
CHANGED
data/lib/predicate.rb
CHANGED
@@ -95,6 +95,20 @@ class Predicate
|
|
95
95
|
expr.and_split(attr_list).map{|e| Predicate.new(e)}
|
96
96
|
end
|
97
97
|
|
98
|
+
# Returns a hash `(attr -> Pattr)` associating attribute names
|
99
|
+
# to predicates, so that each predicate `Pattr` only makes
|
100
|
+
# reference to the corresponding attribute name `attr`, while
|
101
|
+
# the conjunction of `Pattr`s is still equivalent to the
|
102
|
+
# original predicate.
|
103
|
+
#
|
104
|
+
# A `nil` key may map a predicate that still makes references
|
105
|
+
# to more than one attribute.
|
106
|
+
def attr_split
|
107
|
+
expr.attr_split.each_pair.each_with_object({}) do |(k,v),h|
|
108
|
+
h[k] = Predicate.new(v)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
98
112
|
def ==(other)
|
99
113
|
other.is_a?(Predicate) && (other.expr==expr)
|
100
114
|
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require_relative "shared/a_comparison_factory_method"
|
2
|
+
class Predicate
|
3
|
+
describe Factory, 'intersect' do
|
4
|
+
|
5
|
+
subject{ Factory.intersect(:x, [2, 3]) }
|
6
|
+
|
7
|
+
it{ should be_a(Intersect) }
|
8
|
+
|
9
|
+
it{ should eq([:intersect, [:identifier, :x], [2, 3]]) }
|
10
|
+
|
11
|
+
end
|
12
|
+
end
|
data/spec/grammar/test_sexpr.rb
CHANGED
@@ -7,10 +7,15 @@ class Predicate
|
|
7
7
|
let(:contradiction){
|
8
8
|
[:contradiction, false]
|
9
9
|
}
|
10
|
+
|
10
11
|
let(:identifier){
|
11
12
|
[:identifier, :name]
|
12
13
|
}
|
13
14
|
|
15
|
+
let(:values){
|
16
|
+
[12, 15]
|
17
|
+
}
|
18
|
+
|
14
19
|
before do
|
15
20
|
subject.should be_a(Sexpr)
|
16
21
|
end
|
@@ -87,6 +92,18 @@ class Predicate
|
|
87
92
|
it{ should be_a(Lte) }
|
88
93
|
end
|
89
94
|
|
95
|
+
describe "in" do
|
96
|
+
let(:expr){ [:in, identifier, values] }
|
97
|
+
|
98
|
+
it{ should be_a(In) }
|
99
|
+
end
|
100
|
+
|
101
|
+
describe "intersect" do
|
102
|
+
let(:expr){ [:intersect, identifier, values] }
|
103
|
+
|
104
|
+
it{ should be_a(Intersect) }
|
105
|
+
end
|
106
|
+
|
90
107
|
describe "literal" do
|
91
108
|
let(:expr){ [:literal, 12] }
|
92
109
|
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
class Predicate
|
3
|
+
describe And, "attr_split" do
|
4
|
+
|
5
|
+
let(:tautology){ Factory.tautology }
|
6
|
+
|
7
|
+
subject{ predicate.attr_split }
|
8
|
+
|
9
|
+
context 'when each side returns a singleton hash' do
|
10
|
+
let(:predicate){ Factory.eq(:x, 2) & Factory.eq(:y, 3) }
|
11
|
+
|
12
|
+
it 'returns the hash union' do
|
13
|
+
expect(subject).to eql({
|
14
|
+
:x => Factory.eq(:x, 2),
|
15
|
+
:y => Factory.eq(:y, 3)
|
16
|
+
})
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
context 'when right and left side share some attributes' do
|
21
|
+
let(:predicate){
|
22
|
+
Factory.eq(:x, 2) & (
|
23
|
+
Factory.eq(:y, 3) & Factory.in(:x, [1, 18])
|
24
|
+
)
|
25
|
+
}
|
26
|
+
|
27
|
+
it 'returns the hash union' do
|
28
|
+
expect(subject).to eql({
|
29
|
+
:x => Factory.eq(:x, 2) & Factory.in(:x, [1, 18]),
|
30
|
+
:y => Factory.eq(:y, 3)
|
31
|
+
})
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
context 'when right and left side share some attributes, but split cannot be made' do
|
36
|
+
let(:predicate){
|
37
|
+
Factory.eq(:x, 2) & (
|
38
|
+
Factory.eq(:y, 3) | Factory.in(:x, [1, 18])
|
39
|
+
)
|
40
|
+
}
|
41
|
+
|
42
|
+
it 'returns the hash union' do
|
43
|
+
expect(subject).to eql({
|
44
|
+
:x => Factory.eq(:x, 2),
|
45
|
+
nil => Factory.eq(:y, 3) | Factory.in(:x, [1, 18])
|
46
|
+
})
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
class Predicate
|
3
|
+
describe Predicate, "attr_split" do
|
4
|
+
|
5
|
+
let(:p){ Predicate }
|
6
|
+
subject{ pred.attr_split }
|
7
|
+
|
8
|
+
context "on tautology" do
|
9
|
+
let(:pred){ p.tautology }
|
10
|
+
|
11
|
+
it{ should eq({}) }
|
12
|
+
end
|
13
|
+
|
14
|
+
context "on contradiction" do
|
15
|
+
let(:pred){ p.contradiction }
|
16
|
+
|
17
|
+
it{ should eq({ nil => pred }) }
|
18
|
+
end
|
19
|
+
|
20
|
+
context "on identifier" do
|
21
|
+
let(:pred){ p.identifier(:x) }
|
22
|
+
|
23
|
+
it{ should eq({ x: pred }) }
|
24
|
+
end
|
25
|
+
|
26
|
+
context "on not" do
|
27
|
+
let(:pred){ p.not(:x) }
|
28
|
+
|
29
|
+
it{ should eq({ x: pred }) }
|
30
|
+
end
|
31
|
+
|
32
|
+
context "on eq" do
|
33
|
+
let(:pred){ p.eq(:x, 2) }
|
34
|
+
|
35
|
+
it{ should eq({ x: pred }) }
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
@@ -6,27 +6,41 @@ class Predicate
|
|
6
6
|
|
7
7
|
subject{ predicate.qualify(qualifier) }
|
8
8
|
|
9
|
-
|
9
|
+
context 'when the qualifier is a Symbol' do
|
10
|
+
let(:qualifier) { :t }
|
11
|
+
let(:predicate){ p.in(:x, [2]) & p.eq(:y, :z) }
|
12
|
+
|
13
|
+
it 'works as expected' do
|
14
|
+
left = p.in(Factory.qualified_identifier(:t, :x), [2])
|
15
|
+
right = p.eq(Factory.qualified_identifier(:t, :y), Factory.qualified_identifier(:t, :z))
|
16
|
+
expect(subject).to eql(left & right)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
context 'when the qualifier is a Hash' do
|
10
21
|
|
11
|
-
|
12
|
-
let(:predicate){ p.in(:x, [2]) & p.eq(:y, 3) }
|
22
|
+
let(:qualifier) { {:x => :t} }
|
13
23
|
|
14
|
-
|
24
|
+
context 'on a full AST predicate' do
|
25
|
+
let(:predicate){ p.in(:x, [2]) & p.eq(:y, 3) }
|
15
26
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
27
|
+
it{ should eq(p.in(Factory.qualified_identifier(:t, :x), [2]) & p.eq(:y, 3)) }
|
28
|
+
|
29
|
+
specify "it should tag expressions correctly" do
|
30
|
+
subject.expr.should be_a(Sexpr)
|
31
|
+
subject.expr.should be_a(Expr)
|
32
|
+
subject.expr.should be_a(And)
|
33
|
+
end
|
20
34
|
end
|
21
|
-
end
|
22
35
|
|
23
|
-
|
24
|
-
|
36
|
+
context 'on a predicate that contains natives' do
|
37
|
+
let(:predicate){ p.in(:x, [2]) & p.native(lambda{}) }
|
25
38
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
39
|
+
it 'raises an error' do
|
40
|
+
lambda{
|
41
|
+
subject
|
42
|
+
}.should raise_error(NotSupportedError)
|
43
|
+
end
|
30
44
|
end
|
31
45
|
end
|
32
46
|
|
@@ -45,6 +45,14 @@ class Predicate
|
|
45
45
|
end
|
46
46
|
end
|
47
47
|
|
48
|
+
context 'qualified eq(name: "bob")' do
|
49
|
+
let(:predicate) { Predicate.eq(:name, "Bob").qualify(:t) }
|
50
|
+
|
51
|
+
it 'works as expected' do
|
52
|
+
expect(subject).to eql("SELECT * FROM `items` WHERE (`t`.`name` = 'Bob')")
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
48
56
|
context 'eq(name: :address)' do
|
49
57
|
let(:predicate) { Predicate.eq(:name, :address) }
|
50
58
|
|
@@ -77,6 +85,21 @@ class Predicate
|
|
77
85
|
end
|
78
86
|
end
|
79
87
|
|
88
|
+
context 'in with something responding to sql_literal' do
|
89
|
+
let(:operand){
|
90
|
+
Object.new.tap{|o|
|
91
|
+
def o.sql_literal(db)
|
92
|
+
"Hello World"
|
93
|
+
end
|
94
|
+
}
|
95
|
+
}
|
96
|
+
let(:predicate) { Predicate.in(:price, operand) }
|
97
|
+
|
98
|
+
it 'works as expected' do
|
99
|
+
expect(subject).to eql("SELECT * FROM `items` WHERE (`price` IN (Hello World))")
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
80
103
|
context 'not' do
|
81
104
|
let(:predicate) { !Predicate.in(:price, [10.0, 17.99]) }
|
82
105
|
|
@@ -109,6 +132,14 @@ class Predicate
|
|
109
132
|
end
|
110
133
|
end
|
111
134
|
|
135
|
+
context 'intersect' do
|
136
|
+
let(:predicate) { Predicate.intersect(:x, [8, 9]) }
|
137
|
+
|
138
|
+
it 'raises an error' do
|
139
|
+
expect { subject }.to raise_error(NotSupportedError)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
112
143
|
context 'native' do
|
113
144
|
let(:predicate) { Predicate.native(->(t){ false }) }
|
114
145
|
|
data/spec/test_predicate.rb
CHANGED
@@ -2,11 +2,12 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
shared_examples_for "a predicate" do
|
4
4
|
|
5
|
+
let(:w){ [8, 9] }
|
5
6
|
let(:x){ 12 }
|
6
7
|
let(:y){ 13 }
|
7
8
|
|
8
9
|
it 'provides a proc for easy evaluation' do
|
9
|
-
got = subject.to_proc.call(x:
|
10
|
+
got = subject.to_proc.call(w: w, x: x, y: y)
|
10
11
|
[ TrueClass, FalseClass ].should include(got.class)
|
11
12
|
end
|
12
13
|
|
@@ -24,7 +25,7 @@ shared_examples_for "a predicate" do
|
|
24
25
|
|
25
26
|
it 'has free variables' do
|
26
27
|
(fv = subject.free_variables).should be_a(Array)
|
27
|
-
(fv - [ :x, :y ]).should be_empty
|
28
|
+
(fv - [ :w, :x, :y ]).should be_empty
|
28
29
|
end
|
29
30
|
|
30
31
|
it 'always splits around and trivially when no free variables are touched' do
|
@@ -65,6 +66,12 @@ describe "Predicate.among" do
|
|
65
66
|
it_should_behave_like "a predicate"
|
66
67
|
end
|
67
68
|
|
69
|
+
describe "Predicate.intersect" do
|
70
|
+
subject{ Predicate.intersect(:w, [2, 3]) }
|
71
|
+
|
72
|
+
it_should_behave_like "a predicate"
|
73
|
+
end
|
74
|
+
|
68
75
|
describe "Predicate.eq" do
|
69
76
|
subject{ Predicate.eq(:x, 2) }
|
70
77
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: predicate
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Bernard Lambeau
|
@@ -103,6 +103,7 @@ files:
|
|
103
103
|
- lib/predicate/nodes/gte.rb
|
104
104
|
- lib/predicate/nodes/identifier.rb
|
105
105
|
- lib/predicate/nodes/in.rb
|
106
|
+
- lib/predicate/nodes/intersect.rb
|
106
107
|
- lib/predicate/nodes/literal.rb
|
107
108
|
- lib/predicate/nodes/lt.rb
|
108
109
|
- lib/predicate/nodes/lte.rb
|
@@ -135,6 +136,7 @@ files:
|
|
135
136
|
- spec/factory/test_gte.rb
|
136
137
|
- spec/factory/test_identifier.rb
|
137
138
|
- spec/factory/test_in.rb
|
139
|
+
- spec/factory/test_intersect.rb
|
138
140
|
- spec/factory/test_literal.rb
|
139
141
|
- spec/factory/test_lt.rb
|
140
142
|
- spec/factory/test_lte.rb
|
@@ -147,6 +149,7 @@ files:
|
|
147
149
|
- spec/grammar/test_match.rb
|
148
150
|
- spec/grammar/test_sexpr.rb
|
149
151
|
- spec/nodes/and/test_and_split.rb
|
152
|
+
- spec/nodes/and/test_attr_split.rb
|
150
153
|
- spec/nodes/dyadic_comp/test_and_split.rb
|
151
154
|
- spec/nodes/identifier/test_and_split.rb
|
152
155
|
- spec/nodes/identifier/test_free_variables.rb
|
@@ -158,6 +161,7 @@ files:
|
|
158
161
|
- spec/nodes/qualified_identifier/test_name.rb
|
159
162
|
- spec/nodes/qualified_identifier/test_qualifier.rb
|
160
163
|
- spec/predicate/test_and_split.rb
|
164
|
+
- spec/predicate/test_attr_split.rb
|
161
165
|
- spec/predicate/test_bool_and.rb
|
162
166
|
- spec/predicate/test_bool_not.rb
|
163
167
|
- spec/predicate/test_bool_or.rb
|