factbase 0.19.11 → 0.19.12
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 +5 -4
- data/Gemfile.lock +16 -12
- data/README.md +49 -18
- data/Rakefile +2 -7
- data/factbase.gemspec +11 -11
- data/lib/factbase/accum.rb +1 -1
- data/lib/factbase/cached/cached_fact.rb +1 -2
- data/lib/factbase/cached/cached_factbase.rb +3 -3
- data/lib/factbase/cached/cached_query.rb +4 -6
- data/lib/factbase/cached/cached_term.rb +1 -2
- data/lib/factbase/churn.rb +4 -8
- data/lib/factbase/fact.rb +12 -9
- data/lib/factbase/flatten.rb +2 -2
- data/lib/factbase/impatient.rb +14 -13
- data/lib/factbase/indexed/indexed_and.rb +14 -20
- data/lib/factbase/indexed/indexed_eq.rb +5 -1
- data/lib/factbase/indexed/indexed_fact.rb +1 -4
- data/lib/factbase/indexed/indexed_factbase.rb +4 -4
- data/lib/factbase/indexed/indexed_gt.rb +3 -1
- data/lib/factbase/indexed/indexed_gte.rb +51 -0
- data/lib/factbase/indexed/indexed_lt.rb +3 -1
- data/lib/factbase/indexed/indexed_lte.rb +51 -0
- data/lib/factbase/indexed/indexed_not.rb +1 -1
- data/lib/factbase/indexed/indexed_or.rb +2 -2
- data/lib/factbase/indexed/indexed_query.rb +6 -7
- data/lib/factbase/indexed/indexed_term.rb +10 -6
- data/lib/factbase/indexed/indexed_unique.rb +4 -2
- data/lib/factbase/inv.rb +3 -3
- data/lib/factbase/lazy_taped.rb +10 -13
- data/lib/factbase/lazy_taped_hash.rb +2 -1
- data/lib/factbase/light.rb +1 -1
- data/lib/factbase/logged.rb +37 -34
- data/lib/factbase/pre.rb +3 -3
- data/lib/factbase/query.rb +4 -5
- data/lib/factbase/rules.rb +8 -8
- data/lib/factbase/sync/sync_factbase.rb +2 -2
- data/lib/factbase/syntax.rb +18 -19
- data/lib/factbase/tallied.rb +6 -7
- data/lib/factbase/taped.rb +5 -11
- data/lib/factbase/tee.rb +2 -2
- data/lib/factbase/term.rb +53 -60
- data/lib/factbase/terms/agg.rb +3 -4
- data/lib/factbase/terms/arithmetic.rb +7 -7
- data/lib/factbase/terms/as.rb +2 -2
- data/lib/factbase/terms/assert.rb +5 -13
- data/lib/factbase/terms/base.rb +6 -7
- data/lib/factbase/terms/best.rb +1 -1
- data/lib/factbase/terms/boolean.rb +1 -1
- data/lib/factbase/terms/compare.rb +2 -1
- data/lib/factbase/terms/defn.rb +8 -6
- data/lib/factbase/terms/empty.rb +1 -1
- data/lib/factbase/terms/first.rb +2 -2
- data/lib/factbase/terms/head.rb +3 -3
- data/lib/factbase/terms/inverted.rb +2 -2
- data/lib/factbase/terms/join.rb +8 -7
- data/lib/factbase/terms/matches.rb +4 -4
- data/lib/factbase/terms/max.rb +1 -1
- data/lib/factbase/terms/min.rb +1 -1
- data/lib/factbase/terms/nth.rb +3 -3
- data/lib/factbase/terms/plus.rb +1 -1
- data/lib/factbase/terms/prev.rb +3 -6
- data/lib/factbase/terms/sorted.rb +2 -2
- data/lib/factbase/terms/sprintf.rb +5 -4
- data/lib/factbase/terms/sum.rb +1 -1
- data/lib/factbase/terms/to_float.rb +2 -2
- data/lib/factbase/terms/to_integer.rb +2 -2
- data/lib/factbase/terms/to_string.rb +1 -1
- data/lib/factbase/terms/to_time.rb +2 -2
- data/lib/factbase/terms/traced.rb +2 -2
- data/lib/factbase/terms/undef.rb +2 -2
- data/lib/factbase/terms/unique.rb +3 -7
- data/lib/factbase/to_json.rb +1 -1
- data/lib/factbase/to_xml.rb +5 -9
- data/lib/factbase/to_yaml.rb +1 -1
- data/lib/factbase/version.rb +1 -2
- data/lib/factbase.rb +27 -10
- data/lib/fuzz.rb +3 -3
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f97152f34dd9eb3ba72cc5380a87d321bbec59c316078108b38c2c078c23a0fd
|
|
4
|
+
data.tar.gz: 0f678a254dc256d1983965742771c37d0956a89f93ec91e098f63b4d2957117a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a8ebb9cd303ab0a6b34842ec06fa2d5f4ed1ec1d2c8fff89e1068531782386c1c6da2bba4f2d116635ca37532d314c25626fe0fdcac03a10ead6b57871483485
|
|
7
|
+
data.tar.gz: 1f2be490a544b5417059853ed135011c85215bbea7fb9b96551bcd67badc32e20a9384acf97d6a3b4908e63332937bb4502df168c79aa19ed95afc1a364ee4bd
|
data/Gemfile
CHANGED
|
@@ -10,16 +10,17 @@ gem 'benchmark', '~>0.5', require: false
|
|
|
10
10
|
gem 'minitest', '~>6.0', require: false
|
|
11
11
|
gem 'minitest-reporters', '~>1.7', require: false
|
|
12
12
|
gem 'os', '~>1.1', require: false
|
|
13
|
-
gem 'psych', '5.3.1', require: false
|
|
13
|
+
gem 'psych', '5.3.1', require: false
|
|
14
14
|
gem 'qbash', '~>0.4', require: false
|
|
15
15
|
gem 'rake', '~>13.2', require: false
|
|
16
|
-
gem 'rdoc', '7.1.0', require: false
|
|
16
|
+
gem 'rdoc', '7.1.0', require: false
|
|
17
17
|
gem 'rubocop', '~>1.74', require: false
|
|
18
|
+
gem 'rubocop-elegant', '~>0.5', require: false
|
|
18
19
|
gem 'rubocop-minitest', '~>0.38', require: false
|
|
19
20
|
gem 'rubocop-performance', '~>1.25', require: false
|
|
20
21
|
gem 'rubocop-rake', '~>0.7', require: false
|
|
21
22
|
gem 'simplecov', '~>0.22', require: false
|
|
22
23
|
gem 'simplecov-cobertura', '~>3.0', require: false
|
|
23
|
-
gem 'stackprof', '0.2.27', require: false, platforms: [:ruby]
|
|
24
|
+
gem 'stackprof', '0.2.27', require: false, platforms: [:ruby]
|
|
24
25
|
gem 'threads', '~>0.4', require: false
|
|
25
|
-
gem 'yard', '0.9.38', require: false
|
|
26
|
+
gem 'yard', '0.9.38', require: false
|
data/Gemfile.lock
CHANGED
|
@@ -30,8 +30,8 @@ GEM
|
|
|
30
30
|
loog (~> 0.6)
|
|
31
31
|
tago (~> 0.1)
|
|
32
32
|
ellipsized (0.3.0)
|
|
33
|
-
erb (6.0.
|
|
34
|
-
json (2.
|
|
33
|
+
erb (6.0.4)
|
|
34
|
+
json (2.19.5)
|
|
35
35
|
language_server-protocol (3.17.0.5)
|
|
36
36
|
lint_roller (1.1.0)
|
|
37
37
|
logger (1.7.0)
|
|
@@ -46,18 +46,18 @@ GEM
|
|
|
46
46
|
builder
|
|
47
47
|
minitest (>= 5.0, < 7)
|
|
48
48
|
ruby-progressbar
|
|
49
|
-
nokogiri (1.19.
|
|
49
|
+
nokogiri (1.19.3-arm64-darwin)
|
|
50
50
|
racc (~> 1.4)
|
|
51
|
-
nokogiri (1.19.
|
|
51
|
+
nokogiri (1.19.3-x64-mingw-ucrt)
|
|
52
52
|
racc (~> 1.4)
|
|
53
|
-
nokogiri (1.19.
|
|
53
|
+
nokogiri (1.19.3-x86_64-darwin)
|
|
54
54
|
racc (~> 1.4)
|
|
55
|
-
nokogiri (1.19.
|
|
55
|
+
nokogiri (1.19.3-x86_64-linux-gnu)
|
|
56
56
|
racc (~> 1.4)
|
|
57
57
|
os (1.1.4)
|
|
58
58
|
others (0.1.1)
|
|
59
|
-
parallel (1.
|
|
60
|
-
parser (3.3.
|
|
59
|
+
parallel (2.1.0)
|
|
60
|
+
parser (3.3.11.1)
|
|
61
61
|
ast (~> 2.4.1)
|
|
62
62
|
racc
|
|
63
63
|
prism (1.9.0)
|
|
@@ -76,22 +76,25 @@ GEM
|
|
|
76
76
|
erb
|
|
77
77
|
psych (>= 4.0.0)
|
|
78
78
|
tsort
|
|
79
|
-
regexp_parser (2.
|
|
79
|
+
regexp_parser (2.12.0)
|
|
80
80
|
rexml (3.4.4)
|
|
81
|
-
rubocop (1.
|
|
81
|
+
rubocop (1.86.2)
|
|
82
82
|
json (~> 2.3)
|
|
83
83
|
language_server-protocol (~> 3.17.0.2)
|
|
84
84
|
lint_roller (~> 1.1.0)
|
|
85
|
-
parallel (
|
|
85
|
+
parallel (>= 1.10)
|
|
86
86
|
parser (>= 3.3.0.2)
|
|
87
87
|
rainbow (>= 2.2.2, < 4.0)
|
|
88
88
|
regexp_parser (>= 2.9.3, < 3.0)
|
|
89
89
|
rubocop-ast (>= 1.49.0, < 2.0)
|
|
90
90
|
ruby-progressbar (~> 1.7)
|
|
91
91
|
unicode-display_width (>= 2.4.0, < 4.0)
|
|
92
|
-
rubocop-ast (1.49.
|
|
92
|
+
rubocop-ast (1.49.1)
|
|
93
93
|
parser (>= 3.3.7.2)
|
|
94
94
|
prism (~> 1.7)
|
|
95
|
+
rubocop-elegant (0.5.1)
|
|
96
|
+
lint_roller (~> 1.1)
|
|
97
|
+
rubocop (~> 1.75)
|
|
95
98
|
rubocop-minitest (0.39.1)
|
|
96
99
|
lint_roller (~> 1.1)
|
|
97
100
|
rubocop (>= 1.75.0, < 2.0)
|
|
@@ -148,6 +151,7 @@ DEPENDENCIES
|
|
|
148
151
|
rake (~> 13.2)
|
|
149
152
|
rdoc (= 7.1.0)
|
|
150
153
|
rubocop (~> 1.74)
|
|
154
|
+
rubocop-elegant (~> 0.5)
|
|
151
155
|
rubocop-minitest (~> 0.38)
|
|
152
156
|
rubocop-performance (~> 1.25)
|
|
153
157
|
rubocop-rake (~> 0.7)
|
data/README.md
CHANGED
|
@@ -11,7 +11,10 @@
|
|
|
11
11
|
[](https://rubydoc.info/github/yegor256/factbase/master/frames)
|
|
12
12
|
[](https://hitsofcode.com/view/github/yegor256/factbase)
|
|
13
13
|
[](https://github.com/yegor256/factbase/blob/master/LICENSE.txt)
|
|
14
|
-
[](https://app.fossa.com/projects/git%2Bgithub
|
|
14
|
+
[](https://app.fossa.com/projects/git%2Bgithub%2Fyegor256%2Ffactbase?ref=badge_shield&issueType=license)
|
|
15
|
+
|
|
16
|
+
> **WARNING:** `Factbase` is **NOT** thread-safe. Use `Factbase::SyncFactbase`
|
|
17
|
+
> for concurrent access from multiple threads.
|
|
15
18
|
|
|
16
19
|
This Ruby gem manages an in-memory database of facts.
|
|
17
20
|
A fact is simply an associative array of properties and their values.
|
|
@@ -19,7 +22,8 @@ The values are either atomic literals or non-empty sets of literals.
|
|
|
19
22
|
It is possible to delete a fact, but impossible to delete a property
|
|
20
23
|
from a fact.
|
|
21
24
|
|
|
22
|
-
Here is how you use it (it
|
|
25
|
+
Here is how you use it (wrap it with `Factbase::SyncFactbase`
|
|
26
|
+
for thread-safe access):
|
|
23
27
|
|
|
24
28
|
```ruby
|
|
25
29
|
fb = Factbase.new
|
|
@@ -119,6 +123,27 @@ end
|
|
|
119
123
|
assert(0 == fb.size)
|
|
120
124
|
```
|
|
121
125
|
|
|
126
|
+
## Query Parameters
|
|
127
|
+
|
|
128
|
+
You can use `$name` variables in queries and pass values at runtime:
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
fb = Factbase.new
|
|
132
|
+
fb.insert.foo = 42
|
|
133
|
+
fb.insert.foo = 99
|
|
134
|
+
fb.query('(eq foo $bar)').each(Factbase.new, bar: [42]).to_a
|
|
135
|
+
# Returns the fact with foo = 42
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Variables are passed as keyword arguments to `each` or `one`. The value must
|
|
139
|
+
be an array (even for a single value). This works with any term:
|
|
140
|
+
|
|
141
|
+
```ruby
|
|
142
|
+
fb.query(
|
|
143
|
+
'(eq salary (agg (eq dept $dept) (avg salary)))'
|
|
144
|
+
).each(Factbase.new, dept: ['eng']).to_a
|
|
145
|
+
```
|
|
146
|
+
|
|
122
147
|
## Terms
|
|
123
148
|
|
|
124
149
|
There are some boolean terms available in a query
|
|
@@ -293,7 +318,13 @@ Persistence uses Ruby's
|
|
|
293
318
|
platforms, unlike [JSON](https://www.json.org/json-en.html) or
|
|
294
319
|
[Protocol Buffers](https://protobuf.dev/). Output-only decorators
|
|
295
320
|
`Factbase::ToJSON`, `Factbase::ToXML`, and `Factbase::ToYAML` exist but
|
|
296
|
-
do not support round-trip import.
|
|
321
|
+
do not support round-trip import. Because `import` calls `Marshal.load`
|
|
322
|
+
on the incoming bytes, the input must come from a source the caller
|
|
323
|
+
trusts; a `Marshal` stream crafted by an attacker can execute arbitrary
|
|
324
|
+
code in the calling process, so factbase blobs received over the
|
|
325
|
+
network or read from a user-supplied path should be authenticated
|
|
326
|
+
out-of-band before being imported, as described in the
|
|
327
|
+
[Ruby security notes](https://docs.ruby-lang.org/en/3.3/security_rdoc.html#label-Marshal.load).
|
|
297
328
|
|
|
298
329
|
`Factbase::IndexedFactbase` lazily builds a hash-based inverted index for
|
|
299
330
|
equality queries, keyed by array `object_id`, property name, and
|
|
@@ -326,24 +357,24 @@ This is the result of the benchmark:
|
|
|
326
357
|
<!-- benchmark_begin -->
|
|
327
358
|
```text
|
|
328
359
|
user
|
|
329
|
-
void scan 0.
|
|
330
|
-
20k facts: export:
|
|
331
|
-
20k facts: import:
|
|
332
|
-
50k facts: read 0.
|
|
333
|
-
50k facts: read in txn 0.
|
|
334
|
-
50k facts: insert 0.
|
|
335
|
-
50k facts: insert in txn 0.
|
|
336
|
-
50k facts: modify 1.
|
|
337
|
-
50k facts: modify in txn
|
|
338
|
-
12k facts: large query: match 3k 13.
|
|
339
|
-
12k facts: large query: match 3k in txn 18.
|
|
340
|
-
12k facts: large query: match zero
|
|
341
|
-
12k facts: large query: match zero in txn 19.
|
|
360
|
+
void scan 0.001115
|
|
361
|
+
20k facts: export: 2979KB 0.979531
|
|
362
|
+
20k facts: import: 2979KB 1.055363
|
|
363
|
+
50k facts: read 0.000212
|
|
364
|
+
50k facts: read in txn 0.001724
|
|
365
|
+
50k facts: insert 0.000081
|
|
366
|
+
50k facts: insert in txn 0.000226
|
|
367
|
+
50k facts: modify 1.604103
|
|
368
|
+
50k facts: modify in txn 3.126437
|
|
369
|
+
12k facts: large query: match 3k 13.220472
|
|
370
|
+
12k facts: large query: match 3k in txn 18.484006
|
|
371
|
+
12k facts: large query: match zero 13.730576
|
|
372
|
+
12k facts: large query: match zero in txn 19.485743
|
|
342
373
|
```
|
|
343
374
|
|
|
344
375
|
The results were calculated in [this GHA job][benchmark-gha]
|
|
345
|
-
on 2026-
|
|
376
|
+
on 2026-06-14 at 16:40,
|
|
346
377
|
on Linux with 4 CPUs.
|
|
347
378
|
<!-- benchmark_end -->
|
|
348
379
|
|
|
349
|
-
[benchmark-gha]: https://github.com/yegor256/factbase/actions/runs/
|
|
380
|
+
[benchmark-gha]: https://github.com/yegor256/factbase/actions/runs/27505290212
|
data/Rakefile
CHANGED
|
@@ -63,7 +63,7 @@ end
|
|
|
63
63
|
desc 'Benchmark them all'
|
|
64
64
|
task :benchmark, [:name, :cycles] do |_t, args|
|
|
65
65
|
bname = args[:name] || 'essential'
|
|
66
|
-
cycles = (args[:cycles] || 5)
|
|
66
|
+
cycles = Integer(args[:cycles] || 5)
|
|
67
67
|
require_relative 'lib/factbase'
|
|
68
68
|
require_relative 'lib/factbase/cached/cached_factbase'
|
|
69
69
|
require_relative 'lib/factbase/indexed/indexed_factbase'
|
|
@@ -84,16 +84,11 @@ task :benchmark, [:name, :cycles] do |_t, args|
|
|
|
84
84
|
fb = Factbase.new
|
|
85
85
|
fb = Factbase::IndexedFactbase.new(fb)
|
|
86
86
|
fb = Factbase::CachedFactbase.new(fb)
|
|
87
|
-
Kernel.
|
|
87
|
+
Kernel.__send__(File.basename(f).gsub(/\.rb$/, '').to_sym, b, fb, cycles)
|
|
88
88
|
end
|
|
89
89
|
end
|
|
90
90
|
end
|
|
91
91
|
|
|
92
|
-
# Run profiling on a benchmark and generate a flamegraph.
|
|
93
|
-
# To run this task, you need to have stackprof installed.
|
|
94
|
-
# https://github.com/tmm1/stackprof
|
|
95
|
-
# To run profiling for a specific benchmark you can run:
|
|
96
|
-
# bundle exec rake flamegraph\[bench_slow_query\]
|
|
97
92
|
desc 'Profile a benchmark (e.g., flamegraph[bench_slow_query])'
|
|
98
93
|
task :flamegraph, [:name] do |_t, args|
|
|
99
94
|
require 'stackprof'
|
data/factbase.gemspec
CHANGED
|
@@ -7,7 +7,7 @@ require 'English'
|
|
|
7
7
|
require_relative 'lib/factbase/version'
|
|
8
8
|
|
|
9
9
|
Gem::Specification.new do |s|
|
|
10
|
-
s.required_rubygems_version = Gem::Requirement.new('>= 0') if s.respond_to?
|
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new('>= 0') if s.respond_to?(:required_rubygems_version=)
|
|
11
11
|
s.required_ruby_version = '>=3.0'
|
|
12
12
|
s.name = 'factbase'
|
|
13
13
|
s.version = Factbase::VERSION
|
|
@@ -25,15 +25,15 @@ Gem::Specification.new do |s|
|
|
|
25
25
|
s.files = `git ls-files | grep -v -E '^(test/|\\.|fixtures|benchmark|renovate)'`.split($RS)
|
|
26
26
|
s.rdoc_options = ['--charset=UTF-8']
|
|
27
27
|
s.extra_rdoc_files = ['README.md', 'LICENSE.txt']
|
|
28
|
-
s.add_dependency
|
|
29
|
-
s.add_dependency
|
|
30
|
-
s.add_dependency
|
|
31
|
-
s.add_dependency
|
|
32
|
-
s.add_dependency
|
|
33
|
-
s.add_dependency
|
|
34
|
-
s.add_dependency
|
|
35
|
-
s.add_dependency
|
|
36
|
-
s.add_dependency
|
|
37
|
-
s.add_dependency
|
|
28
|
+
s.add_dependency('backtrace', '~>0.4')
|
|
29
|
+
s.add_dependency('decoor', '~>0.1')
|
|
30
|
+
s.add_dependency('ellipsized', '~>0.3')
|
|
31
|
+
s.add_dependency('json', '~>2.7')
|
|
32
|
+
s.add_dependency('logger', '~>1.0')
|
|
33
|
+
s.add_dependency('loog', '~>0.6')
|
|
34
|
+
s.add_dependency('nokogiri', '~>1.10')
|
|
35
|
+
s.add_dependency('others', '~>0.1')
|
|
36
|
+
s.add_dependency('tago', '~>0.1')
|
|
37
|
+
s.add_dependency('yaml', '~>0.3')
|
|
38
38
|
s.metadata['rubygems_mfa_required'] = 'true'
|
|
39
39
|
end
|
data/lib/factbase/accum.rb
CHANGED
|
@@ -22,9 +22,9 @@ class Factbase::CachedFactbase
|
|
|
22
22
|
# @param [Factbase] origin Original factbase to decorate
|
|
23
23
|
# @param [Hash] cache Cache to use
|
|
24
24
|
def initialize(origin, cache = {})
|
|
25
|
-
raise 'Wrong type of original' unless origin.respond_to?(:query)
|
|
25
|
+
raise(ArgumentError, 'Wrong type of original') unless origin.respond_to?(:query)
|
|
26
26
|
@origin = origin
|
|
27
|
-
raise 'Wrong type of cache' unless cache.is_a?(Hash)
|
|
27
|
+
raise(ArgumentError, 'Wrong type of cache') unless cache.is_a?(Hash)
|
|
28
28
|
@cache = cache
|
|
29
29
|
end
|
|
30
30
|
|
|
@@ -58,7 +58,7 @@ class Factbase::CachedFactbase
|
|
|
58
58
|
# @return [Factbase::Churn] How many facts have been changed (zero if rolled back)
|
|
59
59
|
def txn
|
|
60
60
|
@origin.txn do |fbt|
|
|
61
|
-
yield
|
|
61
|
+
yield(Factbase::CachedFactbase.new(fbt, @cache))
|
|
62
62
|
end
|
|
63
63
|
end
|
|
64
64
|
end
|
|
@@ -36,13 +36,12 @@ class Factbase::CachedQuery
|
|
|
36
36
|
def each(fb = @fb, params = {})
|
|
37
37
|
return to_enum(__method__, fb, params) unless block_given?
|
|
38
38
|
invalidate_if_dirty!
|
|
39
|
-
key = "each #{@origin}"
|
|
40
|
-
|
|
41
|
-
@cache[key] = @origin.each(fb, params).to_a if before.nil?
|
|
39
|
+
key = "each #{@origin}"
|
|
40
|
+
@cache[key] = @origin.each(fb, params).to_a if @cache[key].nil?
|
|
42
41
|
c = 0
|
|
43
42
|
@cache[key].each do |f|
|
|
44
43
|
c += 1
|
|
45
|
-
yield
|
|
44
|
+
yield(Factbase::CachedFact.new(f, @cache))
|
|
46
45
|
end
|
|
47
46
|
c
|
|
48
47
|
end
|
|
@@ -54,8 +53,7 @@ class Factbase::CachedQuery
|
|
|
54
53
|
def one(fb = @fb, params = {})
|
|
55
54
|
invalidate_if_dirty!
|
|
56
55
|
key = "one: #{@origin} #{params}"
|
|
57
|
-
|
|
58
|
-
@cache[key] = @origin.one(fb, params) if before.nil?
|
|
56
|
+
@cache[key] = @origin.one(fb, params) if @cache[key].nil?
|
|
59
57
|
@cache[key]
|
|
60
58
|
end
|
|
61
59
|
|
|
@@ -19,8 +19,7 @@ module Factbase::CachedTerm
|
|
|
19
19
|
return super unless static? && !abstract?
|
|
20
20
|
return super if %i[head unique].include?(@op)
|
|
21
21
|
key = [maps.object_id, to_s]
|
|
22
|
-
|
|
23
|
-
@cache[key] = super if before.nil?
|
|
22
|
+
@cache[key] = super if @cache[key].nil?
|
|
24
23
|
@cache[key]
|
|
25
24
|
end
|
|
26
25
|
end
|
data/lib/factbase/churn.rb
CHANGED
|
@@ -22,16 +22,16 @@ class Factbase::Churn
|
|
|
22
22
|
if zero?
|
|
23
23
|
'nothing'
|
|
24
24
|
else
|
|
25
|
-
"#{
|
|
25
|
+
"#{inserted}i/#{deleted}d/#{added}a"
|
|
26
26
|
end
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
def zero?
|
|
30
|
-
|
|
30
|
+
inserted.zero? && deleted.zero? && added.zero?
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
def to_i
|
|
34
|
-
|
|
34
|
+
inserted + deleted + added
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
def append(ins, del, add)
|
|
@@ -43,10 +43,6 @@ class Factbase::Churn
|
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
def +(other)
|
|
46
|
-
Factbase::Churn.new(
|
|
47
|
-
@inserted + other.inserted,
|
|
48
|
-
@deleted + other.deleted,
|
|
49
|
-
@added + other.added
|
|
50
|
-
)
|
|
46
|
+
Factbase::Churn.new(inserted + other.inserted, deleted + other.deleted, added + other.added)
|
|
51
47
|
end
|
|
52
48
|
end
|
data/lib/factbase/fact.rb
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
# SPDX-License-Identifier: MIT
|
|
5
5
|
|
|
6
6
|
require 'json'
|
|
7
|
-
require 'time'
|
|
8
7
|
require 'others'
|
|
8
|
+
require 'time'
|
|
9
9
|
require_relative '../factbase'
|
|
10
10
|
|
|
11
11
|
# A single fact in a factbase.
|
|
@@ -37,7 +37,7 @@ class Factbase::Fact
|
|
|
37
37
|
# Convert it to a string.
|
|
38
38
|
# @return [String] String representation of it (in JSON)
|
|
39
39
|
def to_s
|
|
40
|
-
"[ #{@map.map { |k, v| "#{k}: #{v}" }.sort
|
|
40
|
+
"[ #{@map.map { |k, v| "#{k}: #{v}" }.sort!.join(', ')} ]"
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
# Get a list of all props.
|
|
@@ -50,12 +50,15 @@ class Factbase::Fact
|
|
|
50
50
|
k = args[0].to_s
|
|
51
51
|
if k.end_with?('=')
|
|
52
52
|
kk = k[0..-2]
|
|
53
|
-
raise "Invalid prop name '#{kk}'" unless kk.match?(/^[a-z_][_a-zA-Z0-9]*$/)
|
|
54
|
-
raise "Prohibited prop name '#{kk}'" if methods.include?(kk.to_sym)
|
|
53
|
+
raise(ArgumentError, "Invalid prop name '#{kk}'") unless kk.match?(/^[a-z_][_a-zA-Z0-9]*$/)
|
|
54
|
+
raise(ArgumentError, "Prohibited prop name '#{kk}'") if methods.include?(kk.to_sym)
|
|
55
55
|
v = args[1]
|
|
56
|
-
raise "The value of '#{kk}' can't be nil" if v.nil?
|
|
57
|
-
raise "The value of '#{kk}' can't be empty" if v == ''
|
|
58
|
-
raise "The type '#{v.class}' of '#{kk}' is not allowed" unless [
|
|
56
|
+
raise(ArgumentError, "The value of '#{kk}' can't be nil") if v.nil?
|
|
57
|
+
raise(ArgumentError, "The value of '#{kk}' can't be empty") if v == ''
|
|
58
|
+
raise(ArgumentError, "The type '#{v.class}' of '#{kk}' is not allowed") unless [
|
|
59
|
+
String, Integer, Float,
|
|
60
|
+
Time
|
|
61
|
+
].include?(v.class)
|
|
59
62
|
v = v.utc if v.is_a?(Time)
|
|
60
63
|
@map[kk] = [] if @map[kk].nil?
|
|
61
64
|
@map[kk] << v
|
|
@@ -65,8 +68,8 @@ class Factbase::Fact
|
|
|
65
68
|
else
|
|
66
69
|
v = @map[k]
|
|
67
70
|
if v.nil?
|
|
68
|
-
raise "Can't get '#{k}', the fact is empty" if @map.empty?
|
|
69
|
-
raise "Can't find '#{k}' attribute out of [#{@map.keys.join(', ')}]"
|
|
71
|
+
raise(ArgumentError, "Can't get '#{k}', the fact is empty") if @map.empty?
|
|
72
|
+
raise(ArgumentError, "Can't find '#{k}' attribute out of [#{@map.keys.join(', ')}]")
|
|
70
73
|
end
|
|
71
74
|
v[0]
|
|
72
75
|
end
|
data/lib/factbase/flatten.rb
CHANGED
|
@@ -21,8 +21,8 @@ class Factbase::Flatten
|
|
|
21
21
|
# @return [Array<HashMap>] The hashmaps, but improved
|
|
22
22
|
def it
|
|
23
23
|
@maps
|
|
24
|
-
.sort_by { |m| m[@sorter]
|
|
24
|
+
.sort_by { |m| Array(m[@sorter]).map(&:to_s) }
|
|
25
25
|
.map { |m| m.sort.to_h }
|
|
26
|
-
.map { |m| m.transform_values { |v| v.size == 1 ? v[0] : v } }
|
|
26
|
+
.map! { |m| m.transform_values { |v| v.size == 1 ? v[0] : v } }
|
|
27
27
|
end
|
|
28
28
|
end
|
data/lib/factbase/impatient.rb
CHANGED
|
@@ -18,9 +18,9 @@ class Factbase::Impatient
|
|
|
18
18
|
# @param [Factbase] fb The factbase to decorate
|
|
19
19
|
# @param [Integer] timeout Timeout in seconds
|
|
20
20
|
def initialize(fb, timeout: 15)
|
|
21
|
-
raise 'The "fb" is nil' if fb.nil?
|
|
21
|
+
raise(ArgumentError, 'The "fb" is nil') if fb.nil?
|
|
22
22
|
@origin = fb
|
|
23
|
-
@timeout = timeout
|
|
23
|
+
@timeout = Float(timeout)
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
decoor(:origin)
|
|
@@ -36,7 +36,7 @@ class Factbase::Impatient
|
|
|
36
36
|
|
|
37
37
|
def txn
|
|
38
38
|
@origin.txn do |fbt|
|
|
39
|
-
yield
|
|
39
|
+
yield(Factbase::Impatient.new(fbt, timeout: @timeout))
|
|
40
40
|
end
|
|
41
41
|
end
|
|
42
42
|
|
|
@@ -56,17 +56,15 @@ class Factbase::Impatient
|
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
def each(fb = @fb, params = {}, &)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
return to_enum(__method__, fb, params) unless block_given?
|
|
60
|
+
n = 0
|
|
61
|
+
impatient('each') do
|
|
62
|
+
@fb.query(@term, @maps).each(fb, params) do |f|
|
|
63
|
+
yield(f)
|
|
64
|
+
n += 1
|
|
62
65
|
end
|
|
63
|
-
return a unless block_given?
|
|
64
|
-
yielded = 0
|
|
65
|
-
a.each do |f|
|
|
66
|
-
yield f
|
|
67
|
-
yielded += 1
|
|
68
66
|
end
|
|
69
|
-
|
|
67
|
+
n
|
|
70
68
|
end
|
|
71
69
|
|
|
72
70
|
def one(fb = @fb, params = {})
|
|
@@ -86,7 +84,10 @@ class Factbase::Impatient
|
|
|
86
84
|
def impatient(name, &)
|
|
87
85
|
Timeout.timeout(@timeout, &)
|
|
88
86
|
rescue Timeout::Error => e
|
|
89
|
-
raise
|
|
87
|
+
raise(
|
|
88
|
+
StandardError,
|
|
89
|
+
"#{name}() timed out after #{@timeout.seconds} (#{e.message}), fb size is #{@fb.size}: #{@term}"
|
|
90
|
+
)
|
|
90
91
|
end
|
|
91
92
|
end
|
|
92
93
|
end
|
|
@@ -11,12 +11,12 @@ class Factbase::IndexedAnd
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def predict(maps, fb, params)
|
|
14
|
-
return
|
|
14
|
+
return if @idx.nil?
|
|
15
15
|
key = [maps.object_id, @term.operands.first, @term.op]
|
|
16
16
|
r = nil
|
|
17
17
|
if @term.operands.all? { |o| o.op == :eq } && @term.operands.size > 1 \
|
|
18
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
|
|
19
|
+
props = @term.operands.map { |o| o.operands.first }.sort!
|
|
20
20
|
key = [maps.object_id, props, :multi_and_eq]
|
|
21
21
|
entry = @idx[key]
|
|
22
22
|
maps_array = maps.to_a
|
|
@@ -45,19 +45,24 @@ class Factbase::IndexedAnd
|
|
|
45
45
|
j = tuples.flat_map { |t| entry[:index][t] || [] }.uniq(&:object_id)
|
|
46
46
|
r = maps.respond_to?(:repack) ? maps.repack(j) : j
|
|
47
47
|
else
|
|
48
|
+
fail = false
|
|
48
49
|
@term.operands.each do |o|
|
|
49
50
|
n = o.predict(maps, fb, params)
|
|
50
|
-
|
|
51
|
+
if n.nil?
|
|
52
|
+
fail = true
|
|
53
|
+
break
|
|
54
|
+
end
|
|
51
55
|
if r.nil?
|
|
52
56
|
r = n
|
|
53
|
-
elsif n.size < r.size * 8
|
|
57
|
+
elsif n.size < r.size * 8
|
|
54
58
|
small, large = n.size < r.size ? [n.to_a, r.to_a] : [r.to_a, n.to_a]
|
|
55
59
|
ids = Set.new(small.map(&:object_id))
|
|
56
60
|
r = large.select { |f| ids.include?(f.object_id) }
|
|
57
61
|
end
|
|
58
|
-
break if r.size < maps.size / 32
|
|
59
|
-
break if r.size < 128
|
|
62
|
+
break if r.size < maps.size / 32
|
|
63
|
+
break if r.size < 128
|
|
60
64
|
end
|
|
65
|
+
return if fail
|
|
61
66
|
end
|
|
62
67
|
r
|
|
63
68
|
end
|
|
@@ -69,19 +74,8 @@ class Factbase::IndexedAnd
|
|
|
69
74
|
end
|
|
70
75
|
|
|
71
76
|
def _all_tuples(fact, props)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if props.size > 1
|
|
76
|
-
tails = _all_tuples(fact, props[1..])
|
|
77
|
-
ext = []
|
|
78
|
-
tuples.each do |t|
|
|
79
|
-
tails.each do |tail|
|
|
80
|
-
ext << (t + tail)
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
tuples = ext
|
|
84
|
-
end
|
|
85
|
-
tuples
|
|
77
|
+
values = props.map { |p| fact[p.to_s] || [] }
|
|
78
|
+
return [] if values.any?(&:empty?)
|
|
79
|
+
values[0].product(*values[1..])
|
|
86
80
|
end
|
|
87
81
|
end
|
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
# SPDX-FileCopyrightText: Copyright (c) 2024-2026 Yegor Bugayenko
|
|
4
4
|
# SPDX-License-Identifier: MIT
|
|
5
5
|
|
|
6
|
-
# Indexed term 'eq'.
|
|
6
|
+
# Indexed term 'eq' that uses the hash-based inverted index for fast equality lookups.
|
|
7
|
+
#
|
|
8
|
+
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
|
9
|
+
# Copyright:: Copyright (c) 2024-2026 Yegor Bugayenko
|
|
10
|
+
# License:: MIT
|
|
7
11
|
class Factbase::IndexedEq
|
|
8
12
|
def initialize(term, idx)
|
|
9
13
|
@term = term
|
|
@@ -26,11 +26,8 @@ class Factbase::IndexedFact
|
|
|
26
26
|
@origin.to_s
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
# When a method is missing, this method is called.
|
|
30
29
|
others do |*args|
|
|
31
|
-
# Only clear index when modifying properties on existing (non-fresh) facts
|
|
32
|
-
# Fresh facts are not in the index yet, so modifications don't affect it
|
|
33
30
|
@idx.clear if args[0].to_s.end_with?('=') && !@fresh.include?(object_id)
|
|
34
|
-
@origin.
|
|
31
|
+
@origin.__send__(*args)
|
|
35
32
|
end
|
|
36
33
|
end
|
|
@@ -23,9 +23,9 @@ class Factbase::IndexedFactbase
|
|
|
23
23
|
# @param [Hash] idx Index to use
|
|
24
24
|
# @param [Set] fresh The set of IDs of newly inserted facts
|
|
25
25
|
def initialize(origin, idx = {}, fresh = Set.new)
|
|
26
|
-
raise 'Wrong type of original' unless origin.respond_to?(:query)
|
|
26
|
+
raise(ArgumentError, 'Wrong type of original') unless origin.respond_to?(:query)
|
|
27
27
|
@origin = origin
|
|
28
|
-
raise 'Wrong type of index' unless idx.is_a?(Hash)
|
|
28
|
+
raise(ArgumentError, 'Wrong type of index') unless idx.is_a?(Hash)
|
|
29
29
|
@idx = idx
|
|
30
30
|
@fresh = fresh
|
|
31
31
|
end
|
|
@@ -64,7 +64,7 @@ class Factbase::IndexedFactbase
|
|
|
64
64
|
inner_idx = {}
|
|
65
65
|
result =
|
|
66
66
|
@origin.txn do |fbt|
|
|
67
|
-
yield
|
|
67
|
+
yield(Factbase::IndexedFactbase.new(fbt, inner_idx, @fresh))
|
|
68
68
|
end
|
|
69
69
|
@idx.clear if result.deleted.positive? || result.added.positive?
|
|
70
70
|
@fresh.clear
|
|
@@ -100,7 +100,7 @@ class Factbase::IndexedFactbase
|
|
|
100
100
|
#
|
|
101
101
|
# @param [String] bytes Binary string to import
|
|
102
102
|
def import(bytes)
|
|
103
|
-
raise 'Empty input, cannot load a factbase' if bytes.empty?
|
|
103
|
+
raise(StandardError, 'Empty input, cannot load a factbase') if bytes.empty?
|
|
104
104
|
data = Marshal.load(bytes)
|
|
105
105
|
if data.is_a?(Hash) && data.key?(:maps)
|
|
106
106
|
@origin.import(data[:maps])
|
|
@@ -44,6 +44,8 @@ class Factbase::IndexedGt
|
|
|
44
44
|
def _search(entry, target)
|
|
45
45
|
idx = entry[:facts].bsearch_index { |v, _| v > target }
|
|
46
46
|
return [] if idx.nil?
|
|
47
|
-
entry[:facts][idx..].map { |_, f| f }
|
|
47
|
+
facts = entry[:facts][idx..].map { |_, f| f }
|
|
48
|
+
facts.uniq!(&:object_id)
|
|
49
|
+
facts
|
|
48
50
|
end
|
|
49
51
|
end
|