factbase 0.9.3 → 0.9.4
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/.rubocop.yml +2 -3
- data/README.md +24 -24
- data/fixtures/stories/and.yml +19 -0
- data/fixtures/stories/empty.yml +21 -0
- data/fixtures/stories/one.yml +14 -0
- data/fixtures/stories/unique.yml +16 -0
- data/lib/factbase/cached/cached_query.rb +7 -7
- data/lib/factbase/indexed/indexed_query.rb +1 -1
- data/lib/factbase/indexed/indexed_term.rb +70 -16
- data/lib/factbase/logged.rb +4 -4
- data/lib/factbase/taped.rb +36 -6
- data/lib/factbase/term.rb +3 -0
- data/lib/factbase/terms/aggregates.rb +6 -1
- data/lib/factbase/terms/meta.rb +2 -0
- data/lib/factbase.rb +1 -1
- data/test/factbase/cached/test_cached_factbase.rb +13 -0
- data/test/factbase/indexed/test_indexed_fact.rb +23 -0
- data/test/factbase/indexed/test_indexed_factbase.rb +44 -0
- data/test/factbase/indexed/test_indexed_query.rb +9 -0
- data/test/factbase/indexed/test_indexed_term.rb +92 -0
- data/test/factbase/terms/test_aggregates.rb +10 -0
- data/test/factbase/test_logged.rb +1 -1
- data/test/factbase/test_query.rb +57 -0
- data/test/factbase/test_taped.rb +14 -0
- metadata +9 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 39eb84c924fd5a4c42a73f3a9cc077d3d48d2973d513602c81b7cebb2ed9c35f
|
4
|
+
data.tar.gz: 9187cac9070ee7444a0849eef2a48ce78118840bc53c2e1e01f642924c69ea91
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c27c52ab2a3c7e9e26b2964d6cecb504249d9d3be4ac1206367dbc885d9e7892671f89c37c681218818849189ccf672348b4478c7c93fa7117883e0e49cd63a4
|
7
|
+
data.tar.gz: b1a7b747bf06380b76369e73bed096a7805f100872809b61f12d3dcf6bec60871e69a822da739b961b62c4d6d3542d68229a403522f24121cee2b8b93765e6c6
|
data/.rubocop.yml
CHANGED
@@ -29,9 +29,9 @@ Metrics/AbcSize:
|
|
29
29
|
Metrics/BlockLength:
|
30
30
|
Max: 50
|
31
31
|
Metrics/CyclomaticComplexity:
|
32
|
-
Max:
|
32
|
+
Max: 40
|
33
33
|
Metrics/PerceivedComplexity:
|
34
|
-
Max:
|
34
|
+
Max: 50
|
35
35
|
Metrics/ClassLength:
|
36
36
|
Enabled: false
|
37
37
|
Layout/EmptyLineAfterGuardClause:
|
@@ -44,4 +44,3 @@ Security/MarshalLoad:
|
|
44
44
|
Enabled: false
|
45
45
|
Layout/MultilineAssignmentLayout:
|
46
46
|
Enabled: true
|
47
|
-
require: []
|
data/README.md
CHANGED
@@ -209,33 +209,33 @@ This is the result of the benchmark:
|
|
209
209
|
<!-- benchmark_begin -->
|
210
210
|
```text
|
211
211
|
user system total real
|
212
|
-
insert 20000 facts 0.
|
213
|
-
export 20000 facts 0.
|
214
|
-
import
|
215
|
-
insert 10 facts 0.
|
216
|
-
query 10 times w/txn
|
217
|
-
query 10 times w/o txn 0.
|
218
|
-
modify 10 attrs w/txn 1.
|
219
|
-
delete 10 facts w/txn 0.
|
220
|
-
(and (eq what 'issue-was-closed') (exists... -> 200
|
221
|
-
(and (eq what 'issue-was-closed') (exists... -> 200/txn
|
222
|
-
(and (eq what 'issue-was-closed') (exists... -> zero
|
223
|
-
(and (eq what 'issue-was-closed') (exists... -> zero/txn
|
224
|
-
(gt time '2024-03-23T03:21:43Z') 0.
|
225
|
-
(gt cost 50) 0.
|
226
|
-
(eq title 'Object Thinking 5000') 0.
|
227
|
-
(and (eq foo 42.998) (or (gt bar 200) (absent zzz))) 0.
|
228
|
-
(eq id (agg (always) (max id))) 0.
|
229
|
-
(join "c<=cost,b<=bar" (eq id (agg (always) (max id))))
|
230
|
-
delete! 0.
|
231
|
-
Taped.append() x50000 0.
|
232
|
-
Taped.each() x125 1.
|
233
|
-
Taped.delete_if() x375 0.
|
212
|
+
insert 20000 facts 0.571788 0.003758 0.575546 ( 0.575570)
|
213
|
+
export 20000 facts 0.020920 0.002996 0.023916 ( 0.023920)
|
214
|
+
import 410695 bytes (20000 facts) 0.027100 0.007016 0.034116 ( 0.034121)
|
215
|
+
insert 10 facts 0.041616 0.004054 0.045670 ( 0.045682)
|
216
|
+
query 10 times w/txn 1.631523 0.024886 1.656409 ( 1.656459)
|
217
|
+
query 10 times w/o txn 0.108811 0.000990 0.109801 ( 0.109804)
|
218
|
+
modify 10 attrs w/txn 1.207344 0.019000 1.226344 ( 1.226358)
|
219
|
+
delete 10 facts w/txn 0.734874 0.001988 0.736862 ( 0.736890)
|
220
|
+
(and (eq what 'issue-was-closed') (exists... -> 200 5.046822 0.008973 5.055795 ( 5.056175)
|
221
|
+
(and (eq what 'issue-was-closed') (exists... -> 200/txn 5.006788 0.009980 5.016768 ( 5.017401)
|
222
|
+
(and (eq what 'issue-was-closed') (exists... -> zero 7.793528 0.003992 7.797520 ( 7.797774)
|
223
|
+
(and (eq what 'issue-was-closed') (exists... -> zero/txn 7.741986 0.001995 7.743981 ( 7.744395)
|
224
|
+
(gt time '2024-03-23T03:21:43Z') 0.084850 0.000000 0.084850 ( 0.084850)
|
225
|
+
(gt cost 50) 0.084610 0.001002 0.085612 ( 0.085614)
|
226
|
+
(eq title 'Object Thinking 5000') 0.002526 0.000000 0.002526 ( 0.002526)
|
227
|
+
(and (eq foo 42.998) (or (gt bar 200) (absent zzz))) 0.163174 0.000000 0.163174 ( 0.163207)
|
228
|
+
(eq id (agg (always) (max id))) 0.247507 0.001001 0.248508 ( 0.248508)
|
229
|
+
(join "c<=cost,b<=bar" (eq id (agg (always) (max id)))) 0.622045 0.004995 0.627040 ( 0.627095)
|
230
|
+
delete! 0.061154 0.000001 0.061155 ( 0.061156)
|
231
|
+
Taped.append() x50000 0.023736 0.003999 0.027735 ( 0.027739)
|
232
|
+
Taped.each() x125 1.350286 0.000002 1.350288 ( 1.350416)
|
233
|
+
Taped.delete_if() x375 0.803790 0.000000 0.803790 ( 0.803852)
|
234
234
|
```
|
235
235
|
|
236
236
|
The results were calculated in [this GHA job][benchmark-gha]
|
237
|
-
on 2025-03-
|
237
|
+
on 2025-03-18 at 12:45,
|
238
238
|
on Linux with 4 CPUs.
|
239
239
|
<!-- benchmark_end -->
|
240
240
|
|
241
|
-
[benchmark-gha]: https://github.com/yegor256/factbase/actions/runs/
|
241
|
+
[benchmark-gha]: https://github.com/yegor256/factbase/actions/runs/13923519605
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2024-2025 Yegor Bugayenko
|
2
|
+
# SPDX-License-Identifier: MIT
|
3
|
+
---
|
4
|
+
# yamllint disable rule:line-length
|
5
|
+
facts:
|
6
|
+
- name: Jeff
|
7
|
+
age: 41
|
8
|
+
movie: Big Lebowski
|
9
|
+
- name: Walter
|
10
|
+
age: 43
|
11
|
+
friend: Donny
|
12
|
+
movie: Big Lebowski
|
13
|
+
queries:
|
14
|
+
- query: (and (eq movie "Big Lebowski") (exists age) (not (exists friend)))
|
15
|
+
size: 1
|
16
|
+
- query: (and (exists age) (not (exists foo)))
|
17
|
+
size: 2
|
18
|
+
- query: (and (exists age) (exists foo))
|
19
|
+
size: 0
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2024-2025 Yegor Bugayenko
|
2
|
+
# SPDX-License-Identifier: MIT
|
3
|
+
---
|
4
|
+
# yamllint disable rule:line-length
|
5
|
+
facts:
|
6
|
+
- man: Jeff
|
7
|
+
wife: Sarah
|
8
|
+
- man: Walter
|
9
|
+
wife: Lucy
|
10
|
+
- man: Donny
|
11
|
+
wife: Nicole
|
12
|
+
- man: Peter
|
13
|
+
wife: Dorah
|
14
|
+
- woman: Lucy
|
15
|
+
- woman: Nicole
|
16
|
+
- woman: Dorah
|
17
|
+
queries:
|
18
|
+
- query: (and (exists man) (not (empty (and (exists woman) (eq woman $wife)))))
|
19
|
+
size: 3
|
20
|
+
- query: (and (exists man) (empty (and (exists woman) (eq woman $wife))))
|
21
|
+
size: 1
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2024-2025 Yegor Bugayenko
|
2
|
+
# SPDX-License-Identifier: MIT
|
3
|
+
---
|
4
|
+
# yamllint disable rule:line-length
|
5
|
+
facts:
|
6
|
+
- name: Jeff
|
7
|
+
friends: [Walter, Donny, Maude]
|
8
|
+
- name: Walter
|
9
|
+
friends: Donny
|
10
|
+
- name: Donny
|
11
|
+
friends: Walter
|
12
|
+
queries:
|
13
|
+
- query: (one friends)
|
14
|
+
size: 2
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2024-2025 Yegor Bugayenko
|
2
|
+
# SPDX-License-Identifier: MIT
|
3
|
+
---
|
4
|
+
# yamllint disable rule:line-length
|
5
|
+
facts:
|
6
|
+
- name: Volvo
|
7
|
+
- name: Ford
|
8
|
+
- name: Toyota
|
9
|
+
price: 5900
|
10
|
+
- name: Toyota
|
11
|
+
price: 4300
|
12
|
+
- name: BMW
|
13
|
+
price: 4400
|
14
|
+
queries:
|
15
|
+
- query: (and (exists price) (unique name))
|
16
|
+
size: 2
|
@@ -4,6 +4,7 @@
|
|
4
4
|
# SPDX-License-Identifier: MIT
|
5
5
|
|
6
6
|
require_relative '../../factbase'
|
7
|
+
require_relative 'cached_fact'
|
7
8
|
|
8
9
|
# Query with a cache, a decorator of another query.
|
9
10
|
#
|
@@ -30,13 +31,12 @@ class Factbase::CachedQuery
|
|
30
31
|
# @param [Hash] params Optional params accessible in the query via the "$" symbol
|
31
32
|
# @yield [Fact] Facts one-by-one
|
32
33
|
# @return [Integer] Total number of facts yielded
|
33
|
-
def each(fb = @fb, params =
|
34
|
+
def each(fb = @fb, params = {})
|
34
35
|
return to_enum(__method__, fb, params) unless block_given?
|
35
|
-
key = "each #{@origin}"
|
36
|
+
key = "each #{@origin}" # params are ignored!
|
36
37
|
before = @cache[key]
|
37
|
-
@cache[key] = @origin.each(fb).to_a if before.nil?
|
38
|
+
@cache[key] = @origin.each(fb, params).to_a if before.nil?
|
38
39
|
@cache[key].each do |f|
|
39
|
-
require_relative 'cached_fact'
|
40
40
|
yield Factbase::CachedFact.new(f, @cache)
|
41
41
|
end
|
42
42
|
end
|
@@ -45,10 +45,10 @@ class Factbase::CachedQuery
|
|
45
45
|
# @param [Hash] fb The factbase
|
46
46
|
# @param [Hash] _params Optional params accessible in the query via the "$" symbol (unused)
|
47
47
|
# @return The value evaluated
|
48
|
-
def one(fb = @fb,
|
49
|
-
key = "one: #{@origin}"
|
48
|
+
def one(fb = @fb, params = {})
|
49
|
+
key = "one: #{@origin} #{params}"
|
50
50
|
before = @cache[key]
|
51
|
-
@cache[key] = @origin.one(fb) if before.nil?
|
51
|
+
@cache[key] = @origin.one(fb, params) if before.nil?
|
52
52
|
@cache[key]
|
53
53
|
end
|
54
54
|
|
@@ -33,7 +33,7 @@ class Factbase::IndexedQuery
|
|
33
33
|
# @return [Integer] Total number of facts yielded
|
34
34
|
def each(fb = @fb, params = {})
|
35
35
|
return to_enum(__method__, fb, params) unless block_given?
|
36
|
-
@origin.each(fb, params) do |f|
|
36
|
+
@origin.each(fb, params).to_a.each do |f|
|
37
37
|
yield Factbase::IndexedFact.new(f, @idx)
|
38
38
|
end
|
39
39
|
end
|
@@ -3,6 +3,7 @@
|
|
3
3
|
# SPDX-FileCopyrightText: Copyright (c) 2024-2025 Yegor Bugayenko
|
4
4
|
# SPDX-License-Identifier: MIT
|
5
5
|
|
6
|
+
require 'tago'
|
6
7
|
require_relative '../../factbase'
|
7
8
|
|
8
9
|
# Term with an index.
|
@@ -11,15 +12,43 @@ require_relative '../../factbase'
|
|
11
12
|
# Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
|
12
13
|
# License:: MIT
|
13
14
|
module Factbase::IndexedTerm
|
15
|
+
# Reduces the provided list of facts (maps) to a smaller array, if it's possible.
|
16
|
+
#
|
17
|
+
# NIL must be returned if indexing is prohibited in this case.
|
18
|
+
#
|
19
|
+
# @param [Array<Hash>] maps Array of facts
|
20
|
+
# @param [Hash] params Key/value params to use
|
21
|
+
# @return [Array<Hash>|nil] Returns a new array, or NIL if the original array must be used
|
14
22
|
def predict(maps, params)
|
15
23
|
case @op
|
24
|
+
when :one
|
25
|
+
key = [maps.object_id, @operands.first, @op]
|
26
|
+
if @idx[key].nil?
|
27
|
+
@idx[key] = []
|
28
|
+
prop = @operands.first.to_s
|
29
|
+
maps.to_a.each do |m|
|
30
|
+
@idx[key].append(m) if !m[prop].nil? && m[prop].size == 1
|
31
|
+
end
|
32
|
+
end
|
33
|
+
(maps & []) | @idx[key]
|
34
|
+
when :exists
|
35
|
+
key = [maps.object_id, @operands.first, @op]
|
36
|
+
if @idx[key].nil?
|
37
|
+
@idx[key] = []
|
38
|
+
prop = @operands.first.to_s
|
39
|
+
maps.to_a.each do |m|
|
40
|
+
@idx[key].append(m) unless m[prop].nil?
|
41
|
+
end
|
42
|
+
end
|
43
|
+
(maps & []) | @idx[key]
|
16
44
|
when :eq
|
17
|
-
if @operands
|
18
|
-
key = [maps.object_id, @operands
|
45
|
+
if @operands.first.is_a?(Symbol) && _scalar?(@operands[1])
|
46
|
+
key = [maps.object_id, @operands.first, @op]
|
19
47
|
if @idx[key].nil?
|
20
48
|
@idx[key] = {}
|
49
|
+
prop = @operands.first.to_s
|
21
50
|
maps.to_a.each do |m|
|
22
|
-
m[
|
51
|
+
m[prop]&.each do |v|
|
23
52
|
@idx[key][v] = [] if @idx[key][v].nil?
|
24
53
|
@idx[key][v].append(m)
|
25
54
|
end
|
@@ -32,23 +61,48 @@ module Factbase::IndexedTerm
|
|
32
61
|
else
|
33
62
|
[@operands[1]]
|
34
63
|
end
|
35
|
-
vv.
|
36
|
-
|
37
|
-
|
64
|
+
if vv.empty?
|
65
|
+
nil
|
66
|
+
else
|
67
|
+
j = vv.map { |v| @idx[key][v] || [] }.reduce(&:|)
|
68
|
+
(maps & []) | j
|
69
|
+
end
|
38
70
|
end
|
39
71
|
when :and
|
40
|
-
|
41
|
-
|
42
|
-
maps
|
43
|
-
|
44
|
-
|
72
|
+
r = nil
|
73
|
+
@operands.each do |o|
|
74
|
+
n = o.predict(maps, params)
|
75
|
+
if n.nil?
|
76
|
+
r = nil
|
77
|
+
break
|
78
|
+
end
|
79
|
+
if r.nil?
|
80
|
+
r = n
|
81
|
+
else
|
82
|
+
r &= n
|
83
|
+
end
|
84
|
+
break if r.empty?
|
45
85
|
end
|
86
|
+
r
|
46
87
|
when :or
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
88
|
+
r = nil
|
89
|
+
@operands.each do |o|
|
90
|
+
n = o.predict(maps, params)
|
91
|
+
if n.nil?
|
92
|
+
r = nil
|
93
|
+
break
|
94
|
+
end
|
95
|
+
r = maps & [] if r.nil?
|
96
|
+
r |= n
|
97
|
+
end
|
98
|
+
r
|
99
|
+
when :not
|
100
|
+
r = @operands.first.predict(maps, params)
|
101
|
+
if r.nil?
|
102
|
+
nil
|
103
|
+
else
|
104
|
+
(maps & []) | (maps.to_a - r.to_a)
|
105
|
+
end
|
52
106
|
end
|
53
107
|
end
|
54
108
|
|
data/lib/factbase/logged.rb
CHANGED
@@ -143,9 +143,9 @@ class Factbase::Logged
|
|
143
143
|
end
|
144
144
|
raise ".each of #{@term.class} returned #{r.class}" unless r.is_a?(Integer)
|
145
145
|
if r.zero?
|
146
|
-
@tube.say(start, "
|
146
|
+
@tube.say(start, "Zero/#{@fb.size} facts found by '#{q}' #{tail}")
|
147
147
|
else
|
148
|
-
@tube.say(start, "Found #{r} fact(s) by '#{q}' #{tail}")
|
148
|
+
@tube.say(start, "Found #{r}/#{@fb.size} fact(s) by '#{q}' #{tail}")
|
149
149
|
end
|
150
150
|
r
|
151
151
|
else
|
@@ -157,9 +157,9 @@ class Factbase::Logged
|
|
157
157
|
end
|
158
158
|
end
|
159
159
|
if array.empty?
|
160
|
-
@tube.say(start, "
|
160
|
+
@tube.say(start, "Zero/#{@fb.size} found by '#{q}' #{tail}")
|
161
161
|
else
|
162
|
-
@tube.say(start, "Found #{array.size} fact(s) by '#{q}' #{tail}")
|
162
|
+
@tube.say(start, "Found #{array.size}/#{@fb.size} fact(s) by '#{q}' #{tail}")
|
163
163
|
end
|
164
164
|
array
|
165
165
|
end
|
data/lib/factbase/taped.rb
CHANGED
@@ -12,11 +12,11 @@ require_relative '../factbase'
|
|
12
12
|
# Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
|
13
13
|
# License:: MIT
|
14
14
|
class Factbase::Taped
|
15
|
-
def initialize(origin)
|
15
|
+
def initialize(origin, inserted: [], deleted: [], added: [])
|
16
16
|
@origin = origin
|
17
|
-
@inserted =
|
18
|
-
@deleted =
|
19
|
-
@added =
|
17
|
+
@inserted = inserted
|
18
|
+
@deleted = deleted
|
19
|
+
@added = added
|
20
20
|
end
|
21
21
|
|
22
22
|
def inserted
|
@@ -39,6 +39,10 @@ class Factbase::Taped
|
|
39
39
|
@origin.size
|
40
40
|
end
|
41
41
|
|
42
|
+
def empty?
|
43
|
+
@origin.empty?
|
44
|
+
end
|
45
|
+
|
42
46
|
def <<(map)
|
43
47
|
@origin << (map)
|
44
48
|
@inserted.append(map.object_id)
|
@@ -63,8 +67,13 @@ class Factbase::Taped
|
|
63
67
|
@origin.to_a
|
64
68
|
end
|
65
69
|
|
66
|
-
def
|
67
|
-
|
70
|
+
def &(other)
|
71
|
+
return Factbase::Taped.new([], inserted: @inserted, deleted: @deleted, added: @added) if other == []
|
72
|
+
join(other, &:&)
|
73
|
+
end
|
74
|
+
|
75
|
+
def |(other)
|
76
|
+
join(other, &:|)
|
68
77
|
end
|
69
78
|
|
70
79
|
# Decorator of Hash.
|
@@ -129,4 +138,25 @@ class Factbase::Taped
|
|
129
138
|
@origin.uniq!
|
130
139
|
end
|
131
140
|
end
|
141
|
+
|
142
|
+
private
|
143
|
+
|
144
|
+
def join(other)
|
145
|
+
n = yield @origin.to_a, other.to_a
|
146
|
+
if other.respond_to?(:inserted)
|
147
|
+
Factbase::Taped.new(
|
148
|
+
n,
|
149
|
+
inserted: @inserted | other.inserted,
|
150
|
+
deleted: @deleted | other.deleted,
|
151
|
+
added: @added | other.added
|
152
|
+
)
|
153
|
+
else
|
154
|
+
Factbase::Taped.new(
|
155
|
+
n,
|
156
|
+
inserted: @inserted,
|
157
|
+
deleted: @deleted,
|
158
|
+
added: @added
|
159
|
+
)
|
160
|
+
end
|
161
|
+
end
|
132
162
|
end
|
data/lib/factbase/term.rb
CHANGED
@@ -85,6 +85,9 @@ class Factbase::Term
|
|
85
85
|
@operands = operands
|
86
86
|
end
|
87
87
|
|
88
|
+
# Extend it with the module.
|
89
|
+
# @param [Module] type The type to extend with
|
90
|
+
# @param [Hash] args Attributes to set
|
88
91
|
def redress!(type, **args)
|
89
92
|
extend type
|
90
93
|
args.each { |k, v| send(:instance_variable_set, :"@#{k}", v) }
|
@@ -74,7 +74,12 @@ module Factbase::Aggregates
|
|
74
74
|
assert_args(1)
|
75
75
|
term = @operands[0]
|
76
76
|
raise "A term expected, but '#{term}' provided" unless term.is_a?(Factbase::Term)
|
77
|
-
|
77
|
+
# rubocop:disable Lint/UnreachableLoop
|
78
|
+
fb.query(term, maps).each(fb, fact) do
|
79
|
+
return false
|
80
|
+
end
|
81
|
+
# rubocop:enable Lint/UnreachableLoop
|
82
|
+
true
|
78
83
|
end
|
79
84
|
|
80
85
|
def _best(maps)
|
data/lib/factbase/terms/meta.rb
CHANGED
@@ -42,12 +42,14 @@ module Factbase::Meta
|
|
42
42
|
_values(0, fact, maps, fb).nil?
|
43
43
|
end
|
44
44
|
|
45
|
+
# The property has many (more than one) values.
|
45
46
|
def many(fact, maps, fb)
|
46
47
|
assert_args(1)
|
47
48
|
v = _values(0, fact, maps, fb)
|
48
49
|
!v.nil? && v.size > 1
|
49
50
|
end
|
50
51
|
|
52
|
+
# The property has exactly one value.
|
51
53
|
def one(fact, maps, fb)
|
52
54
|
assert_args(1)
|
53
55
|
v = _values(0, fact, maps, fb)
|
data/lib/factbase.rb
CHANGED
@@ -82,7 +82,7 @@ require 'yaml'
|
|
82
82
|
# License:: MIT
|
83
83
|
class Factbase
|
84
84
|
# Current version of the gem (changed by .rultor.yml on every release)
|
85
|
-
VERSION = '0.9.
|
85
|
+
VERSION = '0.9.4' unless const_defined?(:VERSION)
|
86
86
|
|
87
87
|
# An exception that may be thrown in a transaction, to roll it back.
|
88
88
|
class Rollback < StandardError; end
|
@@ -19,4 +19,17 @@ class TestCachedFactbase < Factbase::Test
|
|
19
19
|
f.bar = 'test'
|
20
20
|
assert_equal(1, fb.query('(and (eq foo 1) (eq bar "test"))').each.to_a.size)
|
21
21
|
end
|
22
|
+
|
23
|
+
def test_queries_after_update_in_txn
|
24
|
+
origin = Factbase.new
|
25
|
+
fb = Factbase::CachedFactbase.new(origin)
|
26
|
+
fb.insert.foo = 42
|
27
|
+
fb.txn do |fbt|
|
28
|
+
fbt.query('(exists foo)').each do |f|
|
29
|
+
f.bar = 33
|
30
|
+
end
|
31
|
+
end
|
32
|
+
refute_empty(origin.query('(exists bar)').each.to_a)
|
33
|
+
refute_empty(fb.query('(exists bar)').each.to_a)
|
34
|
+
end
|
22
35
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) 2024-2025 Yegor Bugayenko
|
4
|
+
# SPDX-License-Identifier: MIT
|
5
|
+
|
6
|
+
require_relative '../../test__helper'
|
7
|
+
require_relative '../../../lib/factbase'
|
8
|
+
require_relative '../../../lib/factbase/fact'
|
9
|
+
require_relative '../../../lib/factbase/indexed/indexed_fact'
|
10
|
+
|
11
|
+
# Fact test.
|
12
|
+
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
13
|
+
# Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
|
14
|
+
# License:: MIT
|
15
|
+
class TestIndexedFact < Factbase::Test
|
16
|
+
def test_updates_origin
|
17
|
+
origin = Factbase::Fact.new({})
|
18
|
+
fact = Factbase::IndexedFact.new(origin, {})
|
19
|
+
fact.foo = 42
|
20
|
+
refute_nil(origin['foo'])
|
21
|
+
assert_equal(42, origin.foo)
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) 2024-2025 Yegor Bugayenko
|
4
|
+
# SPDX-License-Identifier: MIT
|
5
|
+
|
6
|
+
require_relative '../../test__helper'
|
7
|
+
require_relative '../../../lib/factbase'
|
8
|
+
require_relative '../../../lib/factbase/indexed/indexed_factbase'
|
9
|
+
|
10
|
+
# Factbase test.
|
11
|
+
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
12
|
+
# Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
|
13
|
+
# License:: MIT
|
14
|
+
class TestIndexedFactbase < Factbase::Test
|
15
|
+
def test_queries_after_update
|
16
|
+
origin = Factbase.new
|
17
|
+
fb = Factbase::IndexedFactbase.new(origin)
|
18
|
+
fb.insert.foo = 42
|
19
|
+
fb.query('(exists foo)').each do |f|
|
20
|
+
f.bar = 33
|
21
|
+
end
|
22
|
+
refute_empty(origin.query('(exists bar)').each.to_a)
|
23
|
+
refute_empty(fb.query('(exists bar)').each.to_a)
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_queries_after_update_in_txn
|
27
|
+
origin = Factbase.new
|
28
|
+
fb = Factbase::IndexedFactbase.new(origin)
|
29
|
+
fb.insert.foo = 42
|
30
|
+
fb.txn do |fbt|
|
31
|
+
fbt.query('(exists foo)').each do |f|
|
32
|
+
f.bar = f.foo + 1
|
33
|
+
end
|
34
|
+
end
|
35
|
+
refute_empty(origin.query('(exists bar)').each.to_a)
|
36
|
+
refute_empty(fb.query('(exists bar)').each.to_a)
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_queries_after_insert_in_txn
|
40
|
+
fb = Factbase::IndexedFactbase.new(Factbase.new)
|
41
|
+
fb.txn(&:insert)
|
42
|
+
refute_empty(fb.query('(always)').each.to_a)
|
43
|
+
end
|
44
|
+
end
|
@@ -13,6 +13,15 @@ require_relative '../../../lib/factbase/indexed/indexed_factbase'
|
|
13
13
|
# Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
|
14
14
|
# License:: MIT
|
15
15
|
class TestIndexedQuery < Factbase::Test
|
16
|
+
def test_queries_and_updates_origin
|
17
|
+
fb = Factbase.new
|
18
|
+
fb.insert.foo = 42
|
19
|
+
Factbase::IndexedQuery.new(fb.query('(exists foo)'), {}, fb).each do |f|
|
20
|
+
f.bar = 33
|
21
|
+
end
|
22
|
+
refute_empty(fb.query('(exists bar)').each.to_a)
|
23
|
+
end
|
24
|
+
|
16
25
|
def test_queries_many_times
|
17
26
|
fb = Factbase::IndexedFactbase.new(Factbase.new)
|
18
27
|
total = 5
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) 2024-2025 Yegor Bugayenko
|
4
|
+
# SPDX-License-Identifier: MIT
|
5
|
+
|
6
|
+
require_relative '../../test__helper'
|
7
|
+
require_relative '../../../lib/factbase'
|
8
|
+
require_relative '../../../lib/factbase/term'
|
9
|
+
require_relative '../../../lib/factbase/taped'
|
10
|
+
require_relative '../../../lib/factbase/indexed/indexed_term'
|
11
|
+
|
12
|
+
# Term test.
|
13
|
+
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
14
|
+
# Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
|
15
|
+
# License:: MIT
|
16
|
+
class TestIndexedTerm < Factbase::Test
|
17
|
+
def test_predicts_on_eq
|
18
|
+
term = Factbase::Term.new(:eq, [:foo, 42])
|
19
|
+
idx = {}
|
20
|
+
term.redress!(Factbase::IndexedTerm, idx:)
|
21
|
+
maps = Factbase::Taped.new([{ 'foo' => [42] }, { 'bar' => [7] }, { 'foo' => [22, 42] }, { 'foo' => [] }])
|
22
|
+
n = term.predict(maps, { a: 1 })
|
23
|
+
assert_equal(2, n.size)
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_predicts_on_exists
|
27
|
+
term = Factbase::Term.new(:exists, [:foo])
|
28
|
+
idx = {}
|
29
|
+
term.redress!(Factbase::IndexedTerm, idx:)
|
30
|
+
maps = Factbase::Taped.new([{ 'foo' => [42] }, { 'bar' => [7] }, { 'foo' => [22, 42] }, { 'foo' => [] }])
|
31
|
+
n = term.predict(maps, { a: 1 })
|
32
|
+
assert_equal(3, n.size)
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_predicts_on_one
|
36
|
+
term = Factbase::Term.new(:one, [:foo])
|
37
|
+
idx = {}
|
38
|
+
term.redress!(Factbase::IndexedTerm, idx:)
|
39
|
+
maps = Factbase::Taped.new([{ 'foo' => [42] }, { 'bar' => [7] }, { 'foo' => [22, 42] }, { 'foo' => [] }])
|
40
|
+
n = term.predict(maps, { a: 1 })
|
41
|
+
assert_equal(1, n.size)
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_predicts_on_not
|
45
|
+
term = Factbase::Term.new(:not, [Factbase::Term.new(:eq, [:foo, 42])])
|
46
|
+
idx = {}
|
47
|
+
term.redress!(Factbase::IndexedTerm, idx:)
|
48
|
+
maps = Factbase::Taped.new([{ 'foo' => [42] }, { 'bar' => [7], 'foo' => [22, 42] }, { 'foo' => [22] }])
|
49
|
+
n = term.predict(maps, { a: 1 })
|
50
|
+
assert_equal(1, n.size)
|
51
|
+
end
|
52
|
+
|
53
|
+
def test_predicts_on_and
|
54
|
+
term = Factbase::Term.new(
|
55
|
+
:and,
|
56
|
+
[
|
57
|
+
Factbase::Term.new(:eq, [:foo, 42]),
|
58
|
+
Factbase::Term.new(:eq, [:bar, 7])
|
59
|
+
]
|
60
|
+
)
|
61
|
+
idx = {}
|
62
|
+
term.redress!(Factbase::IndexedTerm, idx:)
|
63
|
+
maps = Factbase::Taped.new([{ 'foo' => [42] }, { 'bar' => [7], 'foo' => [22, 42] }, { 'foo' => [22, 42] }])
|
64
|
+
n = term.predict(maps, { a: 1 })
|
65
|
+
assert_equal(1, n.size)
|
66
|
+
end
|
67
|
+
|
68
|
+
def test_predicts_on_or
|
69
|
+
term = Factbase::Term.new(
|
70
|
+
:or,
|
71
|
+
[
|
72
|
+
Factbase::Term.new(:exists, [:bar]),
|
73
|
+
Factbase::Term.new(:eq, [:foo, 42]),
|
74
|
+
Factbase::Term.new(:eq, [:bar, 7])
|
75
|
+
]
|
76
|
+
)
|
77
|
+
idx = {}
|
78
|
+
term.redress!(Factbase::IndexedTerm, idx:)
|
79
|
+
maps = Factbase::Taped.new([{ 'foo' => [42] }, { 'bar' => [7], 'foo' => [22, 42] }, { 'foo' => [22, 42] }])
|
80
|
+
n = term.predict(maps, { a: 1 })
|
81
|
+
assert_equal(3, n.size)
|
82
|
+
end
|
83
|
+
|
84
|
+
def test_predicts_on_others
|
85
|
+
term = Factbase::Term.new(:boom, [])
|
86
|
+
idx = {}
|
87
|
+
term.redress!(Factbase::IndexedTerm, idx:)
|
88
|
+
maps = Factbase::Taped.new([{ 'foo' => [42] }, { 'alpha' => [] }, {}])
|
89
|
+
n = term.predict(maps, { a: 1 })
|
90
|
+
assert_nil(n)
|
91
|
+
end
|
92
|
+
end
|
@@ -50,4 +50,14 @@ class TestAggregates < Factbase::Test
|
|
50
50
|
assert_equal(r, t.evaluate(Factbase::Fact.new({}), maps, Factbase.new), q)
|
51
51
|
end
|
52
52
|
end
|
53
|
+
|
54
|
+
def test_empty_with_params
|
55
|
+
maps = [
|
56
|
+
{ 'a' => [3], 'b' => [44] },
|
57
|
+
{ 'a' => [4], 'b' => [55] }
|
58
|
+
]
|
59
|
+
t = Factbase::Syntax.new('(empty (eq b $x))').to_term
|
60
|
+
assert(t.evaluate(Factbase::Fact.new({ 'x' => 42 }), maps, Factbase.new))
|
61
|
+
refute(t.evaluate(Factbase::Fact.new({ 'x' => 44 }), maps, Factbase.new))
|
62
|
+
end
|
53
63
|
end
|
@@ -127,7 +127,7 @@ class TestLogged < Factbase::Test
|
|
127
127
|
'Inserted new fact #2',
|
128
128
|
'Set \'bar\' to 3 (Integer)',
|
129
129
|
'Set \'str\' to "Он поскорей звонит. Вбегает\n ... Отъехать в поле к двум дубкам." (String)',
|
130
|
-
'Found 1 fact(s) by \'(exists bar)\'',
|
130
|
+
'Found 1/4 fact(s) by \'(exists bar)\'',
|
131
131
|
'Deleted 3 fact(s) out of 4 by \'(not (exists bar))\''
|
132
132
|
].each do |s|
|
133
133
|
assert_includes(log.to_s, s, "#{log}\n")
|
data/test/factbase/test_query.rb
CHANGED
@@ -188,6 +188,63 @@ class TestQuery < Factbase::Test
|
|
188
188
|
end
|
189
189
|
end
|
190
190
|
|
191
|
+
def test_scans_and_inserts
|
192
|
+
with_factbases do |_, fb|
|
193
|
+
fb.insert.foo = 42
|
194
|
+
before = fb.size
|
195
|
+
more = 0
|
196
|
+
fb.query('(exists foo)').each do |f|
|
197
|
+
fb.insert.bar = f.foo
|
198
|
+
more += 1
|
199
|
+
end
|
200
|
+
assert_equal(before + more, fb.size)
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def test_scans_and_inserts_in_txn
|
205
|
+
with_factbases do |_, fb|
|
206
|
+
fb.insert.foo = 42
|
207
|
+
before = fb.size
|
208
|
+
more = 0
|
209
|
+
fb.query('(exists foo)').each do |f|
|
210
|
+
fb.txn do |fbt|
|
211
|
+
fbt.insert.bar = f.foo
|
212
|
+
more += 1
|
213
|
+
end
|
214
|
+
end
|
215
|
+
assert_equal(before + more, fb.size)
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
def test_scans_and_inserts_in_queried_txn
|
220
|
+
with_factbases do |_, fb|
|
221
|
+
fb.insert.foo = 42
|
222
|
+
before = fb.size
|
223
|
+
more = 0
|
224
|
+
fb.txn do |fbt|
|
225
|
+
fbt.query('(exists foo)').each do |f|
|
226
|
+
fbt.insert.bar = f.foo
|
227
|
+
more += 1
|
228
|
+
end
|
229
|
+
end
|
230
|
+
assert_equal(before + more, fb.size)
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
def test_scans_and_appends_in_queried_txn
|
235
|
+
with_factbases do |badge, fb|
|
236
|
+
fb.insert.foo = 42
|
237
|
+
fb.txn do |fbt|
|
238
|
+
fbt.query('(exists foo)').each do |f|
|
239
|
+
f.bar = 33
|
240
|
+
end
|
241
|
+
refute_empty(fbt.query('(exists bar)').each.to_a)
|
242
|
+
end
|
243
|
+
refute_empty(fb.query('(exists bar)').each.to_a, "in #{badge}")
|
244
|
+
assert_empty(fb.query('(and (exists foo) (not (exists bar)))').each.to_a, "in #{badge}")
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
191
248
|
def test_to_array
|
192
249
|
maps = []
|
193
250
|
maps << { 'foo' => [42] }
|
data/test/factbase/test_taped.rb
CHANGED
@@ -43,4 +43,18 @@ class TestTaped < Factbase::Test
|
|
43
43
|
end
|
44
44
|
assert_equal(1, t.added.size)
|
45
45
|
end
|
46
|
+
|
47
|
+
def test_tracks_factbase
|
48
|
+
t = Factbase::Taped.new([])
|
49
|
+
fb = Factbase.new(t)
|
50
|
+
fb.insert
|
51
|
+
fb.query('(always)').each do |f|
|
52
|
+
f.foo = 42
|
53
|
+
f.foo = 5
|
54
|
+
end
|
55
|
+
fb.query('(always)').delete!
|
56
|
+
assert_equal(1, t.inserted.size)
|
57
|
+
assert_equal(1, t.added.size)
|
58
|
+
assert_equal(1, t.deleted.size)
|
59
|
+
end
|
46
60
|
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: factbase
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.9.
|
4
|
+
version: 0.9.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Yegor Bugayenko
|
8
8
|
bindir: bin
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-03-
|
10
|
+
date: 2025-03-18 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: backtrace
|
@@ -164,17 +164,21 @@ files:
|
|
164
164
|
- factbase.gemspec
|
165
165
|
- fixtures/stories/agg.yml
|
166
166
|
- fixtures/stories/always.yml
|
167
|
+
- fixtures/stories/and.yml
|
167
168
|
- fixtures/stories/as.yml
|
168
169
|
- fixtures/stories/count.yml
|
170
|
+
- fixtures/stories/empty.yml
|
169
171
|
- fixtures/stories/eq.yml
|
170
172
|
- fixtures/stories/gt.yml
|
171
173
|
- fixtures/stories/join.yml
|
172
174
|
- fixtures/stories/max.yml
|
173
175
|
- fixtures/stories/min.yml
|
174
176
|
- fixtures/stories/nth.yml
|
177
|
+
- fixtures/stories/one.yml
|
175
178
|
- fixtures/stories/or.yml
|
176
179
|
- fixtures/stories/sprintf.yml
|
177
180
|
- fixtures/stories/sum.yml
|
181
|
+
- fixtures/stories/unique.yml
|
178
182
|
- lib/factbase.rb
|
179
183
|
- lib/factbase/accum.rb
|
180
184
|
- lib/factbase/cached/cached_fact.rb
|
@@ -218,7 +222,10 @@ files:
|
|
218
222
|
- renovate.json
|
219
223
|
- test/factbase/cached/test_cached_factbase.rb
|
220
224
|
- test/factbase/cached/test_cached_query.rb
|
225
|
+
- test/factbase/indexed/test_indexed_fact.rb
|
226
|
+
- test/factbase/indexed/test_indexed_factbase.rb
|
221
227
|
- test/factbase/indexed/test_indexed_query.rb
|
228
|
+
- test/factbase/indexed/test_indexed_term.rb
|
222
229
|
- test/factbase/sync/test_sync_factbase.rb
|
223
230
|
- test/factbase/sync/test_sync_query.rb
|
224
231
|
- test/factbase/terms/test_aggregates.rb
|