predicate 1.0.0 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/predicate.rb +1 -1
- data/lib/predicate/factory.rb +19 -26
- data/lib/predicate/sequel.rb +16 -0
- data/lib/predicate/sequel/to_sequel.rb +69 -0
- data/lib/predicate/version.rb +1 -1
- data/spec/factory/test_factor_predicate.rb +1 -1
- data/spec/factory/test_from_hash.rb +48 -0
- data/spec/predicate/test_coerce.rb +9 -1
- data/spec/sequel/test_to_sequel.rb +121 -0
- data/tasks/gem.rake +39 -0
- metadata +34 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 359d07688b84bc53009ffef594bbb47724840af8
|
4
|
+
data.tar.gz: 263848ef20f69b555afa46f851f9ceec8673c72b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9b5fe1cc99457949357f9af96860d6686426efdfdd7d7ec8e71ea2011c896bab05e6660bf8b417f5e58882f4221f955c3d52edd600e90e1ac046bd9ad4aea9df
|
7
|
+
data.tar.gz: 98255f1cdc9c13de67466d8028a030d650e5aa66c4c7a6e14bb34016035669e1cdf90302b41623791335c4c73dc35d9b7215eb66168ee8d0ed6bdc567358f954
|
data/lib/predicate.rb
CHANGED
@@ -25,7 +25,7 @@ class Predicate
|
|
25
25
|
when FalseClass then contradiction
|
26
26
|
when Symbol then identifier(arg)
|
27
27
|
when Proc then native(arg)
|
28
|
-
when Hash then
|
28
|
+
when Hash then from_hash(arg)
|
29
29
|
else
|
30
30
|
raise ArgumentError, "Unable to coerce `#{arg}` to a predicate"
|
31
31
|
end
|
data/lib/predicate/factory.rb
CHANGED
@@ -29,21 +29,6 @@ class Predicate
|
|
29
29
|
_factor_predicate([:not, sexpr(operand)])
|
30
30
|
end
|
31
31
|
|
32
|
-
def relation(r)
|
33
|
-
tuples = r.to_a
|
34
|
-
case tuples.size
|
35
|
-
when 0 then contradiction
|
36
|
-
when 1 then eq(tuples.first)
|
37
|
-
else
|
38
|
-
if tuples.first.size==1
|
39
|
-
k = tuples.first.keys.first
|
40
|
-
self.in(k, tuples.map{|t| t[k]})
|
41
|
-
else
|
42
|
-
tuples.inject(contradiction){|p,t| p | eq(t) }
|
43
|
-
end
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
32
|
def in(identifier, values)
|
48
33
|
identifier = sexpr(identifier) if identifier.is_a?(Symbol)
|
49
34
|
_factor_predicate([:in, identifier, values])
|
@@ -51,17 +36,7 @@ class Predicate
|
|
51
36
|
alias :among :in
|
52
37
|
|
53
38
|
def comp(op, h)
|
54
|
-
h
|
55
|
-
if h.empty?
|
56
|
-
return tautology
|
57
|
-
elsif h.size==1
|
58
|
-
_factor_predicate [op, sexpr(h.keys.first), sexpr(h.values.last)]
|
59
|
-
else
|
60
|
-
terms = h.to_a.inject([:and]) do |anded,pair|
|
61
|
-
anded << ([op] << sexpr(pair.first) << sexpr(pair.last))
|
62
|
-
end
|
63
|
-
_factor_predicate terms
|
64
|
-
end
|
39
|
+
from_hash(h, op)
|
65
40
|
end
|
66
41
|
|
67
42
|
[ :eq, :neq, :lt, :lte, :gt, :gte ].each do |m|
|
@@ -76,6 +51,22 @@ class Predicate
|
|
76
51
|
[:lte, sexpr(middle), sexpr(upper_bound)]]
|
77
52
|
end
|
78
53
|
|
54
|
+
def from_hash(h, op = :eq)
|
55
|
+
if h.empty?
|
56
|
+
tautology
|
57
|
+
else
|
58
|
+
terms = h.to_a.map{|(k,v)|
|
59
|
+
if v.is_a?(Array)
|
60
|
+
[:in, sexpr(k), v]
|
61
|
+
else
|
62
|
+
[op, sexpr(k), sexpr(v)]
|
63
|
+
end
|
64
|
+
}
|
65
|
+
terms = terms.size == 1 ? terms.first : terms.unshift(:and)
|
66
|
+
_factor_predicate terms
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
79
70
|
def literal(literal)
|
80
71
|
_factor_predicate([:literal, literal])
|
81
72
|
end
|
@@ -84,6 +75,8 @@ class Predicate
|
|
84
75
|
_factor_predicate([:native, arg])
|
85
76
|
end
|
86
77
|
|
78
|
+
protected
|
79
|
+
|
87
80
|
def sexpr(expr)
|
88
81
|
case expr
|
89
82
|
when Expr then expr
|
@@ -0,0 +1,69 @@
|
|
1
|
+
class Predicate
|
2
|
+
class ToSequel < Sexpr::Processor
|
3
|
+
|
4
|
+
def on_identifier(sexpr)
|
5
|
+
::Sequel.identifier(sexpr.last)
|
6
|
+
end
|
7
|
+
|
8
|
+
def on_qualified_identifier(sexpr)
|
9
|
+
::Sequel.as(sexpr.qualifier, sexpr.name)
|
10
|
+
end
|
11
|
+
|
12
|
+
def on_literal(sexpr)
|
13
|
+
sexpr.last.nil? ? nil : ::Sequel.expr(sexpr.last)
|
14
|
+
end
|
15
|
+
|
16
|
+
###
|
17
|
+
|
18
|
+
def on_tautology(sexpr)
|
19
|
+
::Sequel::SQL::BooleanConstant.new(true)
|
20
|
+
end
|
21
|
+
|
22
|
+
def on_contradiction(sexpr)
|
23
|
+
::Sequel::SQL::BooleanConstant.new(false)
|
24
|
+
end
|
25
|
+
|
26
|
+
def on_eq(sexpr)
|
27
|
+
left, right = apply(sexpr.left), apply(sexpr.right)
|
28
|
+
::Sequel.expr(left => right)
|
29
|
+
end
|
30
|
+
|
31
|
+
def on_neq(sexpr)
|
32
|
+
left, right = apply(sexpr.left), apply(sexpr.right)
|
33
|
+
~::Sequel.expr(left => right)
|
34
|
+
end
|
35
|
+
|
36
|
+
def on_dyadic_comp(sexpr)
|
37
|
+
left, right = apply(sexpr.left), apply(sexpr.right)
|
38
|
+
left.send(sexpr.operator_symbol, right)
|
39
|
+
end
|
40
|
+
alias :on_lt :on_dyadic_comp
|
41
|
+
alias :on_lte :on_dyadic_comp
|
42
|
+
alias :on_gt :on_dyadic_comp
|
43
|
+
alias :on_gte :on_dyadic_comp
|
44
|
+
|
45
|
+
def on_in(sexpr)
|
46
|
+
left, right = apply(sexpr.identifier), sexpr.last
|
47
|
+
::Sequel.expr(left => right)
|
48
|
+
end
|
49
|
+
|
50
|
+
def on_not(sexpr)
|
51
|
+
~apply(sexpr.last)
|
52
|
+
end
|
53
|
+
|
54
|
+
def on_and(sexpr)
|
55
|
+
body = sexpr.sexpr_body
|
56
|
+
body[1..-1].inject(apply(body.first)){|f,t| f & apply(t) }
|
57
|
+
end
|
58
|
+
|
59
|
+
def on_or(sexpr)
|
60
|
+
body = sexpr.sexpr_body
|
61
|
+
body[1..-1].inject(apply(body.first)){|f,t| f | apply(t) }
|
62
|
+
end
|
63
|
+
|
64
|
+
def on_native(sexpr)
|
65
|
+
raise NotImplementedError
|
66
|
+
end
|
67
|
+
|
68
|
+
end # class ToSequel
|
69
|
+
end # class Predicate
|
data/lib/predicate/version.rb
CHANGED
@@ -0,0 +1,48 @@
|
|
1
|
+
require_relative 'shared/a_predicate_ast_node'
|
2
|
+
class Predicate
|
3
|
+
describe Factory, 'from_hash' do
|
4
|
+
|
5
|
+
subject{ Factory.from_hash(h) }
|
6
|
+
|
7
|
+
context "when the hash is empty" do
|
8
|
+
let(:h){ {} }
|
9
|
+
|
10
|
+
it{ should eq(Factory.tautology) }
|
11
|
+
end
|
12
|
+
|
13
|
+
context "when the hash is a singelton" do
|
14
|
+
let(:h){ {:x => 12} }
|
15
|
+
|
16
|
+
it_should_behave_like "a predicate AST node"
|
17
|
+
it{ should be_a(Eq) }
|
18
|
+
it{ should eq([:eq, [:identifier, :x], [:literal, 12]]) }
|
19
|
+
end
|
20
|
+
|
21
|
+
context "when the hash is not a singleton" do
|
22
|
+
let(:h){ {:x => 12, :y => :z} }
|
23
|
+
let(:expected){
|
24
|
+
[:and,
|
25
|
+
[:eq, [:identifier, :x], [:literal, 12]],
|
26
|
+
[:eq, [:identifier, :y], [:identifier, :z]]]
|
27
|
+
}
|
28
|
+
|
29
|
+
it_should_behave_like "a predicate AST node"
|
30
|
+
it{ should be_a(And) }
|
31
|
+
it{ should eq(expected) }
|
32
|
+
end
|
33
|
+
|
34
|
+
context "when the hash has array values" do
|
35
|
+
let(:h){ {:x => [12], :y => :z} }
|
36
|
+
let(:expected){
|
37
|
+
[:and,
|
38
|
+
[:in, [:identifier, :x], [12]],
|
39
|
+
[:eq, [:identifier, :y], [:identifier, :z]]]
|
40
|
+
}
|
41
|
+
|
42
|
+
it_should_behave_like "a predicate AST node"
|
43
|
+
it{ should be_a(And) }
|
44
|
+
it{ should eq(expected) }
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
@@ -67,7 +67,15 @@ class Predicate
|
|
67
67
|
let(:arg){ {status: 10, name: "Jones"} }
|
68
68
|
|
69
69
|
specify{
|
70
|
-
subject.
|
70
|
+
expect(subject).to eq(Predicate.eq(status: 10) & Predicate.eq(name: "Jones"))
|
71
|
+
}
|
72
|
+
end
|
73
|
+
|
74
|
+
describe "from Hash (in)" do
|
75
|
+
let(:arg){ {status: [10, 15]} }
|
76
|
+
|
77
|
+
specify{
|
78
|
+
expect(subject).to eq(Predicate.in(:status, [10,15]))
|
71
79
|
}
|
72
80
|
end
|
73
81
|
|
@@ -0,0 +1,121 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'sequel'
|
3
|
+
require 'sqlite3'
|
4
|
+
require 'predicate/sequel'
|
5
|
+
class Predicate
|
6
|
+
describe ToSequel do
|
7
|
+
|
8
|
+
DB = Sequel.sqlite
|
9
|
+
|
10
|
+
before(:all) do
|
11
|
+
DB.create_table :items do
|
12
|
+
primary_key :id
|
13
|
+
String :name
|
14
|
+
String :address
|
15
|
+
Float :price
|
16
|
+
end
|
17
|
+
items = DB[:items]
|
18
|
+
end
|
19
|
+
|
20
|
+
subject do
|
21
|
+
DB[:items].where(predicate.to_sequel).sql
|
22
|
+
end
|
23
|
+
|
24
|
+
context 'tautology' do
|
25
|
+
let(:predicate) { Predicate.tautology }
|
26
|
+
|
27
|
+
it 'works as expected' do
|
28
|
+
expect(subject).to eql("SELECT * FROM `items` WHERE (1 = 1)")
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
context 'contradiction' do
|
33
|
+
let(:predicate) { Predicate.contradiction }
|
34
|
+
|
35
|
+
it 'works as expected' do
|
36
|
+
expect(subject).to eql("SELECT * FROM `items` WHERE (1 = 0)")
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
context 'eq(name: "bob")' do
|
41
|
+
let(:predicate) { Predicate.eq(:name, "Bob") }
|
42
|
+
|
43
|
+
it 'works as expected' do
|
44
|
+
expect(subject).to eql("SELECT * FROM `items` WHERE (`name` = 'Bob')")
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
context 'eq(name: :address)' do
|
49
|
+
let(:predicate) { Predicate.eq(:name, :address) }
|
50
|
+
|
51
|
+
it 'works as expected' do
|
52
|
+
expect(subject).to eql("SELECT * FROM `items` WHERE (`name` = `address`)")
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
context 'neq(name: "bob")' do
|
57
|
+
let(:predicate) { Predicate.neq(:name, "Bob") }
|
58
|
+
|
59
|
+
it 'works as expected' do
|
60
|
+
expect(subject).to eql("SELECT * FROM `items` WHERE (`name` != 'Bob')")
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
context 'on_dyadic_comp' do
|
65
|
+
let(:predicate) { Predicate.lt(:price, 10.0) }
|
66
|
+
|
67
|
+
it 'works as expected' do
|
68
|
+
expect(subject).to eql("SELECT * FROM `items` WHERE (`price` < 10.0)")
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
context 'in' do
|
73
|
+
let(:predicate) { Predicate.in(:price, [10.0, 17.99]) }
|
74
|
+
|
75
|
+
it 'works as expected' do
|
76
|
+
expect(subject).to eql("SELECT * FROM `items` WHERE (`price` IN (10.0, 17.99))")
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
context 'not' do
|
81
|
+
let(:predicate) { !Predicate.in(:price, [10.0, 17.99]) }
|
82
|
+
|
83
|
+
it 'works as expected' do
|
84
|
+
expect(subject).to eql("SELECT * FROM `items` WHERE (`price` NOT IN (10.0, 17.99))")
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
context 'and' do
|
89
|
+
let(:predicate) { Predicate.eq(:name, "Bob") & Predicate.lt(:price, 10.0) }
|
90
|
+
|
91
|
+
it 'works as expected' do
|
92
|
+
expect(subject).to eql("SELECT * FROM `items` WHERE ((`name` = 'Bob') AND (`price` < 10.0))")
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
context 'or' do
|
97
|
+
let(:predicate) { Predicate.eq(:name, "Bob") | Predicate.lt(:price, 10.0) }
|
98
|
+
|
99
|
+
it 'works as expected' do
|
100
|
+
expect(subject).to eql("SELECT * FROM `items` WHERE ((`name` = 'Bob') OR (`price` < 10.0))")
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
context 'from_hash' do
|
105
|
+
let(:predicate) { Predicate.from_hash(name: "Bob", price: [10.0,17.99]) }
|
106
|
+
|
107
|
+
it 'works as expected' do
|
108
|
+
expect(subject).to eql("SELECT * FROM `items` WHERE ((`name` = 'Bob') AND (`price` IN (10.0, 17.99)))")
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
context 'native' do
|
113
|
+
let(:predicate) { Predicate.native(->(t){ false }) }
|
114
|
+
|
115
|
+
it 'raises an error' do
|
116
|
+
expect { subject }.to raise_error(NotImplementedError)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
121
|
+
end
|
data/tasks/gem.rake
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'rubygems/package_task'
|
2
|
+
|
3
|
+
# Dynamically load the gem spec
|
4
|
+
gemspec_file = File.expand_path('../../predicate.gemspec', __FILE__)
|
5
|
+
gemspec = Kernel.eval(File.read(gemspec_file))
|
6
|
+
|
7
|
+
Gem::PackageTask.new(gemspec) do |t|
|
8
|
+
|
9
|
+
# Name of the package
|
10
|
+
t.name = gemspec.name
|
11
|
+
|
12
|
+
# Version of the package
|
13
|
+
t.version = gemspec.version
|
14
|
+
|
15
|
+
# Directory used to store the package files
|
16
|
+
t.package_dir = "pkg"
|
17
|
+
|
18
|
+
# True if a gzipped tar file (tgz) should be produced
|
19
|
+
t.need_tar = false
|
20
|
+
|
21
|
+
# True if a gzipped tar file (tar.gz) should be produced
|
22
|
+
t.need_tar_gz = false
|
23
|
+
|
24
|
+
# True if a bzip2'd tar file (tar.bz2) should be produced
|
25
|
+
t.need_tar_bz2 = false
|
26
|
+
|
27
|
+
# True if a zip file should be produced (default is false)
|
28
|
+
t.need_zip = false
|
29
|
+
|
30
|
+
# List of files to be included in the package.
|
31
|
+
t.package_files = gemspec.files
|
32
|
+
|
33
|
+
# Tar command for gzipped or bzip2ed archives.
|
34
|
+
t.tar_command = "tar"
|
35
|
+
|
36
|
+
# Zip command for zipped archives.
|
37
|
+
t.zip_command = "zip"
|
38
|
+
|
39
|
+
end
|
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.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Bernard Lambeau
|
@@ -66,6 +66,34 @@ dependencies:
|
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '3.6'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: sequel
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: sqlite3
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
69
97
|
description: Predicate provides a simple class and processors to encode and manipulate
|
70
98
|
(tuple) predicates
|
71
99
|
email: blambeau@gmail.com
|
@@ -103,6 +131,8 @@ files:
|
|
103
131
|
- lib/predicate/processors/qualifier.rb
|
104
132
|
- lib/predicate/processors/renamer.rb
|
105
133
|
- lib/predicate/processors/to_ruby_code.rb
|
134
|
+
- lib/predicate/sequel.rb
|
135
|
+
- lib/predicate/sequel/to_sequel.rb
|
106
136
|
- lib/predicate/version.rb
|
107
137
|
- spec/expr/test_to_proc.rb
|
108
138
|
- spec/expr/test_to_ruby_code.rb
|
@@ -114,6 +144,7 @@ files:
|
|
114
144
|
- spec/factory/test_contradiction.rb
|
115
145
|
- spec/factory/test_eq.rb
|
116
146
|
- spec/factory/test_factor_predicate.rb
|
147
|
+
- spec/factory/test_from_hash.rb
|
117
148
|
- spec/factory/test_gt.rb
|
118
149
|
- spec/factory/test_gte.rb
|
119
150
|
- spec/factory/test_identifier.rb
|
@@ -155,8 +186,10 @@ files:
|
|
155
186
|
- spec/predicate/test_tautology.rb
|
156
187
|
- spec/predicate/test_to_proc.rb
|
157
188
|
- spec/predicate/test_to_ruby_code.rb
|
189
|
+
- spec/sequel/test_to_sequel.rb
|
158
190
|
- spec/spec_helper.rb
|
159
191
|
- spec/test_predicate.rb
|
192
|
+
- tasks/gem.rake
|
160
193
|
- tasks/test.rake
|
161
194
|
homepage: http://github.com/enspirit/predicate
|
162
195
|
licenses:
|