factbase 0.0.50 → 0.0.52
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +4 -4
- data/Gemfile.lock +19 -26
- data/README.md +57 -35
- data/factbase.gemspec +1 -0
- data/lib/factbase/{tuples.rb → accum.rb} +40 -40
- data/lib/factbase/fact.rb +4 -9
- data/lib/factbase/looged.rb +18 -5
- data/lib/factbase/query.rb +27 -5
- data/lib/factbase/rules.rb +6 -2
- data/lib/factbase/syntax.rb +2 -2
- data/lib/factbase/tee.rb +59 -0
- data/lib/factbase/term.rb +32 -278
- data/lib/factbase/terms/aggregates.rb +109 -0
- data/lib/factbase/terms/aliases.rb +60 -0
- data/lib/factbase/terms/debug.rb +39 -0
- data/lib/factbase/terms/defn.rb +54 -0
- data/lib/factbase/terms/logical.rb +95 -0
- data/lib/factbase/terms/math.rb +91 -0
- data/lib/factbase/terms/meta.rb +73 -0
- data/lib/factbase/terms/ordering.rb +51 -0
- data/lib/factbase/terms/strings.rb +41 -0
- data/lib/factbase/to_xml.rb +6 -2
- data/lib/factbase.rb +3 -1
- data/test/factbase/terms/test_aggregates.rb +76 -0
- data/test/factbase/terms/test_aliases.rb +79 -0
- data/test/factbase/terms/test_math.rb +99 -0
- data/test/factbase/test_accum.rb +70 -0
- data/test/factbase/test_looged.rb +8 -0
- data/test/factbase/test_query.rb +51 -8
- data/test/factbase/test_rules.rb +8 -1
- data/test/factbase/test_syntax.rb +1 -1
- data/test/factbase/test_tee.rb +53 -0
- data/test/factbase/test_term.rb +7 -76
- data/test/factbase/test_to_xml.rb +26 -2
- data/test/test_factbase.rb +3 -3
- metadata +32 -4
- data/test/factbase/test_tuples.rb +0 -106
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a70fcc45481bd1611d6bf2ed5c41ef40fe6f1ce67494abfa769c3b859b215960
|
4
|
+
data.tar.gz: 609bd76eaca6f4668bbbf5678514503edb1f9184e9abd62321dc02cdd614f80c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 97e8cc3c9f53349aaf85aa3f95ba9f0e2525dd85f242ade079083b4ffab042dbb0b3f1ad776573646f7fae0107e709404b70e340fc9660b99ef8770a313aa8a9
|
7
|
+
data.tar.gz: e4d9404781d0314d4e6aa22f93affc3260fc053121c9e70204f8690d30de34feb26533254f15519b9e2562a5562e93398779d890ab78c3acc3afcd8ed8fba782
|
data/Gemfile
CHANGED
@@ -23,12 +23,12 @@
|
|
23
23
|
source 'https://rubygems.org'
|
24
24
|
gemspec
|
25
25
|
|
26
|
-
gem 'minitest', '5.
|
26
|
+
gem 'minitest', '5.24.0', require: false
|
27
27
|
gem 'rake', '13.2.1', require: false
|
28
|
-
gem 'rspec-rails', '6.1.
|
28
|
+
gem 'rspec-rails', '6.1.3', require: false
|
29
29
|
gem 'rubocop', '1.64.1', require: false
|
30
|
-
gem 'rubocop-performance', '1.21.
|
31
|
-
gem 'rubocop-rspec', '
|
30
|
+
gem 'rubocop-performance', '1.21.1', require: false
|
31
|
+
gem 'rubocop-rspec', '3.0.1', require: false
|
32
32
|
gem 'simplecov', '0.22.0', require: false
|
33
33
|
gem 'simplecov-cobertura', '2.1.0', require: false
|
34
34
|
gem 'yard', '0.9.36', require: false
|
data/Gemfile.lock
CHANGED
@@ -2,6 +2,7 @@ PATH
|
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
4
|
factbase (0.0.0)
|
5
|
+
backtrace (~> 0.3)
|
5
6
|
json (~> 2.7)
|
6
7
|
loog (~> 0.2)
|
7
8
|
nokogiri (~> 1.10)
|
@@ -38,6 +39,7 @@ GEM
|
|
38
39
|
mutex_m
|
39
40
|
tzinfo (~> 2.0)
|
40
41
|
ast (2.4.2)
|
42
|
+
backtrace (0.4.0)
|
41
43
|
base64 (0.2.0)
|
42
44
|
bigdecimal (3.1.8)
|
43
45
|
builder (3.3.0)
|
@@ -47,11 +49,11 @@ GEM
|
|
47
49
|
diff-lcs (1.5.1)
|
48
50
|
docile (1.4.0)
|
49
51
|
drb (2.2.1)
|
50
|
-
erubi (1.
|
52
|
+
erubi (1.13.0)
|
51
53
|
i18n (1.14.5)
|
52
54
|
concurrent-ruby (~> 1.0)
|
53
55
|
io-console (0.7.2)
|
54
|
-
irb (1.13.
|
56
|
+
irb (1.13.2)
|
55
57
|
rdoc (>= 4.0.0)
|
56
58
|
reline (>= 0.4.2)
|
57
59
|
json (2.7.2)
|
@@ -60,15 +62,15 @@ GEM
|
|
60
62
|
crass (~> 1.0.2)
|
61
63
|
nokogiri (>= 1.12.0)
|
62
64
|
loog (0.5.1)
|
63
|
-
minitest (5.
|
65
|
+
minitest (5.24.0)
|
64
66
|
mutex_m (0.2.0)
|
65
|
-
nokogiri (1.16.
|
67
|
+
nokogiri (1.16.6-arm64-darwin)
|
66
68
|
racc (~> 1.4)
|
67
|
-
nokogiri (1.16.
|
69
|
+
nokogiri (1.16.6-x64-mingw-ucrt)
|
68
70
|
racc (~> 1.4)
|
69
|
-
nokogiri (1.16.
|
71
|
+
nokogiri (1.16.6-x86_64-darwin)
|
70
72
|
racc (~> 1.4)
|
71
|
-
nokogiri (1.16.
|
73
|
+
nokogiri (1.16.6-x86_64-linux)
|
72
74
|
racc (~> 1.4)
|
73
75
|
parallel (1.25.1)
|
74
76
|
parser (3.3.3.0)
|
@@ -111,13 +113,13 @@ GEM
|
|
111
113
|
strscan
|
112
114
|
rspec-core (3.13.0)
|
113
115
|
rspec-support (~> 3.13.0)
|
114
|
-
rspec-expectations (3.13.
|
116
|
+
rspec-expectations (3.13.1)
|
115
117
|
diff-lcs (>= 1.2.0, < 2.0)
|
116
118
|
rspec-support (~> 3.13.0)
|
117
119
|
rspec-mocks (3.13.1)
|
118
120
|
diff-lcs (>= 1.2.0, < 2.0)
|
119
121
|
rspec-support (~> 3.13.0)
|
120
|
-
rspec-rails (6.1.
|
122
|
+
rspec-rails (6.1.3)
|
121
123
|
actionpack (>= 6.1)
|
122
124
|
activesupport (>= 6.1)
|
123
125
|
railties (>= 6.1)
|
@@ -139,19 +141,10 @@ GEM
|
|
139
141
|
unicode-display_width (>= 2.4.0, < 3.0)
|
140
142
|
rubocop-ast (1.31.3)
|
141
143
|
parser (>= 3.3.1.0)
|
142
|
-
rubocop-
|
143
|
-
rubocop (~> 1.41)
|
144
|
-
rubocop-factory_bot (2.26.1)
|
145
|
-
rubocop (~> 1.61)
|
146
|
-
rubocop-performance (1.21.0)
|
144
|
+
rubocop-performance (1.21.1)
|
147
145
|
rubocop (>= 1.48.1, < 2.0)
|
148
146
|
rubocop-ast (>= 1.31.1, < 2.0)
|
149
|
-
rubocop-rspec (
|
150
|
-
rubocop (~> 1.40)
|
151
|
-
rubocop-capybara (~> 2.17)
|
152
|
-
rubocop-factory_bot (~> 2.22)
|
153
|
-
rubocop-rspec_rails (~> 2.28)
|
154
|
-
rubocop-rspec_rails (2.29.1)
|
147
|
+
rubocop-rspec (3.0.1)
|
155
148
|
rubocop (~> 1.61)
|
156
149
|
ruby-progressbar (1.13.0)
|
157
150
|
simplecov (0.22.0)
|
@@ -163,7 +156,7 @@ GEM
|
|
163
156
|
simplecov (~> 0.19)
|
164
157
|
simplecov-html (0.12.3)
|
165
158
|
simplecov_json_formatter (0.1.4)
|
166
|
-
stringio (3.1.
|
159
|
+
stringio (3.1.1)
|
167
160
|
strscan (3.1.0)
|
168
161
|
tago (0.0.2)
|
169
162
|
thor (1.3.1)
|
@@ -173,7 +166,7 @@ GEM
|
|
173
166
|
webrick (1.8.1)
|
174
167
|
yaml (0.3.0)
|
175
168
|
yard (0.9.36)
|
176
|
-
zeitwerk (2.6.
|
169
|
+
zeitwerk (2.6.16)
|
177
170
|
|
178
171
|
PLATFORMS
|
179
172
|
arm64-darwin-22
|
@@ -183,12 +176,12 @@ PLATFORMS
|
|
183
176
|
|
184
177
|
DEPENDENCIES
|
185
178
|
factbase!
|
186
|
-
minitest (= 5.
|
179
|
+
minitest (= 5.24.0)
|
187
180
|
rake (= 13.2.1)
|
188
|
-
rspec-rails (= 6.1.
|
181
|
+
rspec-rails (= 6.1.3)
|
189
182
|
rubocop (= 1.64.1)
|
190
|
-
rubocop-performance (= 1.21.
|
191
|
-
rubocop-rspec (=
|
183
|
+
rubocop-performance (= 1.21.1)
|
184
|
+
rubocop-rspec (= 3.0.1)
|
192
185
|
simplecov (= 0.22.0)
|
193
186
|
simplecov-cobertura (= 2.1.0)
|
194
187
|
yard (= 0.9.36)
|
data/README.md
CHANGED
@@ -55,53 +55,58 @@ assert(f2.query('(eq foo 42)').each.to_a.size == 1)
|
|
55
55
|
```
|
56
56
|
|
57
57
|
There are some boolean terms available in a query
|
58
|
-
(they return either
|
59
|
-
|
60
|
-
* `(always)` and `(never)` are
|
61
|
-
* `(
|
62
|
-
* `(
|
63
|
-
* `(
|
64
|
-
* `(
|
65
|
-
* `(
|
66
|
-
|
67
|
-
* `(
|
68
|
-
* `(
|
69
|
-
* `(
|
70
|
-
* `(
|
71
|
-
* `(
|
72
|
-
* `(
|
58
|
+
(they return either `true` or `false`):
|
59
|
+
|
60
|
+
* `(always)` and `(never)` are `true` and `false`
|
61
|
+
* `(nil v)` is `true` if `v` is `nil`
|
62
|
+
* `(not b)` is the inverse of `b`
|
63
|
+
* `(or b1 b2 ...)` is `true` if at least one argument is `true`
|
64
|
+
* `(and b1 b2 ...)` — if all arguments are `true`
|
65
|
+
* `(when b1 b2)` — if `b1` is `true` and `b2` is `true`
|
66
|
+
or `b1` is `false`
|
67
|
+
* `(exists p)` — if `p` property exists
|
68
|
+
* `(absent p)` — if `p` property is absent
|
69
|
+
* `(zero v)` — if any `v` equals to zero
|
70
|
+
* `(eq v1 v2)` — if any `v1` equals to any `v2`
|
71
|
+
* `(lt v1 v2)` — if any `v1` is less than any `v2`
|
72
|
+
* `(gt v1 v2)` — if any `v1` is greater than any `v2`
|
73
|
+
* `(many v)` — if `v` has many values
|
74
|
+
* `(one v)` — if `v` has one value
|
75
|
+
* `(matches v s)` — if any `v` matches the `s` regular expression
|
73
76
|
|
74
77
|
There are a few terms that return non-boolean values:
|
75
78
|
|
76
|
-
* `(at i
|
77
|
-
* `(size
|
78
|
-
* `(type
|
79
|
-
|
79
|
+
* `(at i v)` is the `i`-th value of `v`
|
80
|
+
* `(size v)` is the cardinality of `v` (zero if `v` is `nil`)
|
81
|
+
* `(type v)` is the type of `v`
|
82
|
+
(`"String"`, `"Integer"`, `"Float"`, `"Time"`, or `"Array"`)
|
83
|
+
* `(either v1 v1)` is `v2` if `v1` is `nil`
|
84
|
+
|
85
|
+
It's possible to modify the facts retrieved, on fly:
|
86
|
+
|
87
|
+
* `(as p v)` adds property `p` with the value `v`
|
88
|
+
* `(join s t)` adds properties named by the `s` mask with the values retrieved
|
89
|
+
by the `t` term, for example, `(join "x<=foo,y<=bar" (gt x 5))` will add
|
90
|
+
`x` and `y` properties, setting them to values found in the `foo` and `bar`
|
91
|
+
properties in the facts that match `(gt x 5)`
|
80
92
|
|
81
93
|
Also, some simple arithmetic:
|
82
94
|
|
83
|
-
* `(plus
|
84
|
-
* `(minus
|
85
|
-
* `(times
|
86
|
-
* `(div
|
95
|
+
* `(plus v1 v2)` is a sum of `∑v1` and `∑v2`
|
96
|
+
* `(minus v1 v2)` is a deducation of `∑v2` from `∑v1`
|
97
|
+
* `(times v1 v2)` is a multiplication of `∏v1` and `∏v2`
|
98
|
+
* `(div v1 v2)` is a division of `∏v1` by `∏v2`
|
87
99
|
|
88
100
|
One term is for meta-programming:
|
89
101
|
|
90
|
-
* `(defn
|
91
|
-
* `(undef
|
102
|
+
* `(defn f "self.to_s")` defines a new term using Ruby syntax and returns `true`
|
103
|
+
* `(undef f)` undefines a term (nothing happens if it's not defined yet),
|
104
|
+
returns `true`
|
92
105
|
|
93
106
|
There are terms that are history of search aware:
|
94
107
|
|
95
|
-
* `(prev
|
96
|
-
* `(unique
|
97
|
-
|
98
|
-
There are also terms that match the entire factbase
|
99
|
-
and must be used inside the `(agg ..)` term:
|
100
|
-
|
101
|
-
* `(count)` returns the tally of facts
|
102
|
-
* `(max k)` returns the maximum value of the `k` property in all facts
|
103
|
-
* `(min k)` returns the minimum
|
104
|
-
* `(sum k)` returns the arithmetic sum of all values of the `k` property
|
108
|
+
* `(prev p)` returns the value of `p` property in the previously seen fact
|
109
|
+
* `(unique p)` returns true if the value of `p` property hasn't been seen yet
|
105
110
|
|
106
111
|
The `agg` term enables sub-queries by evaluating the first argument (term)
|
107
112
|
over all available facts, passing the entire subset to the second argument,
|
@@ -110,6 +115,23 @@ and then returning the result as an atomic value:
|
|
110
115
|
* `(lt age (agg (eq gender 'F') (max age)))` selects all facts where
|
111
116
|
the `age` is smaller than the maximum `age` of all women
|
112
117
|
* `(eq id (agg (always) (max id)))` selects the fact with the largest `id`
|
118
|
+
* `(eq salary (agg (eq dept $dept) (avg salary)))` selects the facts
|
119
|
+
with the salary average in their departments
|
120
|
+
|
121
|
+
There are also terms that match the entire factbase
|
122
|
+
and must be used primarily inside the `(agg ..)` term:
|
123
|
+
|
124
|
+
* `(nth v p)` returns the `p` property of the _v_-th fact (must be
|
125
|
+
a positive integer)
|
126
|
+
* `(first p)` returns the `p` property of the first fact
|
127
|
+
* `(count)` returns the tally of facts
|
128
|
+
* `(max p)` returns the maximum value of the `p` property in all facts
|
129
|
+
* `(min p)` returns the minimum
|
130
|
+
* `(sum p)` returns the arithmetic sum of all values of the `p` property
|
131
|
+
|
132
|
+
It's also possible to use a sub-query in a shorter form than with the `agg`:
|
133
|
+
|
134
|
+
* `(empty q)` is true if the subquery `q` is empty
|
113
135
|
|
114
136
|
## How to contribute
|
115
137
|
|
data/factbase.gemspec
CHANGED
@@ -42,6 +42,7 @@ Gem::Specification.new do |s|
|
|
42
42
|
s.files = `git ls-files`.split($RS)
|
43
43
|
s.rdoc_options = ['--charset=UTF-8']
|
44
44
|
s.extra_rdoc_files = ['README.md', 'LICENSE.txt']
|
45
|
+
s.add_runtime_dependency 'backtrace', '~>0.3'
|
45
46
|
s.add_runtime_dependency 'json', '~>2.7'
|
46
47
|
s.add_runtime_dependency 'loog', '~>0.2'
|
47
48
|
s.add_runtime_dependency 'nokogiri', '~>1.10'
|
@@ -22,55 +22,55 @@
|
|
22
22
|
|
23
23
|
require_relative '../factbase'
|
24
24
|
|
25
|
-
#
|
26
|
-
# from a factbase at a time, which depend on each other. For example,
|
27
|
-
# it's necessary to find a fact where the +name+ is set and then find
|
28
|
-
# another fact, where the salary is the +salary+ is the same as in the
|
29
|
-
# first found fact. Here is how:
|
30
|
-
#
|
31
|
-
# Factbase::Tuples.new(qt, ['(exists name)', '(eq salary, {f0.salary})']).each do |a, b|
|
32
|
-
# puts a.name
|
33
|
-
# puts b.salary
|
34
|
-
# end
|
35
|
-
#
|
36
|
-
# Here, the +{f0.salary}+ is a special substitution place, which is replaced
|
37
|
-
# by the +salary+ of the fact that is found by the previous query.
|
38
|
-
#
|
39
|
-
# The indexing of queries starts from zero.
|
25
|
+
# Accumulator of props.
|
40
26
|
#
|
41
27
|
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
42
28
|
# Copyright:: Copyright (c) 2024 Yegor Bugayenko
|
43
29
|
# License:: MIT
|
44
|
-
class Factbase::
|
45
|
-
|
46
|
-
|
47
|
-
|
30
|
+
class Factbase::Accum
|
31
|
+
# Ctor.
|
32
|
+
# @param [Factbase::Fact] fact The fact to decorate
|
33
|
+
# @param [Hash] props Hash of props that were set
|
34
|
+
# @param [Boolean] pass TRUE if all "set" operations must go through, to the +fact+
|
35
|
+
def initialize(fact, props, pass)
|
36
|
+
@fact = fact
|
37
|
+
@props = props
|
38
|
+
@pass = pass
|
48
39
|
end
|
49
40
|
|
50
|
-
|
51
|
-
|
52
|
-
# @return [Integer] Total number of arrays yielded
|
53
|
-
def each(&)
|
54
|
-
return to_enum(__method__) unless block_given?
|
55
|
-
each_rec([], @queries, &)
|
41
|
+
def to_s
|
42
|
+
"#{@fact} + #{@props}"
|
56
43
|
end
|
57
44
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
45
|
+
def method_missing(*args)
|
46
|
+
k = args[0].to_s
|
47
|
+
if k.end_with?('=')
|
48
|
+
kk = k[0..-2]
|
49
|
+
@props[kk] = [] if @props[kk].nil?
|
50
|
+
@props[kk] << args[1]
|
51
|
+
@fact.method_missing(*args) if @pass
|
52
|
+
return
|
66
53
|
end
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
54
|
+
if k == '[]'
|
55
|
+
kk = args[1].to_s
|
56
|
+
vv = @props[kk].nil? ? [] : @props[kk]
|
57
|
+
vvv = @fact.method_missing(*args)
|
58
|
+
vvv = [vvv] unless vvv.nil? || vvv.is_a?(Array)
|
59
|
+
vv += vvv unless vvv.nil?
|
60
|
+
vv.uniq!
|
61
|
+
return vv.empty? ? nil : vv
|
74
62
|
end
|
63
|
+
return @props[k][0] unless @props[k].nil?
|
64
|
+
@fact.method_missing(*args)
|
65
|
+
end
|
66
|
+
|
67
|
+
# rubocop:disable Style/OptionalBooleanParameter
|
68
|
+
def respond_to?(_method, _include_private = false)
|
69
|
+
# rubocop:enable Style/OptionalBooleanParameter
|
70
|
+
true
|
71
|
+
end
|
72
|
+
|
73
|
+
def respond_to_missing?(_method, _include_private = false)
|
74
|
+
true
|
75
75
|
end
|
76
76
|
end
|
data/lib/factbase/fact.rb
CHANGED
@@ -69,14 +69,9 @@ class Factbase::Fact
|
|
69
69
|
raise "Prop type '#{v.class}' is not allowed" unless [String, Integer, Float, Time].include?(v.class)
|
70
70
|
v = v.utc if v.is_a?(Time)
|
71
71
|
@mutex.synchronize do
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
@map[kk] = [v]
|
76
|
-
else
|
77
|
-
@map[kk] << v
|
78
|
-
@map[kk].uniq!
|
79
|
-
end
|
72
|
+
@map[kk] = [] if @map[kk].nil?
|
73
|
+
@map[kk] << v
|
74
|
+
@map[kk].uniq!
|
80
75
|
end
|
81
76
|
nil
|
82
77
|
elsif k == '[]'
|
@@ -87,7 +82,7 @@ class Factbase::Fact
|
|
87
82
|
raise "Can't get '#{k}', the fact is empty" if @map.empty?
|
88
83
|
raise "Can't find '#{k}' attribute out of [#{@map.keys.join(', ')}]"
|
89
84
|
end
|
90
|
-
v
|
85
|
+
v[0]
|
91
86
|
end
|
92
87
|
end
|
93
88
|
|
data/lib/factbase/looged.rb
CHANGED
@@ -55,7 +55,6 @@ class Factbase::Looged
|
|
55
55
|
|
56
56
|
def txn(this = self, &)
|
57
57
|
start = Time.now
|
58
|
-
before = @fb.size
|
59
58
|
id = nil
|
60
59
|
rollback = false
|
61
60
|
r = @fb.txn(this) do |fbt|
|
@@ -68,7 +67,7 @@ class Factbase::Looged
|
|
68
67
|
if rollback
|
69
68
|
@loog.debug("Txn ##{id} rolled back in #{start.ago}")
|
70
69
|
else
|
71
|
-
@loog.debug("Txn ##{id} #{r ? 'modified' : 'didn\'t touch'}
|
70
|
+
@loog.debug("Txn ##{id} #{r ? 'modified' : 'didn\'t touch'} the factbase in #{start.ago}")
|
72
71
|
end
|
73
72
|
r
|
74
73
|
end
|
@@ -130,12 +129,26 @@ class Factbase::Looged
|
|
130
129
|
@loog = loog
|
131
130
|
end
|
132
131
|
|
133
|
-
def
|
132
|
+
def one(params = {})
|
133
|
+
q = Factbase::Syntax.new(@expr).to_term.to_s
|
134
|
+
r = nil
|
135
|
+
tail = Factbase::Looged.elapsed do
|
136
|
+
r = @fb.query(@expr).one(params)
|
137
|
+
end
|
138
|
+
if r.nil?
|
139
|
+
@loog.debug("Nothing found by '#{q}' #{tail}")
|
140
|
+
else
|
141
|
+
@loog.debug("Found #{r} (#{r.class}) by '#{q}' #{tail}")
|
142
|
+
end
|
143
|
+
r
|
144
|
+
end
|
145
|
+
|
146
|
+
def each(params = {}, &)
|
134
147
|
q = Factbase::Syntax.new(@expr).to_term.to_s
|
135
148
|
if block_given?
|
136
149
|
r = nil
|
137
150
|
tail = Factbase::Looged.elapsed do
|
138
|
-
r = @fb.query(@expr).each(&)
|
151
|
+
r = @fb.query(@expr).each(params, &)
|
139
152
|
end
|
140
153
|
raise ".each of #{@query.class} returned #{r.class}" unless r.is_a?(Integer)
|
141
154
|
if r.zero?
|
@@ -147,7 +160,7 @@ class Factbase::Looged
|
|
147
160
|
else
|
148
161
|
array = []
|
149
162
|
tail = Factbase::Looged.elapsed do
|
150
|
-
@fb.query(@expr).each do |f|
|
163
|
+
@fb.query(@expr).each(params) do |f|
|
151
164
|
array << f
|
152
165
|
end
|
153
166
|
end
|
data/lib/factbase/query.rb
CHANGED
@@ -23,12 +23,16 @@
|
|
23
23
|
require_relative '../factbase'
|
24
24
|
require_relative 'syntax'
|
25
25
|
require_relative 'fact'
|
26
|
+
require_relative 'accum'
|
27
|
+
require_relative 'tee'
|
26
28
|
|
27
29
|
# Query.
|
28
30
|
#
|
29
31
|
# This is an internal class, it is not supposed to be instantiated directly. It
|
30
32
|
# is created by the +query()+ method of the +Factbase+ class.
|
31
33
|
#
|
34
|
+
# It is NOT thread-safe!
|
35
|
+
#
|
32
36
|
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
33
37
|
# Copyright:: Copyright (c) 2024 Yegor Bugayenko
|
34
38
|
# License:: MIT
|
@@ -43,26 +47,44 @@ class Factbase::Query
|
|
43
47
|
@query = query
|
44
48
|
end
|
45
49
|
|
46
|
-
# Iterate
|
50
|
+
# Iterate facts one by one.
|
51
|
+
# @param [Hash] params Optional params accessible in the query via the "$" symbol
|
47
52
|
# @yield [Fact] Facts one-by-one
|
48
53
|
# @return [Integer] Total number of facts yielded
|
49
|
-
def each
|
50
|
-
return to_enum(__method__) unless block_given?
|
54
|
+
def each(params = {})
|
55
|
+
return to_enum(__method__, params) unless block_given?
|
51
56
|
term = Factbase::Syntax.new(@query).to_term
|
52
57
|
yielded = 0
|
53
58
|
@maps.each do |m|
|
59
|
+
extras = {}
|
54
60
|
f = Factbase::Fact.new(@mutex, m)
|
55
|
-
|
61
|
+
params = params.transform_keys(&:to_s) if params.is_a?(Hash)
|
62
|
+
f = Factbase::Tee.new(f, params)
|
63
|
+
a = Factbase::Accum.new(f, extras, false)
|
64
|
+
r = term.evaluate(a, @maps)
|
56
65
|
unless r.is_a?(TrueClass) || r.is_a?(FalseClass)
|
57
66
|
raise "Unexpected evaluation result (#{r.class}), must be Boolean at #{@query}"
|
58
67
|
end
|
59
68
|
next unless r
|
60
|
-
yield f
|
69
|
+
yield Factbase::Accum.new(f, extras, true)
|
61
70
|
yielded += 1
|
62
71
|
end
|
63
72
|
yielded
|
64
73
|
end
|
65
74
|
|
75
|
+
# Read a single value.
|
76
|
+
# @param [Hash] params Optional params accessible in the query via the "$" symbol
|
77
|
+
# @return The value evaluated
|
78
|
+
def one(params = {})
|
79
|
+
term = Factbase::Syntax.new(@query).to_term
|
80
|
+
params = params.transform_keys(&:to_s) if params.is_a?(Hash)
|
81
|
+
r = term.evaluate(Factbase::Tee.new(nil, params), @maps)
|
82
|
+
unless %w[String Integer Float Time Array NilClass].include?(r.class.to_s)
|
83
|
+
raise "Incorrect type #{r.class} returned by #{@query}"
|
84
|
+
end
|
85
|
+
r
|
86
|
+
end
|
87
|
+
|
66
88
|
# Delete all facts that match the query.
|
67
89
|
# @return [Integer] Total number of facts deleted
|
68
90
|
def delete!
|
data/lib/factbase/rules.rb
CHANGED
@@ -126,8 +126,12 @@ class Factbase::Rules
|
|
126
126
|
@check = check
|
127
127
|
end
|
128
128
|
|
129
|
-
def
|
130
|
-
|
129
|
+
def one(params = {})
|
130
|
+
@query.one(params)
|
131
|
+
end
|
132
|
+
|
133
|
+
def each(params = {})
|
134
|
+
return to_enum(__method__, params) unless block_given?
|
131
135
|
@query.each do |f|
|
132
136
|
yield Fact.new(f, @check)
|
133
137
|
end
|
data/lib/factbase/syntax.rb
CHANGED
@@ -44,7 +44,7 @@ class Factbase::Syntax
|
|
44
44
|
def to_term
|
45
45
|
build.simplify
|
46
46
|
rescue StandardError => e
|
47
|
-
err = "#{e.message} in #{@query}"
|
47
|
+
err = "#{e.message} in \"#{@query}\""
|
48
48
|
err = "#{err}, tokens: #{@tokens}" unless @tokens.nil?
|
49
49
|
raise err
|
50
50
|
end
|
@@ -142,7 +142,7 @@ class Factbase::Syntax
|
|
142
142
|
elsif t.match?(/^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$/)
|
143
143
|
Time.parse(t)
|
144
144
|
else
|
145
|
-
raise "Wrong symbol format (#{t})" unless t.match?(/^[_a-z][a-zA-Z0-9_]*$/)
|
145
|
+
raise "Wrong symbol format (#{t})" unless t.match?(/^[_a-z\$][a-zA-Z0-9_]*$/)
|
146
146
|
t.to_sym
|
147
147
|
end
|
148
148
|
end
|
data/lib/factbase/tee.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright (c) 2024 Yegor Bugayenko
|
4
|
+
#
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
# of this software and associated documentation files (the 'Software'), to deal
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
10
|
+
# furnished to do so, subject to the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be included in all
|
13
|
+
# copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
# SOFTWARE.
|
22
|
+
|
23
|
+
require_relative '../factbase'
|
24
|
+
|
25
|
+
# Tee of two facts.
|
26
|
+
#
|
27
|
+
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
28
|
+
# Copyright:: Copyright (c) 2024 Yegor Bugayenko
|
29
|
+
# License:: MIT
|
30
|
+
class Factbase::Tee
|
31
|
+
# Ctor.
|
32
|
+
# @param [Factbase::Fact] fact Primary fact to use for reading
|
33
|
+
# @param [Factbase::Fact] upper Fact to access with a "$" prefix
|
34
|
+
def initialize(fact, upper)
|
35
|
+
@fact = fact
|
36
|
+
@upper = upper
|
37
|
+
end
|
38
|
+
|
39
|
+
def to_s
|
40
|
+
@fact.to_s
|
41
|
+
end
|
42
|
+
|
43
|
+
def method_missing(*args)
|
44
|
+
return @fact.method_missing(*args) unless args[0].to_s == '[]' && args[1].to_s.start_with?('$')
|
45
|
+
n = args[1].to_s
|
46
|
+
n = n[1..] unless @upper.is_a?(Factbase::Tee)
|
47
|
+
@upper[n]
|
48
|
+
end
|
49
|
+
|
50
|
+
# rubocop:disable Style/OptionalBooleanParameter
|
51
|
+
def respond_to?(_method, _include_private = false)
|
52
|
+
# rubocop:enable Style/OptionalBooleanParameter
|
53
|
+
true
|
54
|
+
end
|
55
|
+
|
56
|
+
def respond_to_missing?(_method, _include_private = false)
|
57
|
+
true
|
58
|
+
end
|
59
|
+
end
|