factbase 0.16.8 → 0.17.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/Gemfile +3 -1
- data/Gemfile.lock +26 -19
- data/README.md +28 -27
- data/Rakefile +30 -6
- data/lib/factbase/cached/cached_query.rb +2 -0
- data/lib/factbase/indexed/indexed_absent.rb +25 -0
- data/lib/factbase/indexed/indexed_and.rb +79 -0
- data/lib/factbase/indexed/indexed_eq.rb +46 -0
- data/lib/factbase/indexed/indexed_exists.rb +25 -0
- data/lib/factbase/indexed/indexed_factbase.rb +51 -2
- data/lib/factbase/indexed/indexed_gt.rb +41 -0
- data/lib/factbase/indexed/indexed_lt.rb +41 -0
- data/lib/factbase/indexed/indexed_not.rb +32 -0
- data/lib/factbase/indexed/indexed_one.rb +25 -0
- data/lib/factbase/indexed/indexed_or.rb +28 -0
- data/lib/factbase/indexed/indexed_query.rb +2 -0
- data/lib/factbase/indexed/indexed_term.rb +29 -185
- data/lib/factbase/indexed/indexed_unique.rb +25 -0
- data/lib/factbase/query.rb +3 -1
- data/lib/factbase/sync/sync_factbase.rb +11 -11
- data/lib/factbase/sync/sync_query.rb +7 -8
- data/lib/factbase/term.rb +130 -101
- data/lib/factbase/terms/absent.rb +26 -0
- data/lib/factbase/terms/agg.rb +35 -0
- data/lib/factbase/terms/always.rb +27 -0
- data/lib/factbase/terms/and.rb +35 -0
- data/lib/factbase/terms/arithmetic.rb +55 -0
- data/lib/factbase/terms/as.rb +31 -0
- data/lib/factbase/terms/{debug.rb → assert.rb} +17 -15
- data/lib/factbase/terms/base.rb +17 -0
- data/lib/factbase/terms/best.rb +25 -0
- data/lib/factbase/terms/boolean.rb +28 -0
- data/lib/factbase/terms/compare.rb +38 -0
- data/lib/factbase/terms/concat.rb +26 -0
- data/lib/factbase/terms/count.rb +25 -0
- data/lib/factbase/terms/defn.rb +16 -15
- data/lib/factbase/terms/div.rb +25 -0
- data/lib/factbase/terms/either.rb +31 -0
- data/lib/factbase/terms/empty.rb +36 -0
- data/lib/factbase/terms/env.rb +28 -0
- data/lib/factbase/terms/eq.rb +28 -0
- data/lib/factbase/terms/exists.rb +27 -0
- data/lib/factbase/terms/first.rb +30 -0
- data/lib/factbase/terms/gt.rb +28 -0
- data/lib/factbase/terms/gte.rb +27 -0
- data/lib/factbase/terms/head.rb +37 -0
- data/lib/factbase/terms/inverted.rb +34 -0
- data/lib/factbase/terms/{aliases.rb → join.rb} +15 -15
- data/lib/factbase/terms/lt.rb +28 -0
- data/lib/factbase/terms/lte.rb +28 -0
- data/lib/factbase/terms/many.rb +29 -0
- data/lib/factbase/terms/{strings.rb → matches.rb} +12 -12
- data/lib/factbase/terms/max.rb +32 -0
- data/lib/factbase/terms/min.rb +32 -0
- data/lib/factbase/terms/minus.rb +25 -0
- data/lib/factbase/terms/never.rb +26 -0
- data/lib/factbase/terms/nil.rb +26 -0
- data/lib/factbase/terms/not.rb +27 -0
- data/lib/factbase/terms/nth.rb +33 -0
- data/lib/factbase/terms/one.rb +30 -0
- data/lib/factbase/terms/or.rb +35 -0
- data/lib/factbase/terms/plus.rb +27 -0
- data/lib/factbase/terms/prev.rb +29 -0
- data/lib/factbase/terms/shared.rb +69 -0
- data/lib/factbase/terms/simplified.rb +31 -0
- data/lib/factbase/terms/size.rb +30 -0
- data/lib/factbase/terms/sorted.rb +38 -0
- data/lib/factbase/terms/sprintf.rb +29 -0
- data/lib/factbase/terms/sum.rb +38 -0
- data/lib/factbase/terms/times.rb +25 -0
- data/lib/factbase/terms/to_float.rb +28 -0
- data/lib/factbase/terms/to_integer.rb +28 -0
- data/lib/factbase/terms/to_string.rb +28 -0
- data/lib/factbase/terms/to_time.rb +28 -0
- data/lib/factbase/terms/traced.rb +33 -0
- data/lib/factbase/terms/type.rb +31 -0
- data/lib/factbase/terms/undef.rb +33 -0
- data/lib/factbase/terms/unique.rb +34 -0
- data/lib/factbase/terms/when.rb +29 -0
- data/lib/factbase/terms/zero.rb +28 -0
- data/lib/factbase/version.rb +1 -1
- data/lib/factbase.rb +10 -1
- metadata +68 -12
- data/lib/factbase/terms/aggregates.rb +0 -99
- data/lib/factbase/terms/casting.rb +0 -41
- data/lib/factbase/terms/lists.rb +0 -57
- data/lib/factbase/terms/logical.rb +0 -124
- data/lib/factbase/terms/math.rb +0 -103
- data/lib/factbase/terms/meta.rb +0 -58
- data/lib/factbase/terms/ordering.rb +0 -34
- data/lib/factbase/terms/system.rb +0 -19
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 91ef3ed499ea15abb77bc39cfed30eb9af704921e86c3c03c7bdd9b13cb44270
|
|
4
|
+
data.tar.gz: 3c2e80d00620e0de14ec4be35ff08b28393793c69742dda9bda55c4a5ae01c8f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9c6e18a32a5ae773e78672e449cce031a2c7641b1291b9f637ccb28afb6afddc87091955ce06e75c0e35326f4db37b04fac5b827b1cdc203a8b0262c105ca2c3
|
|
7
|
+
data.tar.gz: 923207d8494313e1af4fae0ecf3068372a5dde28924840edbe1b6d625127afb7e4eb2cf44c9f86a28d9d7fc650336abce9c9b99cd492b3ff29bcbb4cf214c83e
|
data/Gemfile
CHANGED
|
@@ -6,17 +6,19 @@
|
|
|
6
6
|
source 'https://rubygems.org'
|
|
7
7
|
gemspec
|
|
8
8
|
|
|
9
|
+
gem 'benchmark', '~>0.5', require: false
|
|
9
10
|
gem 'minitest', '~>5.25', require: false
|
|
10
11
|
gem 'minitest-reporters', '~>1.7', require: false
|
|
11
12
|
gem 'os', '~>1.1', require: false
|
|
12
13
|
gem 'qbash', '~>0.4', require: false
|
|
13
14
|
gem 'rake', '~>13.2', require: false
|
|
14
|
-
gem 'rdoc', '6.
|
|
15
|
+
gem 'rdoc', '6.16.1', require: false # GPL
|
|
15
16
|
gem 'rubocop', '~>1.74', require: false
|
|
16
17
|
gem 'rubocop-minitest', '~>0.38', require: false
|
|
17
18
|
gem 'rubocop-performance', '~>1.25', require: false
|
|
18
19
|
gem 'rubocop-rake', '~>0.7', require: false
|
|
19
20
|
gem 'simplecov', '~>0.22', require: false
|
|
20
21
|
gem 'simplecov-cobertura', '~>3.0', require: false
|
|
22
|
+
gem 'stackprof', '~>0.2', require: false, platforms: [:ruby]
|
|
21
23
|
gem 'threads', '~>0.4', require: false
|
|
22
24
|
gem 'yard', '~>0.9', require: false
|
data/Gemfile.lock
CHANGED
|
@@ -19,23 +19,24 @@ GEM
|
|
|
19
19
|
ansi (1.5.0)
|
|
20
20
|
ast (2.4.3)
|
|
21
21
|
backtrace (0.4.1)
|
|
22
|
+
benchmark (0.5.0)
|
|
22
23
|
builder (3.3.0)
|
|
23
24
|
concurrent-ruby (1.3.5)
|
|
24
|
-
date (3.
|
|
25
|
+
date (3.5.0)
|
|
25
26
|
decoor (0.1.0)
|
|
26
27
|
docile (1.4.1)
|
|
27
28
|
elapsed (0.2.0)
|
|
28
29
|
loog (~> 0.6)
|
|
29
30
|
tago (~> 0.1)
|
|
30
31
|
ellipsized (0.3.0)
|
|
31
|
-
erb (
|
|
32
|
-
json (2.
|
|
32
|
+
erb (6.0.0)
|
|
33
|
+
json (2.17.1)
|
|
33
34
|
language_server-protocol (3.17.0.5)
|
|
34
35
|
lint_roller (1.1.0)
|
|
35
36
|
logger (1.7.0)
|
|
36
37
|
loog (0.6.1)
|
|
37
38
|
logger (~> 1.0)
|
|
38
|
-
minitest (5.
|
|
39
|
+
minitest (5.26.2)
|
|
39
40
|
minitest-reporters (1.7.1)
|
|
40
41
|
ansi
|
|
41
42
|
builder
|
|
@@ -52,27 +53,28 @@ GEM
|
|
|
52
53
|
os (1.1.4)
|
|
53
54
|
others (0.1.1)
|
|
54
55
|
parallel (1.27.0)
|
|
55
|
-
parser (3.3.
|
|
56
|
+
parser (3.3.10.0)
|
|
56
57
|
ast (~> 2.4.1)
|
|
57
58
|
racc
|
|
58
|
-
prism (1.
|
|
59
|
+
prism (1.6.0)
|
|
59
60
|
psych (5.2.6)
|
|
60
61
|
date
|
|
61
62
|
stringio
|
|
62
|
-
qbash (0.4.
|
|
63
|
+
qbash (0.4.8)
|
|
63
64
|
backtrace (> 0)
|
|
64
65
|
elapsed (> 0)
|
|
65
66
|
loog (> 0)
|
|
66
67
|
tago (> 0)
|
|
67
68
|
racc (1.8.1)
|
|
68
69
|
rainbow (3.1.1)
|
|
69
|
-
rake (13.3.
|
|
70
|
-
rdoc (6.
|
|
70
|
+
rake (13.3.1)
|
|
71
|
+
rdoc (6.16.1)
|
|
71
72
|
erb
|
|
72
73
|
psych (>= 4.0.0)
|
|
74
|
+
tsort
|
|
73
75
|
regexp_parser (2.11.3)
|
|
74
76
|
rexml (3.4.4)
|
|
75
|
-
rubocop (1.81.
|
|
77
|
+
rubocop (1.81.7)
|
|
76
78
|
json (~> 2.3)
|
|
77
79
|
language_server-protocol (~> 3.17.0.2)
|
|
78
80
|
lint_roller (~> 1.1.0)
|
|
@@ -83,17 +85,17 @@ GEM
|
|
|
83
85
|
rubocop-ast (>= 1.47.1, < 2.0)
|
|
84
86
|
ruby-progressbar (~> 1.7)
|
|
85
87
|
unicode-display_width (>= 2.4.0, < 4.0)
|
|
86
|
-
rubocop-ast (1.
|
|
88
|
+
rubocop-ast (1.48.0)
|
|
87
89
|
parser (>= 3.3.7.2)
|
|
88
90
|
prism (~> 1.4)
|
|
89
91
|
rubocop-minitest (0.38.2)
|
|
90
92
|
lint_roller (~> 1.1)
|
|
91
93
|
rubocop (>= 1.75.0, < 2.0)
|
|
92
94
|
rubocop-ast (>= 1.38.0, < 2.0)
|
|
93
|
-
rubocop-performance (1.26.
|
|
95
|
+
rubocop-performance (1.26.1)
|
|
94
96
|
lint_roller (~> 1.1)
|
|
95
97
|
rubocop (>= 1.75.0, < 2.0)
|
|
96
|
-
rubocop-ast (>= 1.
|
|
98
|
+
rubocop-ast (>= 1.47.1, < 2.0)
|
|
97
99
|
rubocop-rake (0.7.1)
|
|
98
100
|
lint_roller (~> 1.1)
|
|
99
101
|
rubocop (>= 1.72.1)
|
|
@@ -107,16 +109,18 @@ GEM
|
|
|
107
109
|
simplecov (~> 0.19)
|
|
108
110
|
simplecov-html (0.13.2)
|
|
109
111
|
simplecov_json_formatter (0.1.4)
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
112
|
+
stackprof (0.2.27)
|
|
113
|
+
stringio (3.1.9)
|
|
114
|
+
tago (0.4.0)
|
|
115
|
+
threads (0.5.0)
|
|
113
116
|
backtrace (~> 0)
|
|
114
117
|
concurrent-ruby (~> 1.0)
|
|
118
|
+
tsort (0.2.0)
|
|
115
119
|
unicode-display_width (3.2.0)
|
|
116
120
|
unicode-emoji (~> 4.1)
|
|
117
121
|
unicode-emoji (4.1.0)
|
|
118
122
|
yaml (0.4.0)
|
|
119
|
-
yard (0.9.
|
|
123
|
+
yard (0.9.38)
|
|
120
124
|
|
|
121
125
|
PLATFORMS
|
|
122
126
|
arm64-darwin-22
|
|
@@ -125,24 +129,27 @@ PLATFORMS
|
|
|
125
129
|
x64-mingw-ucrt
|
|
126
130
|
x86_64-darwin-20
|
|
127
131
|
x86_64-darwin-21
|
|
132
|
+
x86_64-darwin-24
|
|
128
133
|
x86_64-linux
|
|
129
134
|
|
|
130
135
|
DEPENDENCIES
|
|
136
|
+
benchmark (~> 0.5)
|
|
131
137
|
factbase!
|
|
132
138
|
minitest (~> 5.25)
|
|
133
139
|
minitest-reporters (~> 1.7)
|
|
134
140
|
os (~> 1.1)
|
|
135
141
|
qbash (~> 0.4)
|
|
136
142
|
rake (~> 13.2)
|
|
137
|
-
rdoc (= 6.
|
|
143
|
+
rdoc (= 6.16.1)
|
|
138
144
|
rubocop (~> 1.74)
|
|
139
145
|
rubocop-minitest (~> 0.38)
|
|
140
146
|
rubocop-performance (~> 1.25)
|
|
141
147
|
rubocop-rake (~> 0.7)
|
|
142
148
|
simplecov (~> 0.22)
|
|
143
149
|
simplecov-cobertura (~> 3.0)
|
|
150
|
+
stackprof (~> 0.2)
|
|
144
151
|
threads (~> 0.4)
|
|
145
152
|
yard (~> 0.9)
|
|
146
153
|
|
|
147
154
|
BUNDLED WITH
|
|
148
|
-
2.
|
|
155
|
+
2.6.8
|
data/README.md
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
[](https://www.jetbrains.com/ruby/)
|
|
5
5
|
|
|
6
6
|
[](https://github.com/yegor256/factbase/actions/workflows/rake.yml)
|
|
7
|
+
[](https://zerocracy.github.io/judges-action/zerocracy-vitals.html)
|
|
7
8
|
[](https://www.0pdd.com/p?name=yegor256/factbase)
|
|
8
9
|
[](https://badge.fury.io/rb/factbase)
|
|
9
10
|
[](https://codecov.io/github/yegor256/factbase?branch=master)
|
|
@@ -193,7 +194,7 @@ Read
|
|
|
193
194
|
[these guidelines](https://www.yegor256.com/2014/04/15/github-guidelines.html).
|
|
194
195
|
Make sure your build is green before you contribute
|
|
195
196
|
your pull request. You will need to have
|
|
196
|
-
[Ruby](https://www.ruby-lang.org/en/) 3.
|
|
197
|
+
[Ruby](https://www.ruby-lang.org/en/) 3.4+ and
|
|
197
198
|
[Bundler](https://bundler.io/) installed. Then:
|
|
198
199
|
|
|
199
200
|
```bash
|
|
@@ -210,35 +211,35 @@ This is the result of the benchmark:
|
|
|
210
211
|
<!-- benchmark_begin -->
|
|
211
212
|
```text
|
|
212
213
|
user
|
|
213
|
-
insert 20000 facts 0.
|
|
214
|
-
export 20000 facts 0.
|
|
215
|
-
import
|
|
216
|
-
insert 10 facts 0.
|
|
217
|
-
query 10 times w/txn 2.
|
|
218
|
-
query 10 times w/o txn 0.
|
|
219
|
-
modify 10 attrs w/txn 1.
|
|
220
|
-
delete 10 facts w/txn 1.
|
|
221
|
-
(and (eq what 'issue-was-closed') (exists... -> 200 1.
|
|
222
|
-
(and (eq what 'issue-was-closed') (exists... -> 200/txn 1.
|
|
223
|
-
(and (eq what 'issue-was-closed') (exists... -> zero 1.
|
|
224
|
-
(and (eq what 'issue-was-closed') (exists... -> zero/txn 1.
|
|
225
|
-
(gt time '2024-03-23T03:21:43Z') 0.
|
|
226
|
-
(gt cost 50) 0.
|
|
227
|
-
(eq title 'Object Thinking 5000') 0.
|
|
228
|
-
(and (eq foo 42.998) (or (gt bar 200) (absent z... 0.
|
|
229
|
-
(and (exists foo) (not (exists blue)))
|
|
230
|
-
(eq id (agg (always) (max id))) 0.
|
|
231
|
-
(join "c<=cost,b<=bar" (eq id (agg (always) (ma... 1.
|
|
232
|
-
(and (eq what "foo") (join "w<=what" (and (eq i... 7.
|
|
233
|
-
delete! 0.
|
|
234
|
-
Taped.append() x50000 0.
|
|
235
|
-
Taped.each() x125 1.
|
|
236
|
-
Taped.delete_if() x375 0.
|
|
214
|
+
insert 20000 facts 0.637574
|
|
215
|
+
export 20000 facts 0.019855
|
|
216
|
+
import 410750 bytes (20000 facts) 0.035065
|
|
217
|
+
insert 10 facts 0.044615
|
|
218
|
+
query 10 times w/txn 2.455739
|
|
219
|
+
query 10 times w/o txn 0.050423
|
|
220
|
+
modify 10 attrs w/txn 1.894240
|
|
221
|
+
delete 10 facts w/txn 1.043079
|
|
222
|
+
(and (eq what 'issue-was-closed') (exists... -> 200 1.279827
|
|
223
|
+
(and (eq what 'issue-was-closed') (exists... -> 200/txn 1.263302
|
|
224
|
+
(and (eq what 'issue-was-closed') (exists... -> zero 1.253773
|
|
225
|
+
(and (eq what 'issue-was-closed') (exists... -> zero/txn 1.292489
|
|
226
|
+
(gt time '2024-03-23T03:21:43Z') 0.398074
|
|
227
|
+
(gt cost 50) 0.254020
|
|
228
|
+
(eq title 'Object Thinking 5000') 0.037238
|
|
229
|
+
(and (eq foo 42.998) (or (gt bar 200) (absent z... 0.047599
|
|
230
|
+
(and (exists foo) (not (exists blue))) 1.115156
|
|
231
|
+
(eq id (agg (always) (max id))) 0.711832
|
|
232
|
+
(join "c<=cost,b<=bar" (eq id (agg (always) (ma... 1.393622
|
|
233
|
+
(and (eq what "foo") (join "w<=what" (and (eq i... 7.428505
|
|
234
|
+
delete! 0.272962
|
|
235
|
+
Taped.append() x50000 0.020619
|
|
236
|
+
Taped.each() x125 1.721876
|
|
237
|
+
Taped.delete_if() x375 0.844673
|
|
237
238
|
```
|
|
238
239
|
|
|
239
240
|
The results were calculated in [this GHA job][benchmark-gha]
|
|
240
|
-
on 2025-
|
|
241
|
+
on 2025-10-15 at 14:53,
|
|
241
242
|
on Linux with 4 CPUs.
|
|
242
243
|
<!-- benchmark_end -->
|
|
243
244
|
|
|
244
|
-
[benchmark-gha]: https://github.com/yegor256/factbase/actions/runs/
|
|
245
|
+
[benchmark-gha]: https://github.com/yegor256/factbase/actions/runs/18533080805
|
data/Rakefile
CHANGED
|
@@ -44,6 +44,7 @@ require 'yard'
|
|
|
44
44
|
desc 'Build Yard documentation'
|
|
45
45
|
YARD::Rake::YardocTask.new do |t|
|
|
46
46
|
t.files = ['lib/**/*.rb']
|
|
47
|
+
t.options = ['--fail-on-warning']
|
|
47
48
|
end
|
|
48
49
|
|
|
49
50
|
require 'rubocop/rake_task'
|
|
@@ -53,20 +54,43 @@ RuboCop::RakeTask.new(:rubocop) do |task|
|
|
|
53
54
|
end
|
|
54
55
|
|
|
55
56
|
desc 'Benchmark them all'
|
|
56
|
-
task :benchmark do
|
|
57
|
+
task :benchmark, [:name] do |_t, args|
|
|
58
|
+
bname = args[:name] || 'all'
|
|
57
59
|
require_relative 'lib/factbase'
|
|
58
|
-
fb = Factbase.new
|
|
59
60
|
require_relative 'lib/factbase/cached/cached_factbase'
|
|
60
|
-
fb = Factbase::CachedFactbase.new(fb)
|
|
61
61
|
require_relative 'lib/factbase/indexed/indexed_factbase'
|
|
62
|
-
fb = Factbase::IndexedFactbase.new(fb)
|
|
63
62
|
require_relative 'lib/factbase/sync/sync_factbase'
|
|
64
|
-
fb = Factbase::SyncFactbase.new(fb)
|
|
65
63
|
require 'benchmark'
|
|
66
64
|
Benchmark.bm(60) do |b|
|
|
67
|
-
|
|
65
|
+
fb = Factbase.new
|
|
66
|
+
fb = Factbase::CachedFactbase.new(fb)
|
|
67
|
+
fb = Factbase::IndexedFactbase.new(fb)
|
|
68
|
+
fb = Factbase::SyncFactbase.new(fb)
|
|
69
|
+
if bname == 'all'
|
|
70
|
+
Dir['benchmark/bench_*.rb'].each do |f|
|
|
71
|
+
require_relative f
|
|
72
|
+
Kernel.send(File.basename(f).gsub(/\.rb$/, '').to_sym, b, fb)
|
|
73
|
+
end
|
|
74
|
+
else
|
|
75
|
+
f = "benchmark/#{bname}.rb"
|
|
68
76
|
require_relative f
|
|
69
77
|
Kernel.send(File.basename(f).gsub(/\.rb$/, '').to_sym, b, fb)
|
|
70
78
|
end
|
|
71
79
|
end
|
|
72
80
|
end
|
|
81
|
+
|
|
82
|
+
# Run profiling on a benchmark and generate a flamegraph.
|
|
83
|
+
# To run this task, you need to have stackprof installed.
|
|
84
|
+
# https://github.com/tmm1/stackprof
|
|
85
|
+
# To run profiling for a specific benchmark you can run:
|
|
86
|
+
# bundle exec rake flamegraph\[bench_slow_query\]
|
|
87
|
+
desc 'Profile a benchmark (e.g., flamegraph[bench_slow_query])'
|
|
88
|
+
task :flamegraph, [:name] do |_t, args|
|
|
89
|
+
require 'stackprof'
|
|
90
|
+
bname = args[:name] || 'all'
|
|
91
|
+
puts "Starting profiling for '#{bname}'..."
|
|
92
|
+
StackProf.run(mode: :cpu, out: 'stackprof-cpu-myapp.dump', raw: true) do
|
|
93
|
+
Rake::Task['benchmark'].invoke(bname)
|
|
94
|
+
end
|
|
95
|
+
`stackprof --d3-flamegraph stackprof-cpu-myapp.dump > flamegraph.html`
|
|
96
|
+
end
|
|
@@ -12,6 +12,8 @@ require_relative 'cached_fact'
|
|
|
12
12
|
# Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
|
|
13
13
|
# License:: MIT
|
|
14
14
|
class Factbase::CachedQuery
|
|
15
|
+
include Enumerable
|
|
16
|
+
|
|
15
17
|
# Constructor.
|
|
16
18
|
# @param [Factbase::Query] origin Original query
|
|
17
19
|
# @param [Hash] cache The cache
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) 2024-2025 Yegor Bugayenko
|
|
4
|
+
# SPDX-License-Identifier: MIT
|
|
5
|
+
|
|
6
|
+
# Indexed term 'absent'.
|
|
7
|
+
class Factbase::IndexedAbsent
|
|
8
|
+
def initialize(term, idx)
|
|
9
|
+
@term = term
|
|
10
|
+
@idx = idx
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def predict(maps, _fb, _params)
|
|
14
|
+
return nil if @idx.nil?
|
|
15
|
+
key = [maps.object_id, @term.operands.first, @term.op]
|
|
16
|
+
if @idx[key].nil?
|
|
17
|
+
@idx[key] = []
|
|
18
|
+
prop = @term.operands.first.to_s
|
|
19
|
+
maps.to_a.each do |m|
|
|
20
|
+
@idx[key].append(m) if m[prop].nil?
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
(maps & []) | @idx[key]
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) 2024-2025 Yegor Bugayenko
|
|
4
|
+
# SPDX-License-Identifier: MIT
|
|
5
|
+
|
|
6
|
+
# Indexed term 'and'.
|
|
7
|
+
class Factbase::IndexedAnd
|
|
8
|
+
def initialize(term, idx)
|
|
9
|
+
@term = term
|
|
10
|
+
@idx = idx
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def predict(maps, fb, params)
|
|
14
|
+
return nil if @idx.nil?
|
|
15
|
+
key = [maps.object_id, @term.operands.first, @term.op]
|
|
16
|
+
r = nil
|
|
17
|
+
if @term.operands.all? { |o| o.op == :eq } && @term.operands.size > 1 \
|
|
18
|
+
&& @term.operands.all? { |o| o.operands.first.is_a?(Symbol) && _scalar?(o.operands[1]) }
|
|
19
|
+
props = @term.operands.map { |o| o.operands.first }.sort
|
|
20
|
+
key = [maps.object_id, props, :multi_and_eq]
|
|
21
|
+
if @idx[key].nil?
|
|
22
|
+
@idx[key] = {}
|
|
23
|
+
maps.to_a.each do |m|
|
|
24
|
+
_all_tuples(m, props).each do |t|
|
|
25
|
+
@idx[key][t] = [] if @idx[key][t].nil?
|
|
26
|
+
@idx[key][t].append(m)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
tuples = Enumerator.product(
|
|
31
|
+
*@term.operands.sort_by { |o| o.operands.first }.map do |o|
|
|
32
|
+
if o.operands[1].is_a?(Symbol)
|
|
33
|
+
params[o.operands[1].to_s] || []
|
|
34
|
+
else
|
|
35
|
+
[o.operands[1]]
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
)
|
|
39
|
+
j = tuples.map { |t| @idx[key][t] || [] }.reduce(&:|)
|
|
40
|
+
r = (maps & []) | j
|
|
41
|
+
else
|
|
42
|
+
@term.operands.each do |o|
|
|
43
|
+
n = o.predict(maps, fb, params)
|
|
44
|
+
break if n.nil?
|
|
45
|
+
if r.nil?
|
|
46
|
+
r = n
|
|
47
|
+
elsif n.size < r.size * 8 # to skip some obvious matchings
|
|
48
|
+
r &= n.to_a
|
|
49
|
+
end
|
|
50
|
+
break if r.size < maps.size / 32 # it's already small enough
|
|
51
|
+
break if r.size < 128 # it's obviously already small enough
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
r
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def _scalar?(item)
|
|
60
|
+
item.is_a?(String) || item.is_a?(Time) || item.is_a?(Integer) || item.is_a?(Float) || item.is_a?(Symbol)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def _all_tuples(fact, props)
|
|
64
|
+
prop = props.first.to_s
|
|
65
|
+
tuples = []
|
|
66
|
+
tuples += (fact[prop] || []).zip
|
|
67
|
+
if props.size > 1
|
|
68
|
+
tails = _all_tuples(fact, props[1..])
|
|
69
|
+
ext = []
|
|
70
|
+
tuples.each do |t|
|
|
71
|
+
tails.each do |tail|
|
|
72
|
+
ext << (t + tail)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
tuples = ext
|
|
76
|
+
end
|
|
77
|
+
tuples
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) 2024-2025 Yegor Bugayenko
|
|
4
|
+
# SPDX-License-Identifier: MIT
|
|
5
|
+
|
|
6
|
+
# Indexed term 'eq'.
|
|
7
|
+
class Factbase::IndexedEq
|
|
8
|
+
def initialize(term, idx)
|
|
9
|
+
@term = term
|
|
10
|
+
@idx = idx
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def predict(maps, _fb, params)
|
|
14
|
+
return nil if @idx.nil?
|
|
15
|
+
key = [maps.object_id, @term.operands.first, @term.op]
|
|
16
|
+
return unless @term.operands.first.is_a?(Symbol) && _scalar?(@term.operands[1])
|
|
17
|
+
if @idx[key].nil?
|
|
18
|
+
@idx[key] = {}
|
|
19
|
+
prop = @term.operands.first.to_s
|
|
20
|
+
maps.to_a.each do |m|
|
|
21
|
+
m[prop]&.each do |v|
|
|
22
|
+
@idx[key][v] = [] if @idx[key][v].nil?
|
|
23
|
+
@idx[key][v].append(m)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
vv =
|
|
28
|
+
if @term.operands[1].is_a?(Symbol)
|
|
29
|
+
params[@term.operands[1].to_s] || []
|
|
30
|
+
else
|
|
31
|
+
[@term.operands[1]]
|
|
32
|
+
end
|
|
33
|
+
if vv.empty?
|
|
34
|
+
(maps & [])
|
|
35
|
+
else
|
|
36
|
+
j = vv.map { |v| @idx[key][v] || [] }.reduce(&:|)
|
|
37
|
+
(maps & []) | j
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def _scalar?(item)
|
|
44
|
+
item.is_a?(String) || item.is_a?(Time) || item.is_a?(Integer) || item.is_a?(Float) || item.is_a?(Symbol)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) 2024-2025 Yegor Bugayenko
|
|
4
|
+
# SPDX-License-Identifier: MIT
|
|
5
|
+
|
|
6
|
+
# Indexed term 'exists'.
|
|
7
|
+
class Factbase::IndexedExists
|
|
8
|
+
def initialize(term, idx)
|
|
9
|
+
@term = term
|
|
10
|
+
@idx = idx
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def predict(maps, _fb, _params)
|
|
14
|
+
return nil if @idx.nil?
|
|
15
|
+
key = [maps.object_id, @term.operands.first, @term.op]
|
|
16
|
+
if @idx[key].nil?
|
|
17
|
+
@idx[key] = []
|
|
18
|
+
prop = @term.operands.first.to_s
|
|
19
|
+
maps.to_a.each do |m|
|
|
20
|
+
@idx[key].append(m) unless m[prop].nil?
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
(maps & []) | @idx[key]
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -57,8 +57,57 @@ class Factbase::IndexedFactbase
|
|
|
57
57
|
# Run an ACID transaction.
|
|
58
58
|
# @return [Factbase::Churn] How many facts have been changed (zero if rolled back)
|
|
59
59
|
def txn
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
result =
|
|
61
|
+
@origin.txn do |fbt|
|
|
62
|
+
yield Factbase::IndexedFactbase.new(fbt, @idx)
|
|
63
|
+
end
|
|
64
|
+
@idx.clear
|
|
65
|
+
result
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Export it into a chain of bytes, including both data and index.
|
|
69
|
+
#
|
|
70
|
+
# Here is how you can export it to a file, for example:
|
|
71
|
+
#
|
|
72
|
+
# fb = Factbase::IndexedFactbase.new(Factbase.new)
|
|
73
|
+
# fb.insert.foo = 42
|
|
74
|
+
# File.binwrite("foo.fb", fb.export)
|
|
75
|
+
#
|
|
76
|
+
# The data is binary, it's not a text!
|
|
77
|
+
#
|
|
78
|
+
# @return [String] Binary string containing serialized data and index
|
|
79
|
+
def export
|
|
80
|
+
Marshal.dump({ maps: @origin.export, idx: @idx })
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Import from a chain of bytes, including both data and index.
|
|
84
|
+
#
|
|
85
|
+
# Here is how you can read it from a file, for example:
|
|
86
|
+
#
|
|
87
|
+
# fb = Factbase::IndexedFactbase.new(Factbase.new)
|
|
88
|
+
# fb.import(File.binread("foo.fb"))
|
|
89
|
+
#
|
|
90
|
+
# The facts that existed in the factbase before importing will remain there.
|
|
91
|
+
# The facts from the incoming byte stream will be added to them.
|
|
92
|
+
# If the byte stream doesn't contain an index (for backward compatibility),
|
|
93
|
+
# the index will be empty and will be built on first use.
|
|
94
|
+
#
|
|
95
|
+
# @param [String] bytes Binary string to import
|
|
96
|
+
def import(bytes)
|
|
97
|
+
raise 'Empty input, cannot load a factbase' if bytes.empty?
|
|
98
|
+
data = Marshal.load(bytes)
|
|
99
|
+
if data.is_a?(Hash) && data.key?(:maps)
|
|
100
|
+
@origin.import(data[:maps])
|
|
101
|
+
@idx.merge!(data[:idx]) if data[:idx].is_a?(Hash)
|
|
102
|
+
else
|
|
103
|
+
@origin.import(bytes)
|
|
104
|
+
@idx.clear
|
|
62
105
|
end
|
|
63
106
|
end
|
|
107
|
+
|
|
108
|
+
# Size, the total number of facts in the factbase.
|
|
109
|
+
# @return [Integer] How many facts are in there
|
|
110
|
+
def size
|
|
111
|
+
@origin.size
|
|
112
|
+
end
|
|
64
113
|
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) 2024-2025 Yegor Bugayenko
|
|
4
|
+
# SPDX-License-Identifier: MIT
|
|
5
|
+
|
|
6
|
+
# Indexed term 'gt'.
|
|
7
|
+
class Factbase::IndexedGt
|
|
8
|
+
def initialize(term, idx)
|
|
9
|
+
@term = term
|
|
10
|
+
@idx = idx
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def predict(maps, _fb, params)
|
|
14
|
+
return nil if @idx.nil?
|
|
15
|
+
return unless @term.operands.first.is_a?(Symbol) && _scalar?(@term.operands[1])
|
|
16
|
+
prop = @term.operands.first.to_s
|
|
17
|
+
cache_key = [maps.object_id, @term.operands.first, :sorted]
|
|
18
|
+
if @idx[cache_key].nil?
|
|
19
|
+
@idx[cache_key] = []
|
|
20
|
+
maps.to_a.each do |m|
|
|
21
|
+
values = m[prop]
|
|
22
|
+
next if values.nil?
|
|
23
|
+
values.each do |v|
|
|
24
|
+
@idx[cache_key] << [v, m]
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
@idx[cache_key].sort_by! { |pair| pair[0] }
|
|
28
|
+
end
|
|
29
|
+
threshold = @term.operands[1].is_a?(Symbol) ? params[@term.operands[1].to_s]&.first : @term.operands[1]
|
|
30
|
+
return nil if threshold.nil?
|
|
31
|
+
i = @idx[cache_key].bsearch_index { |pair| pair[0] > threshold } || @idx[cache_key].size
|
|
32
|
+
result = @idx[cache_key][i..].map { |pair| pair[1] }.uniq
|
|
33
|
+
(maps & []) | result
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def _scalar?(item)
|
|
39
|
+
item.is_a?(String) || item.is_a?(Time) || item.is_a?(Integer) || item.is_a?(Float) || item.is_a?(Symbol)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) 2024-2025 Yegor Bugayenko
|
|
4
|
+
# SPDX-License-Identifier: MIT
|
|
5
|
+
|
|
6
|
+
# Indexed term 'lt'.
|
|
7
|
+
class Factbase::IndexedLt
|
|
8
|
+
def initialize(term, idx)
|
|
9
|
+
@term = term
|
|
10
|
+
@idx = idx
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def predict(maps, _fb, params)
|
|
14
|
+
return nil if @idx.nil?
|
|
15
|
+
return unless @term.operands.first.is_a?(Symbol) && _scalar?(@term.operands[1])
|
|
16
|
+
prop = @term.operands.first.to_s
|
|
17
|
+
cache_key = [maps.object_id, @term.operands.first, :sorted]
|
|
18
|
+
if @idx[cache_key].nil?
|
|
19
|
+
@idx[cache_key] = []
|
|
20
|
+
maps.to_a.each do |m|
|
|
21
|
+
values = m[prop]
|
|
22
|
+
next if values.nil?
|
|
23
|
+
values.each do |v|
|
|
24
|
+
@idx[cache_key] << [v, m]
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
@idx[cache_key].sort_by! { |pair| pair[0] }
|
|
28
|
+
end
|
|
29
|
+
threshold = @term.operands[1].is_a?(Symbol) ? params[@term.operands[1].to_s]&.first : @term.operands[1]
|
|
30
|
+
return nil if threshold.nil?
|
|
31
|
+
i = @idx[cache_key].bsearch_index { |pair| pair[0] >= threshold } || @idx[cache_key].size
|
|
32
|
+
result = @idx[cache_key][0...i].map { |pair| pair[1] }.uniq
|
|
33
|
+
(maps & []) | result
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def _scalar?(item)
|
|
39
|
+
item.is_a?(String) || item.is_a?(Time) || item.is_a?(Integer) || item.is_a?(Float) || item.is_a?(Symbol)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) 2024-2025 Yegor Bugayenko
|
|
4
|
+
# SPDX-License-Identifier: MIT
|
|
5
|
+
|
|
6
|
+
# Indexed term 'not'.
|
|
7
|
+
class Factbase::IndexedNot
|
|
8
|
+
def initialize(term, idx)
|
|
9
|
+
@term = term
|
|
10
|
+
@idx = idx
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def predict(maps, fb, params)
|
|
14
|
+
return nil if @idx.nil?
|
|
15
|
+
key = [maps.object_id, @term.operands.first, @term.op]
|
|
16
|
+
if @idx[key].nil?
|
|
17
|
+
yes = @term.operands.first.predict(maps, fb, params)
|
|
18
|
+
if yes.nil?
|
|
19
|
+
@idx[key] = { r: nil }
|
|
20
|
+
else
|
|
21
|
+
yes = yes.to_a.to_set
|
|
22
|
+
@idx[key] = { r: maps.to_a.reject { |m| yes.include?(m) } }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
r = @idx[key][:r]
|
|
26
|
+
if r.nil?
|
|
27
|
+
nil
|
|
28
|
+
else
|
|
29
|
+
(maps & []) | r
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|