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.
Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +3 -1
  3. data/Gemfile.lock +26 -19
  4. data/README.md +28 -27
  5. data/Rakefile +30 -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 +51 -2
  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 +3 -1
  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 +130 -101
  24. data/lib/factbase/terms/absent.rb +26 -0
  25. data/lib/factbase/terms/agg.rb +35 -0
  26. data/lib/factbase/terms/always.rb +27 -0
  27. data/lib/factbase/terms/and.rb +35 -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/best.rb +25 -0
  33. data/lib/factbase/terms/boolean.rb +28 -0
  34. data/lib/factbase/terms/compare.rb +38 -0
  35. data/lib/factbase/terms/concat.rb +26 -0
  36. data/lib/factbase/terms/count.rb +25 -0
  37. data/lib/factbase/terms/defn.rb +16 -15
  38. data/lib/factbase/terms/div.rb +25 -0
  39. data/lib/factbase/terms/either.rb +31 -0
  40. data/lib/factbase/terms/empty.rb +36 -0
  41. data/lib/factbase/terms/env.rb +28 -0
  42. data/lib/factbase/terms/eq.rb +28 -0
  43. data/lib/factbase/terms/exists.rb +27 -0
  44. data/lib/factbase/terms/first.rb +30 -0
  45. data/lib/factbase/terms/gt.rb +28 -0
  46. data/lib/factbase/terms/gte.rb +27 -0
  47. data/lib/factbase/terms/head.rb +37 -0
  48. data/lib/factbase/terms/inverted.rb +34 -0
  49. data/lib/factbase/terms/{aliases.rb → join.rb} +15 -15
  50. data/lib/factbase/terms/lt.rb +28 -0
  51. data/lib/factbase/terms/lte.rb +28 -0
  52. data/lib/factbase/terms/many.rb +29 -0
  53. data/lib/factbase/terms/{strings.rb → matches.rb} +12 -12
  54. data/lib/factbase/terms/max.rb +32 -0
  55. data/lib/factbase/terms/min.rb +32 -0
  56. data/lib/factbase/terms/minus.rb +25 -0
  57. data/lib/factbase/terms/never.rb +26 -0
  58. data/lib/factbase/terms/nil.rb +26 -0
  59. data/lib/factbase/terms/not.rb +27 -0
  60. data/lib/factbase/terms/nth.rb +33 -0
  61. data/lib/factbase/terms/one.rb +30 -0
  62. data/lib/factbase/terms/or.rb +35 -0
  63. data/lib/factbase/terms/plus.rb +27 -0
  64. data/lib/factbase/terms/prev.rb +29 -0
  65. data/lib/factbase/terms/shared.rb +69 -0
  66. data/lib/factbase/terms/simplified.rb +31 -0
  67. data/lib/factbase/terms/size.rb +30 -0
  68. data/lib/factbase/terms/sorted.rb +38 -0
  69. data/lib/factbase/terms/sprintf.rb +29 -0
  70. data/lib/factbase/terms/sum.rb +38 -0
  71. data/lib/factbase/terms/times.rb +25 -0
  72. data/lib/factbase/terms/to_float.rb +28 -0
  73. data/lib/factbase/terms/to_integer.rb +28 -0
  74. data/lib/factbase/terms/to_string.rb +28 -0
  75. data/lib/factbase/terms/to_time.rb +28 -0
  76. data/lib/factbase/terms/traced.rb +33 -0
  77. data/lib/factbase/terms/type.rb +31 -0
  78. data/lib/factbase/terms/undef.rb +33 -0
  79. data/lib/factbase/terms/unique.rb +34 -0
  80. data/lib/factbase/terms/when.rb +29 -0
  81. data/lib/factbase/terms/zero.rb +28 -0
  82. data/lib/factbase/version.rb +1 -1
  83. data/lib/factbase.rb +10 -1
  84. metadata +68 -12
  85. data/lib/factbase/terms/aggregates.rb +0 -99
  86. data/lib/factbase/terms/casting.rb +0 -41
  87. data/lib/factbase/terms/lists.rb +0 -57
  88. data/lib/factbase/terms/logical.rb +0 -124
  89. data/lib/factbase/terms/math.rb +0 -103
  90. data/lib/factbase/terms/meta.rb +0 -58
  91. data/lib/factbase/terms/ordering.rb +0 -34
  92. data/lib/factbase/terms/system.rb +0 -19
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '0156369b495c6ca1f2ef02f8154f9f639c710e01ac481d230781dc8eecd96e63'
4
- data.tar.gz: 7b90b7b1efdc0128a73287becca34609b57e1779089312fe8367064a8bfff10b
3
+ metadata.gz: 91ef3ed499ea15abb77bc39cfed30eb9af704921e86c3c03c7bdd9b13cb44270
4
+ data.tar.gz: 3c2e80d00620e0de14ec4be35ff08b28393793c69742dda9bda55c4a5ae01c8f
5
5
  SHA512:
6
- metadata.gz: bab2eb3a28c5d26d5815c89f75331205dbcb164cd94c833cbb1c1e5a7eed7903f7d51953be249b49224970221c95d24ecf465592b01a8ebae12d871518dcb759
7
- data.tar.gz: 5a20d5698d0757c8a33e1de0a044a4567c19e44e5f52a4be21982d52389cfb058933fa14b7ef81da03ec7c671241e629f057caf9c6f3e118d379a04d5741b887
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.14.2', require: false # GPL
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.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.15.0)
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.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.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.1)
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.47.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.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,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
- stringio (3.1.7)
111
- tago (0.2.0)
112
- threads (0.4.1)
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.37)
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.14.2)
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.5.16
155
+ 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,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
- 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
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
- @origin.txn do |fbt|
61
- yield Factbase::IndexedFactbase.new(fbt, @idx)
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