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.
Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +5 -4
  3. data/Gemfile.lock +16 -12
  4. data/README.md +49 -18
  5. data/Rakefile +2 -7
  6. data/factbase.gemspec +11 -11
  7. data/lib/factbase/accum.rb +1 -1
  8. data/lib/factbase/cached/cached_fact.rb +1 -2
  9. data/lib/factbase/cached/cached_factbase.rb +3 -3
  10. data/lib/factbase/cached/cached_query.rb +4 -6
  11. data/lib/factbase/cached/cached_term.rb +1 -2
  12. data/lib/factbase/churn.rb +4 -8
  13. data/lib/factbase/fact.rb +12 -9
  14. data/lib/factbase/flatten.rb +2 -2
  15. data/lib/factbase/impatient.rb +14 -13
  16. data/lib/factbase/indexed/indexed_and.rb +14 -20
  17. data/lib/factbase/indexed/indexed_eq.rb +5 -1
  18. data/lib/factbase/indexed/indexed_fact.rb +1 -4
  19. data/lib/factbase/indexed/indexed_factbase.rb +4 -4
  20. data/lib/factbase/indexed/indexed_gt.rb +3 -1
  21. data/lib/factbase/indexed/indexed_gte.rb +51 -0
  22. data/lib/factbase/indexed/indexed_lt.rb +3 -1
  23. data/lib/factbase/indexed/indexed_lte.rb +51 -0
  24. data/lib/factbase/indexed/indexed_not.rb +1 -1
  25. data/lib/factbase/indexed/indexed_or.rb +2 -2
  26. data/lib/factbase/indexed/indexed_query.rb +6 -7
  27. data/lib/factbase/indexed/indexed_term.rb +10 -6
  28. data/lib/factbase/indexed/indexed_unique.rb +4 -2
  29. data/lib/factbase/inv.rb +3 -3
  30. data/lib/factbase/lazy_taped.rb +10 -13
  31. data/lib/factbase/lazy_taped_hash.rb +2 -1
  32. data/lib/factbase/light.rb +1 -1
  33. data/lib/factbase/logged.rb +37 -34
  34. data/lib/factbase/pre.rb +3 -3
  35. data/lib/factbase/query.rb +4 -5
  36. data/lib/factbase/rules.rb +8 -8
  37. data/lib/factbase/sync/sync_factbase.rb +2 -2
  38. data/lib/factbase/syntax.rb +18 -19
  39. data/lib/factbase/tallied.rb +6 -7
  40. data/lib/factbase/taped.rb +5 -11
  41. data/lib/factbase/tee.rb +2 -2
  42. data/lib/factbase/term.rb +53 -60
  43. data/lib/factbase/terms/agg.rb +3 -4
  44. data/lib/factbase/terms/arithmetic.rb +7 -7
  45. data/lib/factbase/terms/as.rb +2 -2
  46. data/lib/factbase/terms/assert.rb +5 -13
  47. data/lib/factbase/terms/base.rb +6 -7
  48. data/lib/factbase/terms/best.rb +1 -1
  49. data/lib/factbase/terms/boolean.rb +1 -1
  50. data/lib/factbase/terms/compare.rb +2 -1
  51. data/lib/factbase/terms/defn.rb +8 -6
  52. data/lib/factbase/terms/empty.rb +1 -1
  53. data/lib/factbase/terms/first.rb +2 -2
  54. data/lib/factbase/terms/head.rb +3 -3
  55. data/lib/factbase/terms/inverted.rb +2 -2
  56. data/lib/factbase/terms/join.rb +8 -7
  57. data/lib/factbase/terms/matches.rb +4 -4
  58. data/lib/factbase/terms/max.rb +1 -1
  59. data/lib/factbase/terms/min.rb +1 -1
  60. data/lib/factbase/terms/nth.rb +3 -3
  61. data/lib/factbase/terms/plus.rb +1 -1
  62. data/lib/factbase/terms/prev.rb +3 -6
  63. data/lib/factbase/terms/sorted.rb +2 -2
  64. data/lib/factbase/terms/sprintf.rb +5 -4
  65. data/lib/factbase/terms/sum.rb +1 -1
  66. data/lib/factbase/terms/to_float.rb +2 -2
  67. data/lib/factbase/terms/to_integer.rb +2 -2
  68. data/lib/factbase/terms/to_string.rb +1 -1
  69. data/lib/factbase/terms/to_time.rb +2 -2
  70. data/lib/factbase/terms/traced.rb +2 -2
  71. data/lib/factbase/terms/undef.rb +2 -2
  72. data/lib/factbase/terms/unique.rb +3 -7
  73. data/lib/factbase/to_json.rb +1 -1
  74. data/lib/factbase/to_xml.rb +5 -9
  75. data/lib/factbase/to_yaml.rb +1 -1
  76. data/lib/factbase/version.rb +1 -2
  77. data/lib/factbase.rb +27 -10
  78. data/lib/fuzz.rb +3 -3
  79. metadata +3 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 712cd682bc9457b563afc7a855e56cad191d2a302406e0e4e9f886ce7fa83af3
4
- data.tar.gz: cd3a312b78a152b7c9631777473e7465648e6b57d31d713d37d93c63834048ed
3
+ metadata.gz: f97152f34dd9eb3ba72cc5380a87d321bbec59c316078108b38c2c078c23a0fd
4
+ data.tar.gz: 0f678a254dc256d1983965742771c37d0956a89f93ec91e098f63b4d2957117a
5
5
  SHA512:
6
- metadata.gz: 013ff487bbd8d2379224361b7794d5219d3235c9c66f645ebe1d6dd928d2298f1a411240328839ea48e08f7dd4c1161cde173530c52f5f8a5afdab52c66cce98
7
- data.tar.gz: cf57ff6a8f881224a2124306871d245cab380331905fd0ec1acecd381a46d636a7877867a8f1ef27611f1dafe4f1e777c1a01aa57f08b35eb3486682aef22ad3
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 # GPL
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 # GPL
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] # GPL
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 # GPL
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.1)
34
- json (2.18.1)
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.0-arm64-darwin)
49
+ nokogiri (1.19.3-arm64-darwin)
50
50
  racc (~> 1.4)
51
- nokogiri (1.19.0-x64-mingw-ucrt)
51
+ nokogiri (1.19.3-x64-mingw-ucrt)
52
52
  racc (~> 1.4)
53
- nokogiri (1.19.0-x86_64-darwin)
53
+ nokogiri (1.19.3-x86_64-darwin)
54
54
  racc (~> 1.4)
55
- nokogiri (1.19.0-x86_64-linux-gnu)
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.27.0)
60
- parser (3.3.10.2)
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.11.3)
79
+ regexp_parser (2.12.0)
80
80
  rexml (3.4.4)
81
- rubocop (1.84.2)
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 (~> 1.10)
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.0)
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
  [![Yard Docs](https://img.shields.io/badge/yard-docs-blue.svg)](https://rubydoc.info/github/yegor256/factbase/master/frames)
12
12
  [![Hits-of-Code](https://hitsofcode.com/github/yegor256/factbase)](https://hitsofcode.com/view/github/yegor256/factbase)
13
13
  [![License](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/yegor256/factbase/blob/master/LICENSE.txt)
14
- [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fyegor256%2Ffactbase.svg?type=shield&issueType=license)](https://app.fossa.com/projects/git%2Bgithub.com%2Fyegor256%2Ffactbase?ref=badge_shield&issueType=license)
14
+ [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fyegor256%2Ffactbase.svg?type=shield&issueType=license)](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's thread-safe, by the way):
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.001049
330
- 20k facts: export: 2980KB 0.840193
331
- 20k facts: import: 2980KB 1.033492
332
- 50k facts: read 0.000179
333
- 50k facts: read in txn 0.002282
334
- 50k facts: insert 0.000082
335
- 50k facts: insert in txn 0.000613
336
- 50k facts: modify 1.427349
337
- 50k facts: modify in txn 2.773065
338
- 12k facts: large query: match 3k 13.166170
339
- 12k facts: large query: match 3k in txn 18.502015
340
- 12k facts: large query: match zero 14.234812
341
- 12k facts: large query: match zero in txn 19.847547
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-05-23 at 03:40,
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/26322478578
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).to_i
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.send(File.basename(f).gsub(/\.rb$/, '').to_sym, b, fb, cycles)
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? :required_rubygems_version=
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 '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'
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
@@ -27,7 +27,7 @@ class Factbase::Accum
27
27
  end
28
28
 
29
29
  def all_properties
30
- @fact.all_properties
30
+ @fact.all_properties | @props.keys
31
31
  end
32
32
 
33
33
  others do |*args|
@@ -24,9 +24,8 @@ class Factbase::CachedFact
24
24
  @origin.to_s
25
25
  end
26
26
 
27
- # When a method is missing, this method is called.
28
27
  others do |*args|
29
28
  @cache.clear if args[0].to_s.end_with?('=')
30
- @origin.send(*args)
29
+ @origin.__send__(*args)
31
30
  end
32
31
  end
@@ -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 Factbase::CachedFactbase.new(fbt, @cache)
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}" # params are ignored!
40
- before = @cache[key]
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 Factbase::CachedFact.new(f, @cache)
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
- before = @cache[key]
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
- before = @cache[key]
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
@@ -22,16 +22,16 @@ class Factbase::Churn
22
22
  if zero?
23
23
  'nothing'
24
24
  else
25
- "#{@inserted}i/#{@deleted}d/#{@added}a"
25
+ "#{inserted}i/#{deleted}d/#{added}a"
26
26
  end
27
27
  end
28
28
 
29
29
  def zero?
30
- @inserted.zero? && @deleted.zero? && @added.zero?
30
+ inserted.zero? && deleted.zero? && added.zero?
31
31
  end
32
32
 
33
33
  def to_i
34
- @inserted + @deleted + @added
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.join(', ')} ]"
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 [String, Integer, Float, Time].include?(v.class)
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
@@ -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
@@ -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.to_f
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 Factbase::Impatient.new(fbt, timeout: @timeout)
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
- a =
60
- impatient('each') do
61
- @fb.query(@term, @maps).each(fb, params).to_a
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
- yielded
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 "#{name}() timed out after #{@timeout.seconds} (#{e.message}), fb size is #{@fb.size}: #{@term}"
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 nil if @idx.nil?
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
- break if n.nil?
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 # to skip some obvious matchings
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 # it's already small enough
59
- break if r.size < 128 # it's obviously already small enough
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
- prop = props.first.to_s
73
- tuples = []
74
- tuples += (fact[prop] || []).zip
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.send(*args)
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 Factbase::IndexedFactbase.new(fbt, inner_idx, @fresh)
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 }.uniq(&:object_id)
47
+ facts = entry[:facts][idx..].map { |_, f| f }
48
+ facts.uniq!(&:object_id)
49
+ facts
48
50
  end
49
51
  end