factbase 0.16.7 → 0.17.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.
Files changed (84) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +2 -1
  3. data/Gemfile.lock +25 -20
  4. data/README.md +28 -27
  5. data/Rakefile +14 -6
  6. data/lib/factbase/cached/cached_query.rb +2 -0
  7. data/lib/factbase/indexed/indexed_absent.rb +25 -0
  8. data/lib/factbase/indexed/indexed_and.rb +79 -0
  9. data/lib/factbase/indexed/indexed_eq.rb +46 -0
  10. data/lib/factbase/indexed/indexed_exists.rb +25 -0
  11. data/lib/factbase/indexed/indexed_factbase.rb +46 -0
  12. data/lib/factbase/indexed/indexed_gt.rb +41 -0
  13. data/lib/factbase/indexed/indexed_lt.rb +41 -0
  14. data/lib/factbase/indexed/indexed_not.rb +32 -0
  15. data/lib/factbase/indexed/indexed_one.rb +25 -0
  16. data/lib/factbase/indexed/indexed_or.rb +28 -0
  17. data/lib/factbase/indexed/indexed_query.rb +2 -0
  18. data/lib/factbase/indexed/indexed_term.rb +29 -185
  19. data/lib/factbase/indexed/indexed_unique.rb +25 -0
  20. data/lib/factbase/query.rb +22 -8
  21. data/lib/factbase/sync/sync_factbase.rb +11 -11
  22. data/lib/factbase/sync/sync_query.rb +7 -8
  23. data/lib/factbase/term.rb +110 -91
  24. data/lib/factbase/terms/absent.rb +26 -0
  25. data/lib/factbase/terms/aggregates.rb +0 -13
  26. data/lib/factbase/terms/always.rb +27 -0
  27. data/lib/factbase/terms/and.rb +28 -0
  28. data/lib/factbase/terms/arithmetic.rb +55 -0
  29. data/lib/factbase/terms/as.rb +31 -0
  30. data/lib/factbase/terms/{debug.rb → assert.rb} +17 -15
  31. data/lib/factbase/terms/base.rb +17 -0
  32. data/lib/factbase/terms/boolean.rb +28 -0
  33. data/lib/factbase/terms/compare.rb +38 -0
  34. data/lib/factbase/terms/concat.rb +26 -0
  35. data/lib/factbase/terms/count.rb +25 -0
  36. data/lib/factbase/terms/defn.rb +16 -15
  37. data/lib/factbase/terms/div.rb +25 -0
  38. data/lib/factbase/terms/either.rb +31 -0
  39. data/lib/factbase/terms/env.rb +28 -0
  40. data/lib/factbase/terms/eq.rb +28 -0
  41. data/lib/factbase/terms/exists.rb +27 -0
  42. data/lib/factbase/terms/first.rb +30 -0
  43. data/lib/factbase/terms/gt.rb +28 -0
  44. data/lib/factbase/terms/gte.rb +27 -0
  45. data/lib/factbase/terms/head.rb +37 -0
  46. data/lib/factbase/terms/inverted.rb +34 -0
  47. data/lib/factbase/terms/{aliases.rb → join.rb} +15 -15
  48. data/lib/factbase/terms/logical.rb +0 -83
  49. data/lib/factbase/terms/lt.rb +28 -0
  50. data/lib/factbase/terms/lte.rb +28 -0
  51. data/lib/factbase/terms/many.rb +29 -0
  52. data/lib/factbase/terms/{strings.rb → matches.rb} +12 -12
  53. data/lib/factbase/terms/minus.rb +25 -0
  54. data/lib/factbase/terms/never.rb +26 -0
  55. data/lib/factbase/terms/nil.rb +26 -0
  56. data/lib/factbase/terms/not.rb +27 -0
  57. data/lib/factbase/terms/one.rb +30 -0
  58. data/lib/factbase/terms/or.rb +28 -0
  59. data/lib/factbase/terms/plus.rb +27 -0
  60. data/lib/factbase/terms/prev.rb +29 -0
  61. data/lib/factbase/terms/shared.rb +69 -0
  62. data/lib/factbase/terms/size.rb +30 -0
  63. data/lib/factbase/terms/sorted.rb +38 -0
  64. data/lib/factbase/terms/sprintf.rb +29 -0
  65. data/lib/factbase/terms/times.rb +25 -0
  66. data/lib/factbase/terms/to_float.rb +28 -0
  67. data/lib/factbase/terms/to_integer.rb +28 -0
  68. data/lib/factbase/terms/to_string.rb +28 -0
  69. data/lib/factbase/terms/to_time.rb +28 -0
  70. data/lib/factbase/terms/traced.rb +33 -0
  71. data/lib/factbase/terms/type.rb +31 -0
  72. data/lib/factbase/terms/undef.rb +33 -0
  73. data/lib/factbase/terms/unique.rb +34 -0
  74. data/lib/factbase/terms/when.rb +29 -0
  75. data/lib/factbase/terms/zero.rb +28 -0
  76. data/lib/factbase/version.rb +1 -1
  77. data/lib/factbase.rb +10 -1
  78. metadata +60 -10
  79. data/lib/factbase/terms/casting.rb +0 -41
  80. data/lib/factbase/terms/lists.rb +0 -57
  81. data/lib/factbase/terms/math.rb +0 -103
  82. data/lib/factbase/terms/meta.rb +0 -58
  83. data/lib/factbase/terms/ordering.rb +0 -34
  84. data/lib/factbase/terms/system.rb +0 -19
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9ffa551b6a4ccacd507da103b9bd6f5036817013d2b50ab9ccfd78293af57325
4
- data.tar.gz: 628177e1d139d8990b2517573e9ffb4ee19866ec59298831699d10e68285e365
3
+ metadata.gz: 8e2593945542e79d93c93e662467bb704ad98d9a481887b5447341c700e4abf8
4
+ data.tar.gz: 5e13613617c829b0669235a275a1796c6556ca8cdced3ef9ee3502cbb2c300db
5
5
  SHA512:
6
- metadata.gz: 46044287ec053180ee51d8d646a719c7824a22134a199dbf3379dfe67d332a048cb1dfaabcb3efc8a8c37e238df664a64b73b0425bf9950aebe22d42c48193c8
7
- data.tar.gz: 82eaa84ac9fe00c39dbe1385dd1c39fd55f08b96dfb13db72f56eb952bc29de49ca3fb30bb2c8b305c7a2b1c2f63f169b70f5d79ca8f200697ca6051d7b46e0a
6
+ metadata.gz: 22d7965bd3b3f60070ca8293e3c054ea5b2553913ccb091328f3d51f2aab3a42c3be56b5197536014c550eecceb708186a826cb1fe67b6e31deb636819ceb866
7
+ data.tar.gz: 5e5fef55d3e316d19d5d96c2de10b23d1788541c09a2331c8762cd73c1c4b15b468c7cb4c0ff4b4f33f3b296f0a875673568f7a1eca92744fbf54c2e0c042233
data/Gemfile CHANGED
@@ -6,12 +6,13 @@
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.14.2', require: false # GPL
15
+ gem 'rdoc', '6.15.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
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.4.1)
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 (5.0.2)
32
- json (2.13.2)
32
+ erb (6.0.0)
33
+ json (2.16.0)
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.25.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.9.0)
56
+ parser (3.3.10.0)
56
57
  ast (~> 2.4.1)
57
58
  racc
58
- prism (1.5.1)
59
+ prism (1.6.0)
59
60
  psych (5.2.6)
60
61
  date
61
62
  stringio
62
- qbash (0.4.5)
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.0)
70
- rdoc (6.14.2)
70
+ rake (13.3.1)
71
+ rdoc (6.15.1)
71
72
  erb
72
73
  psych (>= 4.0.0)
73
- regexp_parser (2.11.2)
74
+ tsort
75
+ regexp_parser (2.11.3)
74
76
  rexml (3.4.4)
75
- rubocop (1.80.2)
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)
@@ -80,20 +82,20 @@ GEM
80
82
  parser (>= 3.3.0.2)
81
83
  rainbow (>= 2.2.2, < 4.0)
82
84
  regexp_parser (>= 2.9.3, < 3.0)
83
- rubocop-ast (>= 1.46.0, < 2.0)
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.46.0)
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.0)
95
+ rubocop-performance (1.26.1)
94
96
  lint_roller (~> 1.1)
95
97
  rubocop (>= 1.75.0, < 2.0)
96
- rubocop-ast (>= 1.44.0, < 2.0)
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,11 +109,12 @@ GEM
107
109
  simplecov (~> 0.19)
108
110
  simplecov-html (0.13.2)
109
111
  simplecov_json_formatter (0.1.4)
110
- stringio (3.1.7)
111
- tago (0.2.0)
112
- threads (0.4.1)
112
+ stringio (3.1.8)
113
+ tago (0.4.0)
114
+ threads (0.5.0)
113
115
  backtrace (~> 0)
114
116
  concurrent-ruby (~> 1.0)
117
+ tsort (0.2.0)
115
118
  unicode-display_width (3.2.0)
116
119
  unicode-emoji (~> 4.1)
117
120
  unicode-emoji (4.1.0)
@@ -125,16 +128,18 @@ PLATFORMS
125
128
  x64-mingw-ucrt
126
129
  x86_64-darwin-20
127
130
  x86_64-darwin-21
131
+ x86_64-darwin-24
128
132
  x86_64-linux
129
133
 
130
134
  DEPENDENCIES
135
+ benchmark (~> 0.5)
131
136
  factbase!
132
137
  minitest (~> 5.25)
133
138
  minitest-reporters (~> 1.7)
134
139
  os (~> 1.1)
135
140
  qbash (~> 0.4)
136
141
  rake (~> 13.2)
137
- rdoc (= 6.14.2)
142
+ rdoc (= 6.15.1)
138
143
  rubocop (~> 1.74)
139
144
  rubocop-minitest (~> 0.38)
140
145
  rubocop-performance (~> 1.25)
@@ -145,4 +150,4 @@ DEPENDENCIES
145
150
  yard (~> 0.9)
146
151
 
147
152
  BUNDLED WITH
148
- 2.5.16
153
+ 2.6.8
data/README.md CHANGED
@@ -4,6 +4,7 @@
4
4
  [![We recommend RubyMine](https://www.elegantobjects.org/rubymine.svg)](https://www.jetbrains.com/ruby/)
5
5
 
6
6
  [![rake](https://github.com/yegor256/factbase/actions/workflows/rake.yml/badge.svg)](https://github.com/yegor256/factbase/actions/workflows/rake.yml)
7
+ [![discipline](https://zerocracy.github.io/judges-action/zerocracy-badge.svg)](https://zerocracy.github.io/judges-action/zerocracy-vitals.html)
7
8
  [![PDD status](https://www.0pdd.com/svg?name=yegor256/factbase)](https://www.0pdd.com/p?name=yegor256/factbase)
8
9
  [![Gem Version](https://badge.fury.io/rb/factbase.svg)](https://badge.fury.io/rb/factbase)
9
10
  [![Test Coverage](https://img.shields.io/codecov/c/github/yegor256/factbase.svg)](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.2+ and
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.595620
214
- export 20000 facts 0.019511
215
- import 410903 bytes (20000 facts) 0.021517
216
- insert 10 facts 0.039990
217
- query 10 times w/txn 2.051662
218
- query 10 times w/o txn 0.043900
219
- modify 10 attrs w/txn 1.928921
220
- delete 10 facts w/txn 1.075461
221
- (and (eq what 'issue-was-closed') (exists... -> 200 1.120060
222
- (and (eq what 'issue-was-closed') (exists... -> 200/txn 1.114975
223
- (and (eq what 'issue-was-closed') (exists... -> zero 1.080422
224
- (and (eq what 'issue-was-closed') (exists... -> zero/txn 1.131242
225
- (gt time '2024-03-23T03:21:43Z') 0.342271
226
- (gt cost 50) 0.188269
227
- (eq title 'Object Thinking 5000') 0.089654
228
- (and (eq foo 42.998) (or (gt bar 200) (absent z... 0.050858
229
- (and (exists foo) (not (exists blue))) 0.920006
230
- (eq id (agg (always) (max id))) 0.597035
231
- (join "c<=cost,b<=bar" (eq id (agg (always) (ma... 1.319789
232
- (and (eq what "foo") (join "w<=what" (and (eq i... 7.039262
233
- delete! 0.219260
234
- Taped.append() x50000 0.025503
235
- Taped.each() x125 1.345823
236
- Taped.delete_if() x375 0.818237
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-08-21 at 14:27,
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/17129904396
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,18 +54,25 @@ 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
- Dir['benchmark/bench_*.rb'].each do |f|
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
@@ -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
@@ -61,4 +61,50 @@ class Factbase::IndexedFactbase
61
61
  yield Factbase::IndexedFactbase.new(fbt, @idx)
62
62
  end
63
63
  end
64
+
65
+ # Export it into a chain of bytes, including both data and index.
66
+ #
67
+ # Here is how you can export it to a file, for example:
68
+ #
69
+ # fb = Factbase::IndexedFactbase.new(Factbase.new)
70
+ # fb.insert.foo = 42
71
+ # File.binwrite("foo.fb", fb.export)
72
+ #
73
+ # The data is binary, it's not a text!
74
+ #
75
+ # @return [String] Binary string containing serialized data and index
76
+ def export
77
+ Marshal.dump({ maps: @origin.export, idx: @idx })
78
+ end
79
+
80
+ # Import from a chain of bytes, including both data and index.
81
+ #
82
+ # Here is how you can read it from a file, for example:
83
+ #
84
+ # fb = Factbase::IndexedFactbase.new(Factbase.new)
85
+ # fb.import(File.binread("foo.fb"))
86
+ #
87
+ # The facts that existed in the factbase before importing will remain there.
88
+ # The facts from the incoming byte stream will be added to them.
89
+ # If the byte stream doesn't contain an index (for backward compatibility),
90
+ # the index will be empty and will be built on first use.
91
+ #
92
+ # @param [String] bytes Binary string to import
93
+ def import(bytes)
94
+ raise 'Empty input, cannot load a factbase' if bytes.empty?
95
+ data = Marshal.load(bytes)
96
+ if data.is_a?(Hash) && data.key?(:maps)
97
+ @origin.import(data[:maps])
98
+ @idx.merge!(data[:idx]) if data[:idx].is_a?(Hash)
99
+ else
100
+ @origin.import(bytes)
101
+ @idx.clear
102
+ end
103
+ end
104
+
105
+ # Size, the total number of facts in the factbase.
106
+ # @return [Integer] How many facts are in there
107
+ def size
108
+ @origin.size
109
+ end
64
110
  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
@@ -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 'one'.
7
+ class Factbase::IndexedOne
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? && m[prop].size == 1
21
+ end
22
+ end
23
+ (maps & []) | @idx[key]
24
+ end
25
+ end