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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA256:
3
- metadata.gz: 4d0149b8d39635ed0f27c39362d1c4aec0dc5f2ccc1290580ed67edf18414e5b
4
- data.tar.gz: a623594513754521196f1a98f5d2f682650a3ec1b02b02b4dfa703600039faa1
2
+ SHA1:
3
+ metadata.gz: 9cef06edb829e0b79b74be5d6f69b329f1cc6656
4
+ data.tar.gz: 32535df41b6b08a634d818308c05424509683b58
5
5
  SHA512:
6
- metadata.gz: dfc8fdb19e1d61171a4c91e893bfc6b6b91d559d8402f3a47dbede8d09dcb2f702f697cbd9e8b6db688e896d1831f837ace44d8c78b8c0e110d767ac0f99db05
7
- data.tar.gz: 21ffbda2c7831051ac8ffba6a5c84d029735ce953e3fc969ae252945b0417ea28bc05f865e0e4e29771b542ca2993ec3720389a7be012b96b378536fb0743602
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`
@@ -201,7 +201,7 @@ class Predicate
201
201
  end
202
202
  end
203
203
 
204
- protected
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)
@@ -84,6 +84,8 @@ rules:
84
84
  - "::Predicate::Placeholder"
85
85
  opaque:
86
86
  - "::Object"
87
+ handler:
88
+ - "::Object"
87
89
  options:
88
90
  - "::Hash"
89
91
  name:
@@ -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,2 @@
1
+ require_relative 'ext/factory'
2
+ require_relative 'ext/to_sequel'
@@ -0,0 +1,10 @@
1
+ class Predicate
2
+ module Postgres
3
+ module PgArray
4
+ module Empty
5
+ include Predicate::UnaryFunc
6
+
7
+ end # module Empty
8
+ end # module PgArray
9
+ end # module Postgres
10
+ end # class Predicate
@@ -0,0 +1,10 @@
1
+ class Predicate
2
+ module Postgres
3
+ module PgArray
4
+ module Literal
5
+ include Predicate::Literal
6
+
7
+ end # module Literal
8
+ end # module PgArray
9
+ end # module Postgres
10
+ end # class Predicate
@@ -0,0 +1,10 @@
1
+ class Predicate
2
+ module Postgres
3
+ module PgArray
4
+ module Overlaps
5
+ include Predicate::BinaryFunc
6
+
7
+ end # module Overlaps
8
+ end # module PgArray
9
+ end # module Postgres
10
+ end # class Predicate
@@ -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
@@ -0,0 +1,4 @@
1
+ require_relative 'sequel'
2
+ require_relative 'postgres/pg_array'
3
+ require_relative 'postgres/rewriter'
4
+ require_relative 'postgres/ext'
@@ -106,6 +106,7 @@ class Predicate
106
106
 
107
107
  def on_opaque(sexpr)
108
108
  return [sexpr.last] if sexpr.last.respond_to?(:sql_literal)
109
+ return [sexpr.last] if sexpr.last.respond_to?(:sql)
109
110
  raise Error, "Unable to compile #{sexpr} to Sequel"
110
111
  end
111
112
 
@@ -1,7 +1,7 @@
1
1
  class Predicate
2
2
  module Version
3
3
  MAJOR = 2
4
- MINOR = 6
4
+ MINOR = 7
5
5
  TINY = 0
6
6
  end
7
7
  VERSION = "#{Version::MAJOR}.#{Version::MINOR}.#{Version::TINY}"
data/lib/predicate.rb CHANGED
@@ -51,8 +51,12 @@ class Predicate
51
51
 
52
52
  private
53
53
 
54
- def _factor_predicate(arg)
55
- Predicate.new Grammar.sexpr(arg)
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.create_table :items do
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
@@ -4,6 +4,10 @@ shared_examples_for "a predicate" do
4
4
  let(:x){ 12 }
5
5
  let(:y){ 13 }
6
6
 
7
+ it 'is a Predicate' do
8
+ subject.should be_a(Predicate)
9
+ end
10
+
7
11
  it 'can be negated easily' do
8
12
  (!subject).should be_a(Predicate)
9
13
  end
data/spec/spec_helper.rb CHANGED
@@ -1,10 +1,26 @@
1
1
  $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2
- require "rspec"
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.6.0
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: 2021-12-11 00:00:00.000000000 Z
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
- rubygems_version: 3.1.4
241
- signing_key:
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