predicate 2.6.0 → 2.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/README.md +33 -1
- data/lib/predicate/factory.rb +7 -3
- data/lib/predicate/grammar.sexp.yml +2 -0
- data/lib/predicate/postgres/ext/factory.rb +28 -0
- data/lib/predicate/postgres/ext/to_sequel.rb +26 -0
- data/lib/predicate/postgres/ext.rb +2 -0
- data/lib/predicate/postgres/pg_array/empty.rb +10 -0
- data/lib/predicate/postgres/pg_array/literal.rb +10 -0
- data/lib/predicate/postgres/pg_array/overlaps.rb +10 -0
- data/lib/predicate/postgres/pg_array.rb +25 -0
- data/lib/predicate/postgres/rewriter.rb +49 -0
- data/lib/predicate/postgres.rb +4 -0
- data/lib/predicate/sequel/to_sequel.rb +1 -0
- data/lib/predicate/version.rb +1 -1
- data/lib/predicate.rb +6 -2
- data/spec/postgres/test_factory.rb +48 -0
- data/spec/postgres/test_to_postgres.rb +41 -0
- data/spec/postgres/test_to_sequel.rb +44 -0
- data/spec/sequel/test_to_sequel.rb +1 -10
- data/spec/shared/a_predicate.rb +4 -0
- data/spec/spec_helper.rb +17 -1
- metadata +33 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 9cef06edb829e0b79b74be5d6f69b329f1cc6656
|
4
|
+
data.tar.gz: 32535df41b6b08a634d818308c05424509683b58
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 650836d9f3b0534271a38b2e58f59f4f93aad9fad9b60017777a0c1ab91e373ffe458d1a77bcc2309405c267b657c89121abb401f6687d19f8f79196d209dd06
|
7
|
+
data.tar.gz: 70a98a7269cda44050632396a4c90cfafddf16a8b06b1dc76fc1962cab7b4e8520e64244dc20cbf8e2369cbf21e7733f913d7b80cffa4dda0ec26b81decaaaae
|
data/README.md
CHANGED
@@ -274,7 +274,7 @@ given list.
|
|
274
274
|
|
275
275
|
```ruby
|
276
276
|
p = Predicate.eq(x: 2, y: 4) # x = 2 & y = 4
|
277
|
-
p1, p2 = p.and_split([:x]) # p1 is x = 2 ; p2 is y = 4
|
277
|
+
p1, p2 = p.and_split([:x]) # p1 is x = 2 ; p2 is y = 4
|
278
278
|
```
|
279
279
|
|
280
280
|
Observe that `and_split` is always possible but may degenerate to an
|
@@ -304,6 +304,38 @@ split = p.attr_split
|
|
304
304
|
# }
|
305
305
|
```
|
306
306
|
|
307
|
+
## Working with PostgreSQL
|
308
|
+
|
309
|
+
(experimental) Predicate supports compiling certain high-level expressions
|
310
|
+
to PostgreSQL native operators. It works in an direct or indirect way:
|
311
|
+
|
312
|
+
```
|
313
|
+
require 'predicate'
|
314
|
+
require 'predicate/postgres'
|
315
|
+
|
316
|
+
# In direct way, you simply create the predicates using PostgreSQL's own
|
317
|
+
# operators
|
318
|
+
p = Predicate.pg_array_overlaps(:x, ['foo', 'bar'])
|
319
|
+
p.to_sequel
|
320
|
+
|
321
|
+
# In indirect way, you use high-level predicates and convert them to
|
322
|
+
# PostgreSQL later using `to_postgres`
|
323
|
+
p = Predicate.interect(:x, ['foo', 'bar'])
|
324
|
+
p = p.to_postgres
|
325
|
+
p.to_sequel
|
326
|
+
```
|
327
|
+
|
328
|
+
Only a few array operators & translations exist, and only on `varchar[]`
|
329
|
+
types. Additional support will be added later. The following
|
330
|
+
translations are implemented (and methods on the right directly available
|
331
|
+
on the `Predicate` class):
|
332
|
+
|
333
|
+
```
|
334
|
+
-> pg_array_literal
|
335
|
+
intersect -> pg_array_overlaps
|
336
|
+
empty -> pg_array_empty
|
337
|
+
```
|
338
|
+
|
307
339
|
## Working with abstract variables
|
308
340
|
|
309
341
|
WARNING: this `var` feature is only compatible with `Predicate#evaluate`
|
data/lib/predicate/factory.rb
CHANGED
@@ -201,7 +201,7 @@ class Predicate
|
|
201
201
|
end
|
202
202
|
end
|
203
203
|
|
204
|
-
|
204
|
+
public
|
205
205
|
|
206
206
|
def sexpr(expr)
|
207
207
|
case expr
|
@@ -217,8 +217,12 @@ class Predicate
|
|
217
217
|
end
|
218
218
|
end
|
219
219
|
|
220
|
-
def _factor_predicate(arg)
|
221
|
-
sexpr(arg)
|
220
|
+
def _factor_predicate(arg, *mods)
|
221
|
+
expr = sexpr(arg)
|
222
|
+
mods.each do |mod|
|
223
|
+
expr.extend(mod)
|
224
|
+
end
|
225
|
+
expr
|
222
226
|
end
|
223
227
|
|
224
228
|
extend(self)
|
@@ -0,0 +1,28 @@
|
|
1
|
+
class Predicate
|
2
|
+
module Factory
|
3
|
+
def pg_array_literal(value, type = :varchar)
|
4
|
+
_factor_predicate([
|
5
|
+
:pg_array_literal,
|
6
|
+
value,
|
7
|
+
type
|
8
|
+
], Postgres::PgArray::Literal)
|
9
|
+
end
|
10
|
+
|
11
|
+
def pg_array_overlaps(left, right, type = :varchar)
|
12
|
+
_factor_predicate([
|
13
|
+
:pg_array_overlaps,
|
14
|
+
sexpr(left),
|
15
|
+
sexpr(right),
|
16
|
+
type
|
17
|
+
], Postgres::PgArray::Overlaps)
|
18
|
+
end
|
19
|
+
|
20
|
+
def pg_array_empty(operand, type = :varchar)
|
21
|
+
_factor_predicate([
|
22
|
+
:pg_empty,
|
23
|
+
sexpr(operand),
|
24
|
+
type
|
25
|
+
], Postgres::PgArray::Empty)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
class Predicate
|
2
|
+
module Postgres
|
3
|
+
module ToSequel
|
4
|
+
def on_pg_array_literal(sexpr)
|
5
|
+
PgArray.to_pg_array(sexpr[1], sexpr[2])
|
6
|
+
end
|
7
|
+
|
8
|
+
def on_pg_array_overlaps(sexpr)
|
9
|
+
type = sexpr.last
|
10
|
+
l = PgArray.to_pg_array(apply(sexpr.left), type)
|
11
|
+
r = PgArray.to_pg_array(apply(sexpr.right), type)
|
12
|
+
l.overlaps(r)
|
13
|
+
end
|
14
|
+
|
15
|
+
def on_pg_array_empty(sexpr)
|
16
|
+
left = PgArray.to_pg_array(apply(sexpr.operand))
|
17
|
+
right = PgArray.to_pg_array([], sexpr.last)
|
18
|
+
::Sequel.expr(left => right)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class Predicate::ToSequel
|
25
|
+
include Predicate::Postgres::ToSequel
|
26
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
class Predicate
|
2
|
+
module Postgres
|
3
|
+
module PgArray
|
4
|
+
|
5
|
+
def to_pg_array(arg, type = :varchar)
|
6
|
+
if arg.is_a?(Sequel::Postgres::PGArray)
|
7
|
+
arg
|
8
|
+
elsif arg.is_a?(Sequel::SQL::Wrapper)
|
9
|
+
::Sequel.pg_array(arg.value, type)
|
10
|
+
elsif arg.is_a?(Array)
|
11
|
+
::Sequel.pg_array(arg, type)
|
12
|
+
elsif arg.respond_to?(:pg_array)
|
13
|
+
arg.pg_array
|
14
|
+
else
|
15
|
+
raise NotSupportedError, "Unexpected pg_array arg `#{arg}`::`#{arg.class}`"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
module_function :to_pg_array
|
19
|
+
|
20
|
+
end # module PgArray
|
21
|
+
end # module Postgres
|
22
|
+
end # class Predicate
|
23
|
+
require_relative 'pg_array/empty'
|
24
|
+
require_relative 'pg_array/overlaps'
|
25
|
+
require_relative 'pg_array/literal'
|
@@ -0,0 +1,49 @@
|
|
1
|
+
class Predicate
|
2
|
+
module Postgres
|
3
|
+
class Rewriter < Sexpr::Rewriter
|
4
|
+
grammar Grammar
|
5
|
+
|
6
|
+
class ToLiteral < Sexpr::Rewriter
|
7
|
+
grammar Grammar
|
8
|
+
|
9
|
+
def on_literal(sexpr)
|
10
|
+
if sexpr.last.is_a?(Array)
|
11
|
+
[ :pg_array_literal, sexpr.last, :varchar ]
|
12
|
+
else
|
13
|
+
sexpr
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
alias :on_missing :copy_and_apply
|
18
|
+
end
|
19
|
+
|
20
|
+
def on_intersect(sexpr)
|
21
|
+
rewriter = ToLiteral.new
|
22
|
+
rewritten = sexpr[1..-1]
|
23
|
+
.map{|expr| rewriter.call(expr) }
|
24
|
+
.unshift(:pg_array_overlaps)
|
25
|
+
rewritten.extend(PgArray::Overlaps)
|
26
|
+
end
|
27
|
+
|
28
|
+
def on_empty(sexpr)
|
29
|
+
rewritten = sexpr[1..-1]
|
30
|
+
.map{|expr| apply(expr) }
|
31
|
+
.unshift(:pg_array_empty)
|
32
|
+
.push(:varchar)
|
33
|
+
rewritten.extend(PgArray::Empty)
|
34
|
+
end
|
35
|
+
|
36
|
+
alias :on_missing :copy_and_apply
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
module Expr
|
41
|
+
def to_postgres(*args)
|
42
|
+
Postgres::Rewriter.new(*args).call(self)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def to_postgres(*args)
|
47
|
+
Predicate.new(expr.to_postgres(*args))
|
48
|
+
end
|
49
|
+
end
|
data/lib/predicate/version.rb
CHANGED
data/lib/predicate.rb
CHANGED
@@ -51,8 +51,12 @@ class Predicate
|
|
51
51
|
|
52
52
|
private
|
53
53
|
|
54
|
-
def _factor_predicate(arg)
|
55
|
-
|
54
|
+
def _factor_predicate(arg, *mods)
|
55
|
+
expr = Grammar.sexpr(arg)
|
56
|
+
mods.each do |mod|
|
57
|
+
expr.extend(mod)
|
58
|
+
end
|
59
|
+
Predicate.new(expr)
|
56
60
|
end
|
57
61
|
|
58
62
|
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class Predicate
|
4
|
+
describe 'pg_factory' do
|
5
|
+
|
6
|
+
subject{
|
7
|
+
predicate
|
8
|
+
}
|
9
|
+
|
10
|
+
let(:predicate) {
|
11
|
+
Predicate.tautology
|
12
|
+
}
|
13
|
+
|
14
|
+
it_should_behave_like "a predicate"
|
15
|
+
|
16
|
+
context 'pg_array_literal' do
|
17
|
+
let(:predicate) do
|
18
|
+
Predicate.pg_array_literal([12])
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
context 'pg_array_empty' do
|
23
|
+
let(:predicate) do
|
24
|
+
Predicate.pg_array_empty(:x)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
context 'pg_array_overlaps' do
|
29
|
+
let(:predicate) do
|
30
|
+
Predicate.pg_array_overlaps(:x, [1,2,3])
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'has expected free variables' do
|
34
|
+
expect(subject.free_variables).to eql([:x])
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
context 'pg_array_overlaps (2)' do
|
39
|
+
let(:predicate) do
|
40
|
+
Predicate.pg_array_overlaps(:x, :y)
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'has expected free variables' do
|
44
|
+
expect(subject.free_variables).to eql([:x, :y])
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class Predicate
|
4
|
+
describe 'to_postgres' do
|
5
|
+
subject {
|
6
|
+
predicate.to_postgres
|
7
|
+
}
|
8
|
+
|
9
|
+
context 'on an intersect' do
|
10
|
+
let(:predicate){
|
11
|
+
Predicate.intersect(:x, [1, 2, 3])
|
12
|
+
}
|
13
|
+
|
14
|
+
it 'returns another predicate' do
|
15
|
+
expect(subject).to be_a(Predicate)
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'replaces intersect by pg_array_overlaps' do
|
19
|
+
expect(subject.sexpr).to eql([
|
20
|
+
:pg_array_overlaps,
|
21
|
+
[ :identifier, :x ],
|
22
|
+
[ :pg_array_literal, [1, 2, 3], :varchar ]
|
23
|
+
])
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
context 'on an intersect' do
|
28
|
+
let(:predicate){
|
29
|
+
Predicate.empty(:x)
|
30
|
+
}
|
31
|
+
|
32
|
+
it 'replaces empty by pg_array_empty' do
|
33
|
+
expect(subject.sexpr).to eql([
|
34
|
+
:pg_array_empty,
|
35
|
+
[ :identifier, :x ],
|
36
|
+
:varchar
|
37
|
+
])
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
class Predicate
|
3
|
+
describe ToSequel do
|
4
|
+
PG_DB = Sequel.connect(ENV['PREDICATE_PG_URL'])
|
5
|
+
|
6
|
+
before(:all) do
|
7
|
+
create_items_database(PG_DB)
|
8
|
+
items = PG_DB[:items]
|
9
|
+
end
|
10
|
+
|
11
|
+
subject do
|
12
|
+
dataset = PG_DB[:items].where(predicate.to_postgres.to_sequel)
|
13
|
+
dataset
|
14
|
+
end
|
15
|
+
|
16
|
+
# after do
|
17
|
+
# subject.to_a
|
18
|
+
# end
|
19
|
+
|
20
|
+
context 'tautology' do
|
21
|
+
let(:predicate) { Predicate.tautology }
|
22
|
+
|
23
|
+
it 'works as expected' do
|
24
|
+
expect(subject.sql).to eql(%Q{SELECT * FROM "items" WHERE true})
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
context 'intersect' do
|
29
|
+
let(:predicate) { Predicate.intersect(:x, [1, 2, 3]) }
|
30
|
+
|
31
|
+
it 'works as expected' do
|
32
|
+
expect(subject.sql).to eql(%Q{SELECT * FROM "items" WHERE ("x" && ARRAY[1,2,3])})
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
context 'empty' do
|
37
|
+
let(:predicate) { Predicate.empty(:x) }
|
38
|
+
|
39
|
+
it 'works as expected' do
|
40
|
+
expect(subject.sql).to eql(%Q{SELECT * FROM "items" WHERE ("x" = '{}'::varchar[])})
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end if ENV['PREDICATE_PG_URL']
|
44
|
+
end
|
@@ -1,19 +1,11 @@
|
|
1
1
|
require 'spec_helper'
|
2
|
-
require 'sequel'
|
3
|
-
require 'sqlite3'
|
4
|
-
require 'predicate/sequel'
|
5
2
|
class Predicate
|
6
3
|
describe ToSequel do
|
7
4
|
|
8
5
|
DB = Sequel.sqlite
|
9
6
|
|
10
7
|
before(:all) do
|
11
|
-
DB
|
12
|
-
primary_key :id
|
13
|
-
String :name
|
14
|
-
String :address
|
15
|
-
Float :price
|
16
|
-
end
|
8
|
+
create_items_database(DB)
|
17
9
|
items = DB[:items]
|
18
10
|
end
|
19
11
|
|
@@ -229,6 +221,5 @@ class Predicate
|
|
229
221
|
expect { subject }.to raise_error(NotSupportedError)
|
230
222
|
end
|
231
223
|
end
|
232
|
-
|
233
224
|
end
|
234
225
|
end
|
data/spec/shared/a_predicate.rb
CHANGED
data/spec/spec_helper.rb
CHANGED
@@ -1,10 +1,26 @@
|
|
1
1
|
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
|
2
|
-
require
|
2
|
+
require 'rspec'
|
3
|
+
require 'sequel'
|
4
|
+
require 'sqlite3'
|
5
|
+
require 'pg'
|
3
6
|
require 'predicate'
|
7
|
+
require 'predicate/sequel'
|
8
|
+
require 'predicate/postgres'
|
4
9
|
require_relative 'shared/a_predicate'
|
5
10
|
|
11
|
+
Sequel.extension :pg_array, :pg_array_ops
|
6
12
|
module Helpers
|
7
13
|
|
14
|
+
def create_items_database(db)
|
15
|
+
db.drop_table? :items
|
16
|
+
db.create_table :items do
|
17
|
+
primary_key :id
|
18
|
+
String :name
|
19
|
+
String :address
|
20
|
+
Float :price
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
8
24
|
end
|
9
25
|
|
10
26
|
RSpec.configure do |c|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: predicate
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.7.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Bernard Lambeau
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-02-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: sexpr
|
@@ -80,6 +80,20 @@ dependencies:
|
|
80
80
|
- - ">="
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: pg
|
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'
|
83
97
|
description: Predicate provides a simple class and processors to encode and manipulate
|
84
98
|
(tuple) predicates
|
85
99
|
email: blambeau@gmail.com
|
@@ -129,6 +143,15 @@ files:
|
|
129
143
|
- lib/predicate/nodes/unary_func.rb
|
130
144
|
- lib/predicate/nodes/var.rb
|
131
145
|
- lib/predicate/placeholder.rb
|
146
|
+
- lib/predicate/postgres.rb
|
147
|
+
- lib/predicate/postgres/ext.rb
|
148
|
+
- lib/predicate/postgres/ext/factory.rb
|
149
|
+
- lib/predicate/postgres/ext/to_sequel.rb
|
150
|
+
- lib/predicate/postgres/pg_array.rb
|
151
|
+
- lib/predicate/postgres/pg_array/empty.rb
|
152
|
+
- lib/predicate/postgres/pg_array/literal.rb
|
153
|
+
- lib/predicate/postgres/pg_array/overlaps.rb
|
154
|
+
- lib/predicate/postgres/rewriter.rb
|
132
155
|
- lib/predicate/processors.rb
|
133
156
|
- lib/predicate/processors/binder.rb
|
134
157
|
- lib/predicate/processors/qualifier.rb
|
@@ -191,6 +214,9 @@ files:
|
|
191
214
|
- spec/nodes/qualified_identifier/test_free_variables.rb
|
192
215
|
- spec/nodes/qualified_identifier/test_name.rb
|
193
216
|
- spec/nodes/qualified_identifier/test_qualifier.rb
|
217
|
+
- spec/postgres/test_factory.rb
|
218
|
+
- spec/postgres/test_to_postgres.rb
|
219
|
+
- spec/postgres/test_to_sequel.rb
|
194
220
|
- spec/predicate/test_and_split.rb
|
195
221
|
- spec/predicate/test_attr_split.rb
|
196
222
|
- spec/predicate/test_bind.rb
|
@@ -222,7 +248,7 @@ homepage: http://github.com/enspirit/predicate
|
|
222
248
|
licenses:
|
223
249
|
- MIT
|
224
250
|
metadata: {}
|
225
|
-
post_install_message:
|
251
|
+
post_install_message:
|
226
252
|
rdoc_options: []
|
227
253
|
require_paths:
|
228
254
|
- lib
|
@@ -237,8 +263,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
237
263
|
- !ruby/object:Gem::Version
|
238
264
|
version: '0'
|
239
265
|
requirements: []
|
240
|
-
|
241
|
-
|
266
|
+
rubyforge_project:
|
267
|
+
rubygems_version: 2.6.14.4
|
268
|
+
signing_key:
|
242
269
|
specification_version: 4
|
243
270
|
summary: Predicate provides a simple class and processors to encode and manipulate
|
244
271
|
(tuple) predicates
|