predicate 2.6.0 → 2.7.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 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