factbase 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -1
- data/Gemfile +1 -1
- data/Gemfile.lock +3 -3
- data/README.md +24 -24
- data/REUSE.toml +7 -2
- data/Rakefile +8 -1
- data/benchmark/bench_factbase.rb +1 -1
- data/fixtures/stories/agg.yml +17 -0
- data/fixtures/stories/always.yml +16 -0
- data/fixtures/stories/as.yml +16 -0
- data/fixtures/stories/count.yml +18 -0
- data/fixtures/stories/eq.yml +30 -0
- data/fixtures/stories/gt.yml +18 -0
- data/fixtures/stories/join.yml +19 -0
- data/fixtures/stories/max.yml +14 -0
- data/fixtures/stories/min.yml +14 -0
- data/fixtures/stories/nth.yml +14 -0
- data/fixtures/stories/or.yml +18 -0
- data/fixtures/stories/sprintf.yml +12 -0
- data/fixtures/stories/sum.yml +14 -0
- data/lib/factbase/cached/cached_fact.rb +28 -0
- data/lib/factbase/cached/cached_factbase.rb +64 -0
- data/lib/factbase/cached/cached_query.rb +61 -0
- data/lib/factbase/cached/cached_term.rb +25 -0
- data/lib/factbase/fact.rb +13 -13
- data/lib/factbase/indexed/indexed_fact.rb +28 -0
- data/lib/factbase/indexed/indexed_factbase.rb +64 -0
- data/lib/factbase/indexed/indexed_query.rb +56 -0
- data/lib/factbase/indexed/indexed_term.rb +60 -0
- data/lib/factbase/light.rb +7 -6
- data/lib/factbase/logged.rb +67 -44
- data/lib/factbase/query.rb +29 -34
- data/lib/factbase/rules.rb +15 -14
- data/lib/factbase/sync/sync_factbase.rb +57 -0
- data/lib/factbase/sync/sync_query.rb +61 -0
- data/lib/factbase/syntax.rb +12 -25
- data/lib/factbase/tallied.rb +10 -9
- data/lib/factbase/taped.rb +8 -0
- data/lib/factbase/tee.rb +2 -0
- data/lib/factbase/term.rb +45 -17
- data/lib/factbase/terms/aggregates.rb +17 -15
- data/lib/factbase/terms/aliases.rb +4 -4
- data/lib/factbase/terms/casting.rb +8 -8
- data/lib/factbase/terms/debug.rb +2 -2
- data/lib/factbase/terms/defn.rb +3 -3
- data/lib/factbase/terms/logical.rb +53 -14
- data/lib/factbase/terms/math.rb +26 -26
- data/lib/factbase/terms/meta.rb +14 -14
- data/lib/factbase/terms/ordering.rb +4 -4
- data/lib/factbase/terms/strings.rb +8 -8
- data/lib/factbase/terms/system.rb +3 -3
- data/lib/factbase.rb +67 -55
- data/test/factbase/cached/test_cached_factbase.rb +22 -0
- data/test/factbase/cached/test_cached_query.rb +79 -0
- data/test/factbase/indexed/test_indexed_query.rb +175 -0
- data/test/factbase/sync/test_sync_query.rb +30 -0
- data/test/factbase/terms/test_aggregates.rb +5 -5
- data/test/factbase/terms/test_aliases.rb +7 -7
- data/test/factbase/terms/test_casting.rb +8 -8
- data/test/factbase/terms/test_debug.rb +6 -6
- data/test/factbase/terms/test_defn.rb +14 -14
- data/test/factbase/terms/test_logical.rb +17 -19
- data/test/factbase/terms/test_math.rb +63 -61
- data/test/factbase/terms/test_meta.rb +36 -36
- data/test/factbase/terms/test_ordering.rb +9 -9
- data/test/factbase/terms/test_strings.rb +10 -10
- data/test/factbase/terms/test_system.rb +6 -6
- data/test/factbase/test_accum.rb +5 -5
- data/test/factbase/test_fact.rb +12 -12
- data/test/factbase/test_logged.rb +7 -0
- data/test/factbase/test_query.rb +99 -37
- data/test/factbase/test_rules.rb +1 -1
- data/test/factbase/test_syntax.rb +12 -12
- data/test/factbase/test_tee.rb +8 -8
- data/test/factbase/test_term.rb +39 -30
- data/test/test__helper.rb +2 -2
- data/test/test_factbase.rb +6 -0
- metadata +29 -4
- data/lib/factbase/query_once.rb +0 -54
- data/lib/factbase/term_once.rb +0 -67
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d9aeae89fbcf5e3c1b74f475a8ad001867869d29eab8b861a26b07a2fee9db93
|
4
|
+
data.tar.gz: 0c20ed8b499cb495ca438621b30c24c9c837a73c8cffdde812ecacabc7b9a49c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5f342a46704cdfa9f4355726fd7ca98f6a20d44090fd84c70e69189167abb97c378aca6f52fb295e736acbe4f501d61c65dd93ae833338327be8e81c9bb8f9a7
|
7
|
+
data.tar.gz: 0ec8785757ba4a370e0b1f50b3a7d50271e5245278b23d1b6a45618c90676517bf45763af7f5b150c606c22571992fcd15a95e771e661d2f62308add8e97d93a
|
data/.rubocop.yml
CHANGED
data/Gemfile
CHANGED
@@ -10,7 +10,7 @@ gem 'minitest', '5.25.4', require: false
|
|
10
10
|
gem 'minitest-reporters', '1.7.1', require: false
|
11
11
|
gem 'rake', '13.2.1', require: false
|
12
12
|
gem 'rspec-rails', '7.1.1', require: false
|
13
|
-
gem 'rubocop', '1.73.
|
13
|
+
gem 'rubocop', '1.73.2', require: false
|
14
14
|
gem 'rubocop-minitest', '>0', require: false
|
15
15
|
gem 'rubocop-performance', '>0', require: false
|
16
16
|
gem 'rubocop-rake', '>0', require: false
|
data/Gemfile.lock
CHANGED
@@ -100,7 +100,7 @@ GEM
|
|
100
100
|
date
|
101
101
|
stringio
|
102
102
|
racc (1.8.1)
|
103
|
-
rack (3.1.
|
103
|
+
rack (3.1.11)
|
104
104
|
rack-session (2.1.0)
|
105
105
|
base64 (>= 0.1.0)
|
106
106
|
rack (>= 3.0.0)
|
@@ -148,7 +148,7 @@ GEM
|
|
148
148
|
rspec-mocks (~> 3.13)
|
149
149
|
rspec-support (~> 3.13)
|
150
150
|
rspec-support (3.13.2)
|
151
|
-
rubocop (1.73.
|
151
|
+
rubocop (1.73.2)
|
152
152
|
json (~> 2.3)
|
153
153
|
language_server-protocol (~> 3.17.0.2)
|
154
154
|
lint_roller (~> 1.1.0)
|
@@ -218,7 +218,7 @@ DEPENDENCIES
|
|
218
218
|
minitest-reporters (= 1.7.1)
|
219
219
|
rake (= 13.2.1)
|
220
220
|
rspec-rails (= 7.1.1)
|
221
|
-
rubocop (= 1.73.
|
221
|
+
rubocop (= 1.73.2)
|
222
222
|
rubocop-minitest (> 0)
|
223
223
|
rubocop-performance (> 0)
|
224
224
|
rubocop-rake (> 0)
|
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
|
213
|
-
export
|
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
|
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.561512 0.006100 0.567612 ( 0.567669)
|
213
|
+
export 20000 facts 0.021207 0.001926 0.023133 ( 0.023136)
|
214
|
+
import 411017 bytes (20000 facts) 0.030268 0.004025 0.034293 ( 0.034298)
|
215
|
+
insert 10 facts 0.041799 0.003981 0.045780 ( 0.045783)
|
216
|
+
query 10 times w/txn 1.949384 0.022040 1.971424 ( 1.971572)
|
217
|
+
query 10 times w/o txn 0.766215 0.000000 0.766215 ( 0.766298)
|
218
|
+
modify 10 attrs w/txn 1.831320 0.019013 1.850333 ( 1.850663)
|
219
|
+
delete 10 facts w/txn 0.720217 0.000982 0.721199 ( 0.721265)
|
220
|
+
(and (eq what 'issue-was-closed') (exists... -> 200 2.699199 0.003986 2.703185 ( 2.703395)
|
221
|
+
(and (eq what 'issue-was-closed') (exists... -> 200/txn 2.711764 0.003998 2.715762 ( 2.716000)
|
222
|
+
(and (eq what 'issue-was-closed') (exists... -> zero 4.091858 0.003001 4.094859 ( 4.095101)
|
223
|
+
(and (eq what 'issue-was-closed') (exists... -> zero/txn 4.156396 0.003004 4.159400 ( 4.159589)
|
224
|
+
(gt time '2024-03-23T03:21:43Z') 0.104266 0.001002 0.105268 ( 0.105284)
|
225
|
+
(gt cost 50) 0.090716 0.000000 0.090716 ( 0.090721)
|
226
|
+
(eq title 'Object Thinking 5000') 0.005533 0.000002 0.005535 ( 0.005536)
|
227
|
+
(and (eq foo 42.998) (or (gt bar 200) (absent zzz))) 0.053251 0.000002 0.053253 ( 0.053255)
|
228
|
+
(eq id (agg (always) (max id))) 0.300388 0.000000 0.300388 ( 0.300414)
|
229
|
+
(join "c<=cost,b<=bar" (eq id (agg (always) (max id)))) 1.254507 0.002999 1.257506 ( 1.257555)
|
230
|
+
delete! 0.051979 0.000000 0.051979 ( 0.051982)
|
231
|
+
Taped.append() x50000 0.028540 0.000000 0.028540 ( 0.028543)
|
232
|
+
Taped.each() x125 1.332618 0.000000 1.332618 ( 1.332645)
|
233
|
+
Taped.delete_if() x375 0.815309 0.000000 0.815309 ( 0.815348)
|
234
234
|
```
|
235
235
|
|
236
236
|
The results were calculated in [this GHA job][benchmark-gha]
|
237
|
-
on 2025-03-
|
237
|
+
on 2025-03-12 at 08:03,
|
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/13806425302
|
data/REUSE.toml
CHANGED
@@ -4,6 +4,9 @@
|
|
4
4
|
version = 1
|
5
5
|
[[annotations]]
|
6
6
|
path = [
|
7
|
+
"**.json",
|
8
|
+
"**.md",
|
9
|
+
"**.txt",
|
7
10
|
"**/*.csv",
|
8
11
|
"**/*.jpg",
|
9
12
|
"**/*.json",
|
@@ -13,13 +16,15 @@ path = [
|
|
13
16
|
"**/*.svg",
|
14
17
|
"**/*.txt",
|
15
18
|
"**/*.vm",
|
19
|
+
"**/.DS_Store",
|
16
20
|
"**/.gitignore",
|
21
|
+
"**/.pdd",
|
17
22
|
"**/CNAME",
|
23
|
+
"**/Gemfile.lock",
|
24
|
+
".DS_Store",
|
18
25
|
".gitattributes",
|
19
26
|
".gitignore",
|
20
|
-
".gitleaksignore",
|
21
27
|
".pdd",
|
22
|
-
".xcop",
|
23
28
|
"Gemfile.lock",
|
24
29
|
"README.md",
|
25
30
|
"renovate.json",
|
data/Rakefile
CHANGED
@@ -15,7 +15,7 @@ def version
|
|
15
15
|
Gem::Specification.load(Dir['*.gemspec'].first).version
|
16
16
|
end
|
17
17
|
|
18
|
-
task default: %i[clean test rubocop yard
|
18
|
+
task default: %i[clean test rubocop yard]
|
19
19
|
|
20
20
|
require 'rake/testtask'
|
21
21
|
desc 'Run all unit tests'
|
@@ -42,7 +42,14 @@ end
|
|
42
42
|
|
43
43
|
desc 'Benchmark them all'
|
44
44
|
task :benchmark do
|
45
|
+
require_relative 'lib/factbase'
|
45
46
|
fb = Factbase.new
|
47
|
+
require_relative 'lib/factbase/cached/cached_factbase'
|
48
|
+
fb = Factbase::CachedFactbase.new(fb)
|
49
|
+
require_relative 'lib/factbase/indexed/indexed_factbase'
|
50
|
+
fb = Factbase::IndexedFactbase.new(fb)
|
51
|
+
require_relative 'lib/factbase/sync/sync_factbase'
|
52
|
+
fb = Factbase::SyncFactbase.new(fb)
|
46
53
|
require 'benchmark'
|
47
54
|
Benchmark.bm(60) do |b|
|
48
55
|
Dir['benchmark/bench_*.rb'].each do |f|
|
data/benchmark/bench_factbase.rb
CHANGED
@@ -0,0 +1,17 @@
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2024-2025 Yegor Bugayenko
|
2
|
+
# SPDX-License-Identifier: MIT
|
3
|
+
---
|
4
|
+
facts:
|
5
|
+
- name: Jeff
|
6
|
+
dob: 1979
|
7
|
+
- name: Walter
|
8
|
+
dob: 1976
|
9
|
+
- name: Maude
|
10
|
+
dob: 1983
|
11
|
+
queries:
|
12
|
+
- query: (agg (lt dob 1980) (max dob))
|
13
|
+
one: 1979
|
14
|
+
- query: (agg (always) (min dob))
|
15
|
+
one: 1976
|
16
|
+
- query: (agg (and (eq name "Jeff") (lt dob 2025)) (min dob))
|
17
|
+
one: 1979
|
@@ -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: Jeff Lebowski
|
7
|
+
age: 41
|
8
|
+
- name: Walter Sobchak
|
9
|
+
age: 45
|
10
|
+
- name: Maude Lebowski
|
11
|
+
age: 31
|
12
|
+
queries:
|
13
|
+
- query: (always)
|
14
|
+
size: 3
|
15
|
+
- query: (not (always))
|
16
|
+
size: 0
|
@@ -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: Jeff
|
7
|
+
dob: 1979
|
8
|
+
- name: Walter
|
9
|
+
dob: 1976
|
10
|
+
- name: Maude
|
11
|
+
dob: 1983
|
12
|
+
queries:
|
13
|
+
- query: (as age (minus 2025 dob))
|
14
|
+
size: 3
|
15
|
+
- query: (as gender "m/f")
|
16
|
+
size: 3
|
@@ -0,0 +1,18 @@
|
|
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
|
+
- name: Walter
|
9
|
+
age: 45
|
10
|
+
- name: Maude
|
11
|
+
age: 31
|
12
|
+
queries:
|
13
|
+
- query: (count)
|
14
|
+
one: 3
|
15
|
+
- query: (agg (eq name "Jeff") (count))
|
16
|
+
one: 1
|
17
|
+
- query: (agg (gt age 40) (count))
|
18
|
+
one: 2
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2024-2025 Yegor Bugayenko
|
2
|
+
# SPDX-License-Identifier: MIT
|
3
|
+
---
|
4
|
+
# yamllint disable rule:line-length
|
5
|
+
facts:
|
6
|
+
- foo: 42
|
7
|
+
bar: 33
|
8
|
+
- name: Jeff Lebowski
|
9
|
+
age: 41
|
10
|
+
- name: Maude
|
11
|
+
friends: [Jeff, Donny]
|
12
|
+
queries:
|
13
|
+
- query: (eq foo 42)
|
14
|
+
size: 1
|
15
|
+
- query: (not (eq foo 42))
|
16
|
+
size: 2
|
17
|
+
- query: (eq foo "42")
|
18
|
+
size: 0
|
19
|
+
- query: (not (eq foo "42"))
|
20
|
+
size: 3
|
21
|
+
- query: (eq foo 10)
|
22
|
+
size: 0
|
23
|
+
- query: (eq friends "Jeff")
|
24
|
+
size: 1
|
25
|
+
- query: (eq friends "Donny")
|
26
|
+
size: 1
|
27
|
+
- query: (eq name "Jeff Lebowski")
|
28
|
+
size: 1
|
29
|
+
- query: (eq bar "Hello")
|
30
|
+
size: 0
|
@@ -0,0 +1,18 @@
|
|
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 Lebowski
|
7
|
+
age: 41
|
8
|
+
- name: Walter Sobchak
|
9
|
+
age: 45
|
10
|
+
- name: Maude Lebowski
|
11
|
+
age: 31
|
12
|
+
queries:
|
13
|
+
- query: (gt age 40)
|
14
|
+
size: 2
|
15
|
+
- query: (not (gt age 40))
|
16
|
+
size: 1
|
17
|
+
- query: (and (gt age 30) (matches name "^M.*$"))
|
18
|
+
size: 1
|
@@ -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
|
+
- name: Walter
|
8
|
+
- name: Maude
|
9
|
+
- person: Jeff
|
10
|
+
age: 41
|
11
|
+
- person: Walter
|
12
|
+
age: 45
|
13
|
+
queries:
|
14
|
+
- query: (and (join 'age<=age' (eq person $name)) (exists age))
|
15
|
+
size: 4
|
16
|
+
- query: (and (join 'age' (eq person $name)) (exists age))
|
17
|
+
size: 4
|
18
|
+
- query: (and (join 'x<=age' (eq age (agg (exists person) (max age)))) (eq x 45))
|
19
|
+
size: 5
|
@@ -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
|
+
age: 41
|
8
|
+
- name: Walter
|
9
|
+
age: 45
|
10
|
+
- name: Maude
|
11
|
+
age: 31
|
12
|
+
queries:
|
13
|
+
- query: (max age)
|
14
|
+
one: 45
|
@@ -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 Lebowski
|
7
|
+
age: 41
|
8
|
+
- name: Walter Sobchak
|
9
|
+
age: 45
|
10
|
+
- name: Maude Lebowski
|
11
|
+
age: 31
|
12
|
+
queries:
|
13
|
+
- query: (min age)
|
14
|
+
one: 31
|
@@ -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
|
+
cities: [LA, SF]
|
8
|
+
- name: Walter
|
9
|
+
cities: [LA, SF, NYC]
|
10
|
+
queries:
|
11
|
+
- query: (eq "NYC" (agg (always) (nth 0 cities)))
|
12
|
+
size: 0
|
13
|
+
- query: (eq "NYC" (agg (always) (nth 1 cities)))
|
14
|
+
size: 2
|
@@ -0,0 +1,18 @@
|
|
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: BMW
|
7
|
+
price: 5600
|
8
|
+
- name: Ford
|
9
|
+
price: 3200
|
10
|
+
owner: Jeff
|
11
|
+
- name: Toyota
|
12
|
+
price: 9800
|
13
|
+
owners: [Jeff, Walter]
|
14
|
+
queries:
|
15
|
+
- query: (or (eq price 5600) (gt price 6000))
|
16
|
+
size: 2
|
17
|
+
- query: (or (eq owner "Jeff") (eq owners "Walter"))
|
18
|
+
size: 2
|
@@ -0,0 +1,12 @@
|
|
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
|
+
queries:
|
9
|
+
- query: (sprintf "Hello, %s!" "world")
|
10
|
+
one: "Hello, world!"
|
11
|
+
- query: (sprintf "Hello, %s!" (agg (eq age 41) (first name)))
|
12
|
+
one: "Hello, Jeff!"
|
@@ -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
|
+
- title: Object Thinking
|
7
|
+
price: 29
|
8
|
+
- name: Clean Code
|
9
|
+
price: 27
|
10
|
+
- name: Refactorings
|
11
|
+
price: 41
|
12
|
+
queries:
|
13
|
+
- query: (sum price)
|
14
|
+
one: 97
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) 2024-2025 Yegor Bugayenko
|
4
|
+
# SPDX-License-Identifier: MIT
|
5
|
+
|
6
|
+
require 'others'
|
7
|
+
require_relative '../../factbase'
|
8
|
+
|
9
|
+
# A single fact in a factbase, which is sentitive to changes.
|
10
|
+
#
|
11
|
+
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
12
|
+
# Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
|
13
|
+
# License:: MIT
|
14
|
+
class Factbase::CachedFact
|
15
|
+
# Ctor.
|
16
|
+
# @param [Factbase::Fact] origin The original fact
|
17
|
+
# @param [Hash] cache Cache of queries (to clean it on attribute addition)
|
18
|
+
def initialize(origin, cache)
|
19
|
+
@origin = origin
|
20
|
+
@cache = cache
|
21
|
+
end
|
22
|
+
|
23
|
+
# When a method is missing, this method is called.
|
24
|
+
others do |*args|
|
25
|
+
@cache.clear if args[0].to_s.end_with?('=')
|
26
|
+
@origin.send(*args)
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) 2024-2025 Yegor Bugayenko
|
4
|
+
# SPDX-License-Identifier: MIT
|
5
|
+
|
6
|
+
require 'decoor'
|
7
|
+
require_relative '../../factbase'
|
8
|
+
require_relative '../../factbase/syntax'
|
9
|
+
require_relative 'cached_fact'
|
10
|
+
require_relative 'cached_query'
|
11
|
+
require_relative 'cached_term'
|
12
|
+
|
13
|
+
# A factbase with a cache.
|
14
|
+
#
|
15
|
+
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
16
|
+
# Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
|
17
|
+
# License:: MIT
|
18
|
+
class Factbase::CachedFactbase
|
19
|
+
decoor(:origin)
|
20
|
+
|
21
|
+
# Constructor.
|
22
|
+
# @param [Factbase] origin Original factbase to decorate
|
23
|
+
# @param [Hash] cache Cache to use
|
24
|
+
def initialize(origin, cache = {})
|
25
|
+
raise 'Wront type of original' unless origin.respond_to?(:query)
|
26
|
+
@origin = origin
|
27
|
+
raise 'Wront type of cache' unless cache.is_a?(Hash)
|
28
|
+
@cache = cache
|
29
|
+
end
|
30
|
+
|
31
|
+
# Insert a new fact and return it.
|
32
|
+
# @return [Factbase::Fact] The fact just inserted
|
33
|
+
def insert
|
34
|
+
@cache.clear
|
35
|
+
Factbase::CachedFact.new(@origin.insert, @cache)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Convert a query to a term.
|
39
|
+
# @param [String] query The query to convert
|
40
|
+
# @return [Factbase::Term] The term
|
41
|
+
def to_term(query)
|
42
|
+
t = @origin.to_term(query)
|
43
|
+
t.redress!(Factbase::CachedTerm, cache: @cache)
|
44
|
+
t
|
45
|
+
end
|
46
|
+
|
47
|
+
# Create a query capable of iterating.
|
48
|
+
# @param [String] term The term to use
|
49
|
+
# @param [Array<Hash>] maps Possible maps to use
|
50
|
+
def query(term, maps = nil)
|
51
|
+
term = to_term(term) if term.is_a?(String)
|
52
|
+
q = @origin.query(term, maps)
|
53
|
+
q = Factbase::CachedQuery.new(q, @cache, self) unless term.abstract?
|
54
|
+
q
|
55
|
+
end
|
56
|
+
|
57
|
+
# Run an ACID transaction.
|
58
|
+
# @return [Factbase::Churn] How many facts have been changed (zero if rolled back)
|
59
|
+
def txn
|
60
|
+
@origin.txn do |fbt|
|
61
|
+
yield Factbase::CachedFactbase.new(fbt, @cache)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) 2024-2025 Yegor Bugayenko
|
4
|
+
# SPDX-License-Identifier: MIT
|
5
|
+
|
6
|
+
require_relative '../../factbase'
|
7
|
+
|
8
|
+
# Query with a cache, a decorator of another query.
|
9
|
+
#
|
10
|
+
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
11
|
+
# Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
|
12
|
+
# License:: MIT
|
13
|
+
class Factbase::CachedQuery
|
14
|
+
# Constructor.
|
15
|
+
# @param [Factbase::Query] origin Original query
|
16
|
+
# @param [Hash] cache The cache
|
17
|
+
def initialize(origin, cache, fb)
|
18
|
+
@origin = origin
|
19
|
+
@cache = cache
|
20
|
+
@fb = fb
|
21
|
+
end
|
22
|
+
|
23
|
+
# Print it as a string.
|
24
|
+
# @return [String] The query as a string
|
25
|
+
def to_s
|
26
|
+
@origin.to_s
|
27
|
+
end
|
28
|
+
|
29
|
+
# Iterate facts one by one.
|
30
|
+
# @param [Hash] params Optional params accessible in the query via the "$" symbol
|
31
|
+
# @yield [Fact] Facts one-by-one
|
32
|
+
# @return [Integer] Total number of facts yielded
|
33
|
+
def each(fb = @fb, params = nil)
|
34
|
+
return to_enum(__method__, fb, params) unless block_given?
|
35
|
+
key = "each #{@origin}"
|
36
|
+
before = @cache[key]
|
37
|
+
@cache[key] = @origin.each(fb).to_a if before.nil?
|
38
|
+
@cache[key].each do |f|
|
39
|
+
require_relative 'cached_fact'
|
40
|
+
yield Factbase::CachedFact.new(f, @cache)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Read a single value.
|
45
|
+
# @param [Hash] fb The factbase
|
46
|
+
# @param [Hash] _params Optional params accessible in the query via the "$" symbol (unused)
|
47
|
+
# @return The value evaluated
|
48
|
+
def one(fb = @fb, _params = nil)
|
49
|
+
key = "one: #{@origin}"
|
50
|
+
before = @cache[key]
|
51
|
+
@cache[key] = @origin.one(fb) if before.nil?
|
52
|
+
@cache[key]
|
53
|
+
end
|
54
|
+
|
55
|
+
# Delete all facts that match the query.
|
56
|
+
# @return [Integer] Total number of facts deleted
|
57
|
+
def delete!(fb = @fb)
|
58
|
+
@cache.clear
|
59
|
+
@origin.delete!(fb)
|
60
|
+
end
|
61
|
+
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
|
+
require_relative '../../factbase'
|
7
|
+
|
8
|
+
# Term with a cache.
|
9
|
+
#
|
10
|
+
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
11
|
+
# Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
|
12
|
+
# License:: MIT
|
13
|
+
module Factbase::CachedTerm
|
14
|
+
# Does it match the fact?
|
15
|
+
# @param [Factbase::Fact] fact The fact
|
16
|
+
# @param [Array<Factbase::Fact>] maps All maps available
|
17
|
+
# @return [bool] TRUE if matches
|
18
|
+
def evaluate(fact, maps, fb)
|
19
|
+
return super unless static? && !abstract?
|
20
|
+
key = [maps.object_id, to_s]
|
21
|
+
before = @cache[key]
|
22
|
+
@cache[key] = super if before.nil?
|
23
|
+
@cache[key]
|
24
|
+
end
|
25
|
+
end
|
data/lib/factbase/fact.rb
CHANGED
@@ -10,28 +10,27 @@ require_relative '../factbase'
|
|
10
10
|
|
11
11
|
# A single fact in a factbase.
|
12
12
|
#
|
13
|
-
# This is an internal class, it is
|
14
|
-
#
|
13
|
+
# This is an internal class, it is supposed to be instantiated only by the
|
14
|
+
# +Factbase+ class.
|
15
15
|
# However, it is possible to use it for testing directly, for example to make a
|
16
16
|
# fact with a single key/value pair inside:
|
17
17
|
#
|
18
18
|
# require 'factbase/fact'
|
19
|
-
# f = Factbase::Fact.new(
|
19
|
+
# f = Factbase::Fact.new({ 'foo' => [42, 256, 'Hello, world!'] })
|
20
20
|
# assert_equal(42, f.foo)
|
21
21
|
#
|
22
22
|
# A fact is basically a key/value hash map, where values are non-empty
|
23
23
|
# sets of values.
|
24
24
|
#
|
25
|
+
# It is NOT thread-safe!
|
26
|
+
#
|
25
27
|
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
26
28
|
# Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
|
27
29
|
# License:: MIT
|
28
30
|
class Factbase::Fact
|
29
31
|
# Ctor.
|
30
|
-
# @param [Mutex] mutex A mutex to use for maps synchronization
|
31
32
|
# @param [Hash] map A map of key/value pairs
|
32
|
-
def initialize(
|
33
|
-
@fb = fb
|
34
|
-
@mutex = mutex
|
33
|
+
def initialize(map)
|
35
34
|
@map = map
|
36
35
|
end
|
37
36
|
|
@@ -48,6 +47,10 @@ class Factbase::Fact
|
|
48
47
|
end
|
49
48
|
|
50
49
|
# When a method is missing, this method is called.
|
50
|
+
# Method missing handler for dynamic property access and setting
|
51
|
+
# @param [Symbol] method The method name being called
|
52
|
+
# @param [Array] args Method arguments
|
53
|
+
# @return [Object] The value retrieved or nil if setting a value
|
51
54
|
others do |*args|
|
52
55
|
k = args[0].to_s
|
53
56
|
if k.end_with?('=')
|
@@ -59,12 +62,9 @@ class Factbase::Fact
|
|
59
62
|
raise "The value of '#{kk}' can't be empty" if v == ''
|
60
63
|
raise "The type '#{v.class}' of '#{kk}' is not allowed" unless [String, Integer, Float, Time].include?(v.class)
|
61
64
|
v = v.utc if v.is_a?(Time)
|
62
|
-
@
|
63
|
-
|
64
|
-
|
65
|
-
@map[kk].uniq!
|
66
|
-
end
|
67
|
-
@fb.cache.clear
|
65
|
+
@map[kk] = [] if @map[kk].nil?
|
66
|
+
@map[kk] << v
|
67
|
+
@map[kk].uniq!
|
68
68
|
nil
|
69
69
|
elsif k == '[]'
|
70
70
|
@map[args[1].to_s]
|