factbase 0.11.2 → 0.12.1
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 +4 -4
- data/.rultor.yml +0 -1
- data/benchmark/bench_query.rb +3 -1
- data/lib/factbase/indexed/indexed_term.rb +11 -2
- data/lib/factbase/rules.rb +1 -1
- data/lib/factbase/terms/aggregates.rb +8 -8
- data/lib/factbase/terms/aliases.rb +3 -3
- data/lib/factbase/terms/debug.rb +26 -1
- data/lib/factbase/terms/logical.rb +1 -1
- data/lib/factbase/terms/strings.rb +2 -2
- data/lib/factbase/version.rb +1 -1
- data/lib/factbase.rb +2 -2
- data/test/factbase/terms/test_debug.rb +76 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1ba24c42ea5df6fab3bfcf4477eff165173de5eb224c80e8bd1b1eeda5a9036d
|
4
|
+
data.tar.gz: b0f9362eec0b148f451dcbc94012db27ac97a02e4c6fbe48e0303a33d5f46c9e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e6460f586f42569fac5f6dedd783ab68e47f1fc2e7e05932033003586b40339044005d383306fce26f3f56a375d7a33d3a2798d3b84356e76c09550c6375e3d9
|
7
|
+
data.tar.gz: c8067f91613d87093ebf824c4800298e2b2e25654fc4ad9b5c3fed4c26e8265e19968a8eac887e5b541de48e5ebc531df6925c4ad73c48b49b1cb80ba429e5b7
|
data/.rultor.yml
CHANGED
data/benchmark/bench_query.rb
CHANGED
@@ -6,7 +6,7 @@
|
|
6
6
|
require_relative '../lib/factbase'
|
7
7
|
|
8
8
|
def bench_query(bmk, fb)
|
9
|
-
total =
|
9
|
+
total = 20_000
|
10
10
|
total.times do |i|
|
11
11
|
f = fb.insert
|
12
12
|
f.id = i
|
@@ -15,6 +15,7 @@ def bench_query(bmk, fb)
|
|
15
15
|
f.cost = rand(1..100)
|
16
16
|
f.foo = rand(0.0..100.0).round(3)
|
17
17
|
f.bar = rand(100..300)
|
18
|
+
f.blue = 1 if rand > 0.5
|
18
19
|
f.seenBy = "User#{i}" if i.even?
|
19
20
|
f.zzz = "Extra#{i}" if (i % 10).zero?
|
20
21
|
end
|
@@ -25,6 +26,7 @@ def bench_query(bmk, fb)
|
|
25
26
|
'(gt cost 50)',
|
26
27
|
'(eq title \'Object Thinking 5000\')',
|
27
28
|
'(and (eq foo 42.998) (or (gt bar 200) (absent zzz)))',
|
29
|
+
'(and (exists foo) (not (exists blue)))',
|
28
30
|
'(eq id (agg (always) (max id)))',
|
29
31
|
'(join "c<=cost,b<=bar" (eq id (agg (always) (max id))))'
|
30
32
|
].each do |q|
|
@@ -120,11 +120,20 @@ module Factbase::IndexedTerm
|
|
120
120
|
end
|
121
121
|
r
|
122
122
|
when :not
|
123
|
-
|
123
|
+
if @idx[key].nil?
|
124
|
+
yes = @operands.first.predict(maps, params)
|
125
|
+
if yes.nil?
|
126
|
+
@idx[key] = { r: nil }
|
127
|
+
else
|
128
|
+
yes = yes.to_a.to_set
|
129
|
+
@idx[key] = { r: maps.to_a.reject { |m| yes.include?(m) } }
|
130
|
+
end
|
131
|
+
end
|
132
|
+
r = @idx[key][:r]
|
124
133
|
if r.nil?
|
125
134
|
nil
|
126
135
|
else
|
127
|
-
(maps & []) |
|
136
|
+
(maps & []) | r
|
128
137
|
end
|
129
138
|
end
|
130
139
|
end
|
data/lib/factbase/rules.rb
CHANGED
@@ -14,7 +14,7 @@ require_relative '../factbase/syntax'
|
|
14
14
|
# to insert a fact without this property to lead to a runtime error. Here is how:
|
15
15
|
#
|
16
16
|
# fb = Factbase.new
|
17
|
-
# fb =
|
17
|
+
# fb = Factbase::Rules.new(fb, '(exists foo)')
|
18
18
|
# fb.txn do |fbt|
|
19
19
|
# f = fbt.insert
|
20
20
|
# f.bar = 3 # No exception here
|
@@ -28,9 +28,9 @@ module Factbase::Aggregates
|
|
28
28
|
def nth(_fact, maps, _fb)
|
29
29
|
assert_args(2)
|
30
30
|
pos = @operands[0]
|
31
|
-
raise "An integer expected, but #{pos} provided" unless pos.is_a?(Integer)
|
31
|
+
raise "An integer is expected, but #{pos} provided" unless pos.is_a?(Integer)
|
32
32
|
k = @operands[1]
|
33
|
-
raise "A symbol expected, but #{k} provided" unless k.is_a?(Symbol)
|
33
|
+
raise "A symbol is expected, but #{k} provided" unless k.is_a?(Symbol)
|
34
34
|
m = maps[pos]
|
35
35
|
return nil if m.nil?
|
36
36
|
m[k.to_s]
|
@@ -39,7 +39,7 @@ module Factbase::Aggregates
|
|
39
39
|
def first(_fact, maps, _fb)
|
40
40
|
assert_args(1)
|
41
41
|
k = @operands[0]
|
42
|
-
raise "A symbol expected, but #{k} provided" unless k.is_a?(Symbol)
|
42
|
+
raise "A symbol is expected, but #{k} provided" unless k.is_a?(Symbol)
|
43
43
|
first = maps[0]
|
44
44
|
return nil if first.nil?
|
45
45
|
first[k.to_s]
|
@@ -47,7 +47,7 @@ module Factbase::Aggregates
|
|
47
47
|
|
48
48
|
def sum(_fact, maps, _fb)
|
49
49
|
k = @operands[0]
|
50
|
-
raise "A symbol expected, but '#{k}' provided" unless k.is_a?(Symbol)
|
50
|
+
raise "A symbol is expected, but '#{k}' provided" unless k.is_a?(Symbol)
|
51
51
|
sum = 0
|
52
52
|
maps.each do |m|
|
53
53
|
vv = m[k.to_s]
|
@@ -63,9 +63,9 @@ module Factbase::Aggregates
|
|
63
63
|
def agg(fact, maps, fb)
|
64
64
|
assert_args(2)
|
65
65
|
selector = @operands[0]
|
66
|
-
raise "A term expected, but '#{selector}' provided" unless selector.is_a?(Factbase::Term)
|
66
|
+
raise "A term is expected, but '#{selector}' provided" unless selector.is_a?(Factbase::Term)
|
67
67
|
term = @operands[1]
|
68
|
-
raise "A term expected, but '#{term}' provided" unless term.is_a?(Factbase::Term)
|
68
|
+
raise "A term is expected, but '#{term}' provided" unless term.is_a?(Factbase::Term)
|
69
69
|
subset = fb.query(selector, maps).each(fb, fact).to_a
|
70
70
|
term.evaluate(nil, subset, fb)
|
71
71
|
end
|
@@ -73,7 +73,7 @@ module Factbase::Aggregates
|
|
73
73
|
def empty(fact, maps, fb)
|
74
74
|
assert_args(1)
|
75
75
|
term = @operands[0]
|
76
|
-
raise "A term expected, but '#{term}' provided" unless term.is_a?(Factbase::Term)
|
76
|
+
raise "A term is expected, but '#{term}' provided" unless term.is_a?(Factbase::Term)
|
77
77
|
# rubocop:disable Lint/UnreachableLoop
|
78
78
|
fb.query(term, maps).each(fb, fact) do
|
79
79
|
return false
|
@@ -84,7 +84,7 @@ module Factbase::Aggregates
|
|
84
84
|
|
85
85
|
def _best(maps)
|
86
86
|
k = @operands[0]
|
87
|
-
raise "A symbol expected, but #{k} provided" unless k.is_a?(Symbol)
|
87
|
+
raise "A symbol is expected, but #{k} provided" unless k.is_a?(Symbol)
|
88
88
|
best = nil
|
89
89
|
maps.each do |m|
|
90
90
|
vv = m[k.to_s]
|
@@ -14,7 +14,7 @@ module Factbase::Aliases
|
|
14
14
|
def as(fact, maps, fb)
|
15
15
|
assert_args(2)
|
16
16
|
a = @operands[0]
|
17
|
-
raise "A symbol expected as first argument of 'as'" unless a.is_a?(Symbol)
|
17
|
+
raise "A symbol is expected as first argument of 'as'" unless a.is_a?(Symbol)
|
18
18
|
vv = _values(1, fact, maps, fb)
|
19
19
|
vv&.each { |v| fact.send(:"#{a}=", v) }
|
20
20
|
true
|
@@ -23,13 +23,13 @@ module Factbase::Aliases
|
|
23
23
|
def join(fact, maps, fb)
|
24
24
|
assert_args(2)
|
25
25
|
jumps = @operands[0]
|
26
|
-
raise "A string expected as first argument of 'join'" unless jumps.is_a?(String)
|
26
|
+
raise "A string is expected as first argument of 'join'" unless jumps.is_a?(String)
|
27
27
|
jumps = jumps.split(',')
|
28
28
|
.map(&:strip)
|
29
29
|
.map { |j| j.split('<=').map(&:strip) }
|
30
30
|
.map { |j| j.size == 1 ? [j[0], j[0]] : j }
|
31
31
|
term = @operands[1]
|
32
|
-
raise "A term expected, but '#{term}' provided" unless term.is_a?(Factbase::Term)
|
32
|
+
raise "A term is expected, but '#{term}' provided" unless term.is_a?(Factbase::Term)
|
33
33
|
subset = fb.query(term, maps).each(fb, fact).to_a
|
34
34
|
subset.each do |s|
|
35
35
|
jumps.each do |to, from|
|
data/lib/factbase/terms/debug.rb
CHANGED
@@ -14,9 +14,34 @@ module Factbase::Debug
|
|
14
14
|
def traced(fact, maps, fb)
|
15
15
|
assert_args(1)
|
16
16
|
t = @operands[0]
|
17
|
-
raise "A term expected, but '#{t}' provided" unless t.is_a?(Factbase::Term)
|
17
|
+
raise "A term is expected, but '#{t}' provided" unless t.is_a?(Factbase::Term)
|
18
18
|
r = t.evaluate(fact, maps, fb)
|
19
19
|
puts "#{self} -> #{r}"
|
20
20
|
r
|
21
21
|
end
|
22
|
+
|
23
|
+
def assert(fact, maps, fb)
|
24
|
+
assert_args(2)
|
25
|
+
message = @operands[0]
|
26
|
+
unless message.is_a?(String)
|
27
|
+
raise ArgumentError,
|
28
|
+
"A string is expected as first argument of 'assert', but '#{message}' provided"
|
29
|
+
end
|
30
|
+
t = @operands[1]
|
31
|
+
unless t.is_a?(Factbase::Term)
|
32
|
+
raise ArgumentError,
|
33
|
+
"A term is expected as second argument of 'assert', but '#{t}' provided"
|
34
|
+
end
|
35
|
+
result = t.evaluate(fact, maps, fb)
|
36
|
+
# Convert result to boolean-like evaluation
|
37
|
+
# Arrays are truthy if they contain at least one truthy element
|
38
|
+
truthy =
|
39
|
+
if result.is_a?(Array)
|
40
|
+
result.any? { |v| v && v != 0 }
|
41
|
+
else
|
42
|
+
result && result != 0
|
43
|
+
end
|
44
|
+
raise message unless truthy
|
45
|
+
true
|
46
|
+
end
|
22
47
|
end
|
@@ -119,6 +119,6 @@ module Factbase::Logical
|
|
119
119
|
val = val[0] if val.respond_to?(:each)
|
120
120
|
return false if val.nil?
|
121
121
|
return val if val.is_a?(TrueClass) || val.is_a?(FalseClass)
|
122
|
-
raise "Boolean expected, while #{val.class} received from #{@operands[pos]}"
|
122
|
+
raise "Boolean is expected, while #{val.class} received from #{@operands[pos]}"
|
123
123
|
end
|
124
124
|
end
|
@@ -25,10 +25,10 @@ module Factbase::Strings
|
|
25
25
|
assert_args(2)
|
26
26
|
str = _values(0, fact, maps, fb)
|
27
27
|
return false if str.nil?
|
28
|
-
raise 'Exactly one string expected' unless str.size == 1
|
28
|
+
raise 'Exactly one string is expected' unless str.size == 1
|
29
29
|
re = _values(1, fact, maps, fb)
|
30
30
|
raise 'Regexp is nil' if re.nil?
|
31
|
-
raise 'Exactly one regexp expected' unless re.size == 1
|
31
|
+
raise 'Exactly one regexp is expected' unless re.size == 1
|
32
32
|
str[0].to_s.match?(re[0])
|
33
33
|
end
|
34
34
|
end
|
data/lib/factbase/version.rb
CHANGED
data/lib/factbase.rb
CHANGED
@@ -102,8 +102,8 @@ class Factbase
|
|
102
102
|
#
|
103
103
|
# A fact, when inserted, is empty. It doesn't contain any properties.
|
104
104
|
#
|
105
|
-
# The operation is thread-safe, meaning that
|
106
|
-
# insert facts parallel without breaking the consistency of the factbase.
|
105
|
+
# The operation is thread-safe, meaning that different threads may
|
106
|
+
# insert facts in parallel without breaking the consistency of the factbase.
|
107
107
|
#
|
108
108
|
# @return [Factbase::Fact] The fact just inserted
|
109
109
|
def insert
|
@@ -20,7 +20,7 @@ class TestDebug < Factbase::Test
|
|
20
20
|
|
21
21
|
def test_traced_raises
|
22
22
|
e = assert_raises(StandardError) { Factbase::Term.new(:traced, ['foo']).evaluate(fact, [], Factbase.new) }
|
23
|
-
assert_match(/A term expected, but 'foo' provided/, e.message)
|
23
|
+
assert_match(/A term is expected, but 'foo' provided/, e.message)
|
24
24
|
end
|
25
25
|
|
26
26
|
def test_traced_raises_when_too_many_args
|
@@ -33,4 +33,79 @@ class TestDebug < Factbase::Test
|
|
33
33
|
end
|
34
34
|
assert_match(/Too many \(\d+\) operands for 'traced' \(\d+ expected\)/, e.message)
|
35
35
|
end
|
36
|
+
|
37
|
+
def test_assert_with_true_condition
|
38
|
+
t = Factbase::Term.new(:assert, ['all must be positive', Factbase::Term.new(:gt, [:foo, 0])])
|
39
|
+
assert(t.evaluate(fact('foo' => 5), [], Factbase.new))
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_assert_with_false_condition
|
43
|
+
t = Factbase::Term.new(:assert, ['all must be positive', Factbase::Term.new(:gt, [:foo, 0])])
|
44
|
+
e =
|
45
|
+
assert_raises(RuntimeError) do
|
46
|
+
t.evaluate(fact('foo' => -1), [], Factbase.new)
|
47
|
+
end
|
48
|
+
assert_equal("all must be positive at (assert 'all must be positive' (gt foo 0))", e.message)
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_assert_with_zero_value
|
52
|
+
t = Factbase::Term.new(:assert, ['value must not be zero', Factbase::Term.new(:gt, [:foo, 0])])
|
53
|
+
e =
|
54
|
+
assert_raises(RuntimeError) do
|
55
|
+
t.evaluate(fact('foo' => 0), [], Factbase.new)
|
56
|
+
end
|
57
|
+
assert_equal("value must not be zero at (assert 'value must not be zero' (gt foo 0))", e.message)
|
58
|
+
end
|
59
|
+
|
60
|
+
def test_assert_with_array_true_condition
|
61
|
+
t = Factbase::Term.new(:assert, ['at least one positive', Factbase::Term.new(:gt, [:foo, 0])])
|
62
|
+
assert(t.evaluate(fact('foo' => [1, 2, 3]), [], Factbase.new))
|
63
|
+
end
|
64
|
+
|
65
|
+
def test_assert_with_array_false_condition
|
66
|
+
t = Factbase::Term.new(:assert, ['at least one positive', Factbase::Term.new(:gt, [:foo, 0])])
|
67
|
+
e =
|
68
|
+
assert_raises(RuntimeError) do
|
69
|
+
t.evaluate(fact('foo' => [-1, -2, -3]), [], Factbase.new)
|
70
|
+
end
|
71
|
+
assert_equal("at least one positive at (assert 'at least one positive' (gt foo 0))", e.message)
|
72
|
+
end
|
73
|
+
|
74
|
+
def test_assert_with_mixed_array
|
75
|
+
t = Factbase::Term.new(:assert, ['at least one positive', Factbase::Term.new(:gt, [:foo, 0])])
|
76
|
+
assert(t.evaluate(fact('foo' => [-1, 0, 3]), [], Factbase.new))
|
77
|
+
end
|
78
|
+
|
79
|
+
def test_assert_raises_when_message_not_string
|
80
|
+
e =
|
81
|
+
assert_raises(StandardError) do
|
82
|
+
Factbase::Term.new(:assert, [123, Factbase::Term.new(:gt, [:foo, 0])]).evaluate(fact, [], Factbase.new)
|
83
|
+
end
|
84
|
+
assert_match(/A string is expected as first argument of 'assert', but '123' provided/, e.message)
|
85
|
+
end
|
86
|
+
|
87
|
+
def test_assert_raises_when_second_arg_not_term
|
88
|
+
e =
|
89
|
+
assert_raises(StandardError) do
|
90
|
+
Factbase::Term.new(:assert, %w[message not_a_term]).evaluate(fact, [], Factbase.new)
|
91
|
+
end
|
92
|
+
assert_match(/A term is expected as second argument of 'assert', but 'not_a_term' provided/, e.message)
|
93
|
+
end
|
94
|
+
|
95
|
+
def test_assert_raises_when_too_few_args
|
96
|
+
e =
|
97
|
+
assert_raises(StandardError) do
|
98
|
+
Factbase::Term.new(:assert, ['message']).evaluate(fact, [], Factbase.new)
|
99
|
+
end
|
100
|
+
assert_match(/Too few \(\d+\) operands for 'assert' \(\d+ expected\)/, e.message)
|
101
|
+
end
|
102
|
+
|
103
|
+
def test_assert_raises_when_too_many_args
|
104
|
+
e =
|
105
|
+
assert_raises(StandardError) do
|
106
|
+
Factbase::Term.new(:assert, ['message', Factbase::Term.new(:gt, [:foo, 0]), 'extra']).evaluate(fact, [],
|
107
|
+
Factbase.new)
|
108
|
+
end
|
109
|
+
assert_match(/Too many \(\d+\) operands for 'assert' \(\d+ expected\)/, e.message)
|
110
|
+
end
|
36
111
|
end
|