predicate 2.3.3 → 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 +5 -5
- data/Gemfile +4 -0
- data/LICENSE.md +17 -19
- data/README.md +467 -0
- data/bin/g +2 -0
- data/lib/predicate/dsl.rb +138 -0
- data/lib/predicate/factory.rb +149 -40
- data/lib/predicate/grammar.rb +11 -2
- data/lib/predicate/grammar.sexp.yml +31 -0
- data/lib/predicate/nodes/${op_name}.rb.jeny +12 -0
- data/lib/predicate/nodes/and.rb +9 -0
- data/lib/predicate/nodes/binary_func.rb +20 -0
- data/lib/predicate/nodes/contradiction.rb +2 -7
- data/lib/predicate/nodes/dyadic_comp.rb +1 -3
- data/lib/predicate/nodes/empty.rb +14 -0
- data/lib/predicate/nodes/eq.rb +11 -3
- data/lib/predicate/nodes/expr.rb +5 -3
- data/lib/predicate/nodes/has_size.rb +14 -0
- data/lib/predicate/nodes/identifier.rb +1 -3
- data/lib/predicate/nodes/in.rb +7 -6
- data/lib/predicate/nodes/intersect.rb +3 -23
- data/lib/predicate/nodes/literal.rb +1 -3
- data/lib/predicate/nodes/match.rb +1 -21
- data/lib/predicate/nodes/nadic_bool.rb +1 -3
- data/lib/predicate/nodes/native.rb +1 -3
- data/lib/predicate/nodes/not.rb +1 -3
- data/lib/predicate/nodes/opaque.rb +1 -3
- data/lib/predicate/nodes/qualified_identifier.rb +1 -3
- data/lib/predicate/nodes/set_op.rb +26 -0
- data/lib/predicate/nodes/subset.rb +11 -0
- data/lib/predicate/nodes/superset.rb +11 -0
- data/lib/predicate/nodes/tautology.rb +6 -7
- data/lib/predicate/nodes/unary_func.rb +16 -0
- data/lib/predicate/nodes/var.rb +46 -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/processors/qualifier.rb +4 -0
- data/lib/predicate/processors/renamer.rb +4 -0
- data/lib/predicate/processors/to_s.rb +28 -0
- data/lib/predicate/processors/unqualifier.rb +4 -0
- data/lib/predicate/sequel/to_sequel.rb +5 -1
- data/lib/predicate/sugar.rb +47 -0
- data/lib/predicate/version.rb +2 -2
- data/lib/predicate.rb +28 -4
- data/spec/dsl/test_dsl.rb +204 -0
- data/spec/dsl/test_evaluate.rb +65 -0
- data/spec/dsl/test_respond_to_missing.rb +35 -0
- data/spec/dsl/test_to_skake_case.rb +38 -0
- data/spec/factory/shared/a_comparison_factory_method.rb +1 -0
- data/spec/factory/test_${op_name}.rb.jeny +12 -0
- data/spec/factory/test_comp.rb +28 -5
- data/spec/factory/test_empty.rb +11 -0
- data/spec/factory/test_has_size.rb +11 -0
- data/spec/factory/test_match.rb +1 -0
- data/spec/factory/test_set_ops.rb +18 -0
- data/spec/factory/test_var.rb +22 -0
- data/spec/factory/test_vars.rb +27 -0
- data/spec/nodes/${op_name}.jeny/test_evaluate.rb.jeny +19 -0
- data/spec/nodes/empty/test_evaluate.rb +42 -0
- data/spec/nodes/has_size/test_evaluate.rb +44 -0
- 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/predicate/test_and_split.rb +18 -0
- data/spec/predicate/test_attr_split.rb +18 -0
- data/spec/predicate/test_constant_variables.rb +24 -2
- data/spec/predicate/test_constants.rb +24 -0
- data/spec/predicate/test_evaluate.rb +205 -3
- data/spec/predicate/test_free_variables.rb +1 -1
- data/spec/predicate/test_to_hash.rb +40 -0
- data/spec/predicate/test_to_s.rb +37 -0
- data/spec/sequel/test_to_sequel.rb +26 -10
- data/spec/shared/a_predicate.rb +34 -0
- data/spec/spec_helper.rb +18 -1
- data/spec/test_predicate.rb +78 -33
- data/spec/test_readme.rb +80 -0
- data/spec/test_sugar.rb +48 -0
- data/tasks/test.rake +3 -3
- metadata +67 -11
- data/spec/factory/test_between.rb +0 -12
- data/spec/factory/test_intersect.rb +0 -12
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
class Predicate
|
|
2
|
+
module SetOp
|
|
3
|
+
include Expr
|
|
4
|
+
|
|
5
|
+
def priority; 80; end
|
|
6
|
+
|
|
7
|
+
def left
|
|
8
|
+
self[1]
|
|
9
|
+
end
|
|
10
|
+
alias :identifier :left
|
|
11
|
+
|
|
12
|
+
def right
|
|
13
|
+
self[2]
|
|
14
|
+
end
|
|
15
|
+
alias :values :right
|
|
16
|
+
|
|
17
|
+
def free_variables
|
|
18
|
+
@free_variables ||= left.free_variables | right.free_variables
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def constant_variables
|
|
22
|
+
[]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -18,13 +18,8 @@ class Predicate
|
|
|
18
18
|
self
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
def dyadic_priority
|
|
22
|
-
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def priority
|
|
26
|
-
100
|
|
27
|
-
end
|
|
21
|
+
def dyadic_priority; 1000; end
|
|
22
|
+
def priority; 100; end
|
|
28
23
|
|
|
29
24
|
def free_variables
|
|
30
25
|
@free_variables ||= []
|
|
@@ -38,5 +33,9 @@ class Predicate
|
|
|
38
33
|
true
|
|
39
34
|
end
|
|
40
35
|
|
|
36
|
+
def to_hash
|
|
37
|
+
{}
|
|
38
|
+
end
|
|
39
|
+
|
|
41
40
|
end
|
|
42
41
|
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
class Predicate
|
|
2
|
+
module Var
|
|
3
|
+
include Expr
|
|
4
|
+
|
|
5
|
+
def priority; 100; end
|
|
6
|
+
|
|
7
|
+
def formaldef
|
|
8
|
+
self[1]
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def semantics
|
|
12
|
+
self[2]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def free_variables
|
|
16
|
+
@free_variables ||= [ [formaldef, semantics] ]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def dig_terms
|
|
20
|
+
@dig_terms ||= case formaldef
|
|
21
|
+
when String
|
|
22
|
+
formaldef.split(".").map{|elm|
|
|
23
|
+
elm =~ /^\d+$/ ? elm.to_i : elm.to_sym
|
|
24
|
+
}
|
|
25
|
+
when Array
|
|
26
|
+
formaldef
|
|
27
|
+
else
|
|
28
|
+
raise ArgumentError, "Unrecognized variable def `#{formaldef}`"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def evaluate(on)
|
|
33
|
+
case semantics
|
|
34
|
+
when :dig
|
|
35
|
+
dig_terms.inject(on){|ctx,elm| ctx.dig(elm) }
|
|
36
|
+
when :send
|
|
37
|
+
dig_terms.inject(on){|ctx,elm| ctx.__send__(elm.to_sym) }
|
|
38
|
+
when :public_send
|
|
39
|
+
dig_terms.inject(on){|ctx,elm| ctx.public_send(elm.to_sym) }
|
|
40
|
+
else
|
|
41
|
+
raise ArgumentError, "Unrecognized variable semantics `#{semantics}`"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -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
|
|
@@ -57,6 +57,14 @@ class Predicate
|
|
|
57
57
|
"#{apply(sexpr.identifier)} INTERSECTS #{to_literal(sexpr.values)}"
|
|
58
58
|
end
|
|
59
59
|
|
|
60
|
+
def on_subset(sexpr)
|
|
61
|
+
"#{apply(sexpr.identifier)} IS SUBSET OF #{to_literal(sexpr.values)}"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def on_superset(sexpr)
|
|
65
|
+
"#{apply(sexpr.identifier)} IS SUPERSET OF #{to_literal(sexpr.values)}"
|
|
66
|
+
end
|
|
67
|
+
|
|
60
68
|
def on_literal(sexpr)
|
|
61
69
|
to_literal(sexpr.last)
|
|
62
70
|
end
|
|
@@ -69,16 +77,36 @@ class Predicate
|
|
|
69
77
|
"#{apply(sexpr.left)} =~ #{apply(sexpr.right)}"
|
|
70
78
|
end
|
|
71
79
|
|
|
80
|
+
def on_empty(sexpr)
|
|
81
|
+
"EMPTY(#{commalist(sexpr.body)})"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def on_size(sexpr)
|
|
85
|
+
"SIZE(#{commalist(sexpr.body)})"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
#jeny(predicate) def on_${op_name}(sexpr)
|
|
89
|
+
#jeny(predicate) "${OP_NAME}(#{commalist(sexpr.body)})"
|
|
90
|
+
#jeny(predicate) end
|
|
91
|
+
|
|
72
92
|
def on_native(sexpr)
|
|
73
93
|
sexpr.last.inspect
|
|
74
94
|
end
|
|
75
95
|
|
|
96
|
+
def on_var(sexpr)
|
|
97
|
+
"#{sexpr.semantics}(#{sexpr.formaldef})"
|
|
98
|
+
end
|
|
99
|
+
|
|
76
100
|
def on_missing(sexpr)
|
|
77
101
|
raise "Unimplemented: #{sexpr.first}"
|
|
78
102
|
end
|
|
79
103
|
|
|
80
104
|
protected
|
|
81
105
|
|
|
106
|
+
def commalist(of)
|
|
107
|
+
of.map{|o| apply(o) }.join(',')
|
|
108
|
+
end
|
|
109
|
+
|
|
82
110
|
def to_literal(x)
|
|
83
111
|
case x
|
|
84
112
|
when Placeholder then "$#{x.object_id}"
|
|
@@ -106,14 +106,18 @@ 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
|
|
|
112
113
|
def on_unsupported(sexpr)
|
|
113
|
-
raise NotSupportedError
|
|
114
|
+
raise NotSupportedError, "Unsupported predicate #{sexpr}"
|
|
114
115
|
end
|
|
116
|
+
alias :on_var :on_unsupported
|
|
115
117
|
alias :on_native :on_unsupported
|
|
116
118
|
alias :on_intersect :on_unsupported
|
|
119
|
+
alias :on_subset :on_unsupported
|
|
120
|
+
alias :on_superset :on_unsupported
|
|
117
121
|
end
|
|
118
122
|
include Methods
|
|
119
123
|
end # class ToSequel
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
class Predicate
|
|
2
|
+
module Sugar
|
|
3
|
+
|
|
4
|
+
[ :eq, :neq, :lt, :lte, :gt, :gte ].each do |m|
|
|
5
|
+
define_method(m) do |left, right=nil|
|
|
6
|
+
return comp(m, left) if TupleLike===left && right.nil?
|
|
7
|
+
super(left, right)
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def between(middle, lower_bound, upper_bound)
|
|
12
|
+
_factor_predicate [:and, [:gte, sexpr(middle), sexpr(lower_bound)],
|
|
13
|
+
[:lte, sexpr(middle), sexpr(upper_bound)]]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def match(left, right, options = nil)
|
|
17
|
+
super(left, right, options)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def min_size(left, right)
|
|
21
|
+
unless right.is_a?(Integer)
|
|
22
|
+
raise ArgumentError, "Integer expected, got #{right}"
|
|
23
|
+
end
|
|
24
|
+
if RUBY_VERSION >= "2.6"
|
|
25
|
+
has_size(left, Range.new(right,nil))
|
|
26
|
+
else
|
|
27
|
+
has_size(left, Range.new(right,(2**32-1)))
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def max_size(left, right)
|
|
32
|
+
unless right.is_a?(Integer)
|
|
33
|
+
raise ArgumentError, "Integer expected, got #{right}"
|
|
34
|
+
end
|
|
35
|
+
has_size(left, 0..right)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def is_null(operand)
|
|
39
|
+
eq(operand, nil)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
#jeny(sugar) def ${op_name}(*args)
|
|
43
|
+
#jeny(sugar) TODO
|
|
44
|
+
#jeny(sugar) end
|
|
45
|
+
|
|
46
|
+
end # module Sugar
|
|
47
|
+
end # class Predicate
|
data/lib/predicate/version.rb
CHANGED
data/lib/predicate.rb
CHANGED
|
@@ -2,12 +2,16 @@ require 'sexpr'
|
|
|
2
2
|
require_relative 'predicate/version'
|
|
3
3
|
require_relative 'predicate/placeholder'
|
|
4
4
|
require_relative 'predicate/factory'
|
|
5
|
+
require_relative 'predicate/sugar'
|
|
5
6
|
require_relative 'predicate/grammar'
|
|
6
7
|
require_relative 'predicate/processors'
|
|
8
|
+
require_relative 'predicate/dsl'
|
|
7
9
|
class Predicate
|
|
8
10
|
|
|
9
|
-
class
|
|
10
|
-
class
|
|
11
|
+
class Error < StandardError; end
|
|
12
|
+
class NotSupportedError < Error; end
|
|
13
|
+
class UnboundError < Error; end
|
|
14
|
+
class TypeError < Error; end
|
|
11
15
|
|
|
12
16
|
TupleLike = ->(t){ t.is_a?(Hash) }
|
|
13
17
|
|
|
@@ -21,6 +25,7 @@ class Predicate
|
|
|
21
25
|
|
|
22
26
|
class << self
|
|
23
27
|
include Factory
|
|
28
|
+
include Sugar
|
|
24
29
|
|
|
25
30
|
def coerce(arg)
|
|
26
31
|
case arg
|
|
@@ -36,10 +41,22 @@ class Predicate
|
|
|
36
41
|
end
|
|
37
42
|
alias :parse :coerce
|
|
38
43
|
|
|
44
|
+
def dsl(var = var(".", :dig), &bl)
|
|
45
|
+
Predicate::Dsl.new(var, false).instance_eval(&bl)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def currying(var = var(".", :dig), &bl)
|
|
49
|
+
Predicate::Dsl.new(var, true).instance_eval(&bl)
|
|
50
|
+
end
|
|
51
|
+
|
|
39
52
|
private
|
|
40
53
|
|
|
41
|
-
def _factor_predicate(arg)
|
|
42
|
-
|
|
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)
|
|
43
60
|
end
|
|
44
61
|
|
|
45
62
|
end
|
|
@@ -142,4 +159,11 @@ class Predicate
|
|
|
142
159
|
expr.to_s(scope)
|
|
143
160
|
end
|
|
144
161
|
|
|
162
|
+
# If possible, converts this predicate back to a `{ attr: value, ... }`
|
|
163
|
+
# hash. Raises an IllegalArgumentError if the predicate cannot be
|
|
164
|
+
# represented that way.
|
|
165
|
+
def to_hash
|
|
166
|
+
expr.to_hash
|
|
167
|
+
end
|
|
168
|
+
|
|
145
169
|
end # class Predicate
|