factbase 0.14.0 → 0.14.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e275580781c232bdbea9b569d456ff3955c4a1fe1add33101398d00acc9605e5
4
- data.tar.gz: 5c3a6d0e61c81a8bc7a2cc0a3ca6f003a77ad0cd9fdae1d68e2ecb55d386b169
3
+ metadata.gz: 1a637de7c717d650eb0c6721794afd2c4a3bbb31a3eda1b1150f8a2f3e1c5322
4
+ data.tar.gz: a0a7698bba101d67c20fd390f1e745679110687643ec6849c445898b26a0d04e
5
5
  SHA512:
6
- metadata.gz: 0d764be837fc143d2a39b7f58d40cc33f93a3fb37a8608cca74b514b41a1ecbfac64c5e4b5bde836fa414fb33ae6cd0c7b7668802a8431f6a6f3a428a3bd6f90
7
- data.tar.gz: 4aa141144fb2b41a4b6a47b0ca2acde9b3c35d1c8bd487700e9fe0d88a19ea6eac0224dd0390969a3377856b0f84d1af3d54f6a870ce63300065f151e2e828f9
6
+ metadata.gz: 99cbde1626587ef0d3552258089dcf9a25f956ee83766f4273d8928faa769995e7150925d0ae2d9506366eed80a4347d3302c7e0f00e854ff1671e6294e78452
7
+ data.tar.gz: 7aa47a8c5fd5dd2ff8a7e63fe7f203f9889a26bd7f20c94b3b126fe9cf8d52c78a5f5131d684e232b3ac37961c7c074f02531afe53c41e2904509b211441cf15
data/Gemfile.lock CHANGED
@@ -24,12 +24,12 @@ GEM
24
24
  date (3.4.1)
25
25
  decoor (0.1.0)
26
26
  docile (1.4.1)
27
- elapsed (0.1.0)
28
- loog (> 0)
29
- tago (> 0)
27
+ elapsed (0.2.0)
28
+ loog (~> 0.6)
29
+ tago (~> 0.1)
30
30
  ellipsized (0.3.0)
31
31
  erb (5.0.2)
32
- json (2.13.0)
32
+ json (2.13.2)
33
33
  language_server-protocol (3.17.0.5)
34
34
  lint_roller (1.1.0)
35
35
  logger (1.7.0)
@@ -52,7 +52,7 @@ GEM
52
52
  os (1.1.4)
53
53
  others (0.1.1)
54
54
  parallel (1.27.0)
55
- parser (3.3.8.0)
55
+ parser (3.3.9.0)
56
56
  ast (~> 2.4.1)
57
57
  racc
58
58
  prism (1.4.0)
@@ -72,7 +72,7 @@ GEM
72
72
  psych (>= 4.0.0)
73
73
  regexp_parser (2.10.0)
74
74
  rexml (3.4.1)
75
- rubocop (1.78.0)
75
+ rubocop (1.79.0)
76
76
  json (~> 2.3)
77
77
  language_server-protocol (~> 3.17.0.2)
78
78
  lint_roller (~> 1.1.0)
@@ -80,8 +80,9 @@ GEM
80
80
  parser (>= 3.3.0.2)
81
81
  rainbow (>= 2.2.2, < 4.0)
82
82
  regexp_parser (>= 2.9.3, < 3.0)
83
- rubocop-ast (>= 1.45.1, < 2.0)
83
+ rubocop-ast (>= 1.46.0, < 2.0)
84
84
  ruby-progressbar (~> 1.7)
85
+ tsort (>= 0.2.0)
85
86
  unicode-display_width (>= 2.4.0, < 4.0)
86
87
  rubocop-ast (1.46.0)
87
88
  parser (>= 3.3.7.2)
@@ -112,6 +113,7 @@ GEM
112
113
  threads (0.4.1)
113
114
  backtrace (~> 0)
114
115
  concurrent-ruby (~> 1.0)
116
+ tsort (0.2.0)
115
117
  unicode-display_width (3.1.4)
116
118
  unicode-emoji (~> 4.0, >= 4.0.4)
117
119
  unicode-emoji (4.0.4)
data/README.md CHANGED
@@ -210,33 +210,35 @@ This is the result of the benchmark:
210
210
  <!-- benchmark_begin -->
211
211
  ```text
212
212
  user
213
- insert 20000 facts 0.618307
214
- export 20000 facts 0.021391
215
- import 410726 bytes (20000 facts) 0.030150
216
- insert 10 facts 0.045651
217
- query 10 times w/txn 1.970434
218
- query 10 times w/o txn 0.039459
219
- modify 10 attrs w/txn 1.819769
220
- delete 10 facts w/txn 1.056563
221
- (and (eq what 'issue-was-closed') (exists... -> 200 2.125753
222
- (and (eq what 'issue-was-closed') (exists... -> 200/txn 1.135618
223
- (and (eq what 'issue-was-closed') (exists... -> zero 2.422668
224
- (and (eq what 'issue-was-closed') (exists... -> zero/txn 1.288236
225
- (gt time '2024-03-23T03:21:43Z') 0.108849
226
- (gt cost 50) 0.090092
227
- (eq title 'Object Thinking 5000') 0.002640
228
- (and (eq foo 42.998) (or (gt bar 200) (absent zzz))) 0.023785
229
- (eq id (agg (always) (max id))) 0.252980
230
- (join "c<=cost,b<=bar" (eq id (agg (always) (max id)))) 0.655997
231
- delete! 0.075510
232
- Taped.append() x50000 0.042923
233
- Taped.each() x125 1.343819
234
- Taped.delete_if() x375 0.830124
213
+ insert 20000 facts 0.658630
214
+ export 20000 facts 0.025967
215
+ import 411020 bytes (20000 facts) 0.024282
216
+ insert 10 facts 0.050191
217
+ query 10 times w/txn 2.063964
218
+ query 10 times w/o txn 0.044326
219
+ modify 10 attrs w/txn 1.745367
220
+ delete 10 facts w/txn 1.023205
221
+ (and (eq what 'issue-was-closed') (exists... -> 200 2.043080
222
+ (and (eq what 'issue-was-closed') (exists... -> 200/txn 1.019861
223
+ (and (eq what 'issue-was-closed') (exists... -> zero 2.350912
224
+ (and (eq what 'issue-was-closed') (exists... -> zero/txn 1.187649
225
+ (gt time '2024-03-23T03:21:43Z') 0.304861
226
+ (gt cost 50) 0.234136
227
+ (eq title 'Object Thinking 5000') 0.091766
228
+ (and (eq foo 42.998) (or (gt bar 200) (absent z... 0.049974
229
+ (and (exists foo) (not (exists blue))) 0.926298
230
+ (eq id (agg (always) (max id))) 0.606111
231
+ (join "c<=cost,b<=bar" (eq id (agg (always) (ma... 1.276007
232
+ (and (eq what "foo") (join "w<=what" (and (eq i... 6.827440
233
+ delete! 0.218373
234
+ Taped.append() x50000 0.023564
235
+ Taped.each() x125 1.348705
236
+ Taped.delete_if() x375 0.812740
235
237
  ```
236
238
 
237
239
  The results were calculated in [this GHA job][benchmark-gha]
238
- on 2025-06-21 at 08:24,
240
+ on 2025-07-28 at 17:02,
239
241
  on Linux with 4 CPUs.
240
242
  <!-- benchmark_end -->
241
243
 
242
- [benchmark-gha]: https://github.com/yegor256/factbase/actions/runs/15793882855
244
+ [benchmark-gha]: https://github.com/yegor256/factbase/actions/runs/16575338618
@@ -3,12 +3,16 @@
3
3
  # SPDX-FileCopyrightText: Copyright (c) 2024-2025 Yegor Bugayenko
4
4
  # SPDX-License-Identifier: MIT
5
5
 
6
+ require 'ellipsized'
7
+ require 'timeout'
6
8
  require_relative '../lib/factbase'
7
9
 
8
10
  def bench_query(bmk, fb)
9
11
  total = 20_000
10
12
  total.times do |i|
11
13
  f = fb.insert
14
+ f.what = 'foo'
15
+ f.where = 'github'
12
16
  f.id = i
13
17
  f.title = "Object Thinking #{i}"
14
18
  f.time = Time.now.iso8601
@@ -20,7 +24,7 @@ def bench_query(bmk, fb)
20
24
  f.zzz = "Extra#{i}" if (i % 10).zero?
21
25
  end
22
26
 
23
- runs = 10
27
+ runs = 3
24
28
  [
25
29
  '(gt time \'2024-03-23T03:21:43Z\')',
26
30
  '(gt cost 50)',
@@ -28,11 +32,21 @@ def bench_query(bmk, fb)
28
32
  '(and (eq foo 42.998) (or (gt bar 200) (absent zzz)))',
29
33
  '(and (exists foo) (not (exists blue)))',
30
34
  '(eq id (agg (always) (max id)))',
31
- '(join "c<=cost,b<=bar" (eq id (agg (always) (max id))))'
35
+ '(join "c<=cost,b<=bar" (eq id (agg (always) (max id))))',
36
+ '(and
37
+ (eq what "foo")
38
+ (join "w<=what"
39
+ (and
40
+ (eq id $id)
41
+ (eq what "foo")
42
+ (eq where "github")))
43
+ (assert "has it" (one id)))'
32
44
  ].each do |q|
33
- bmk.report(q) do
34
- runs.times do
35
- fb.query(q).each.to_a
45
+ bmk.report(q.tr("\n", ' ').gsub(/\s+/, ' ').ellipsized(50, :right)) do
46
+ Timeout.timeout(runs * 3) do
47
+ runs.times do
48
+ fb.query(q).each.to_a
49
+ end
36
50
  end
37
51
  end
38
52
  end
@@ -11,7 +11,10 @@ facts:
11
11
  - person: Walter
12
12
  age: 45
13
13
  queries:
14
- - query: (and (join 'age<=age' (eq person $name)) (exists age))
14
+ - query: |
15
+ (and
16
+ (join 'age<=age' (eq person $name))
17
+ (exists age))
15
18
  size: 4
16
19
  - query: (and (join 'age' (eq person $name)) (exists age))
17
20
  size: 4
@@ -4,6 +4,7 @@
4
4
  # SPDX-License-Identifier: MIT
5
5
 
6
6
  require 'decoor'
7
+ require 'tago'
7
8
  require 'timeout'
8
9
  require_relative 'syntax'
9
10
 
@@ -61,7 +62,7 @@ class Factbase::Impatient
61
62
  qry.each(fb, params, &)
62
63
  end
63
64
  rescue Timeout::Error => e
64
- raise "Query timed out after #{@timeout} seconds: #{e.message}"
65
+ raise "each() timed out after #{@timeout.seconds} (#{e.message}): #{@term}"
65
66
  end
66
67
 
67
68
  def one(fb = @fb, params = {})
@@ -70,7 +71,7 @@ class Factbase::Impatient
70
71
  qry.one(fb, params)
71
72
  end
72
73
  rescue Timeout::Error => e
73
- raise "Query timed out after #{@timeout} seconds: #{e.message}"
74
+ raise "one() timed out after #{@timeout.seconds} (#{e.message}): #{@term}"
74
75
  end
75
76
 
76
77
  def delete!(fb = @fb)
@@ -79,7 +80,7 @@ class Factbase::Impatient
79
80
  qry.delete!(fb)
80
81
  end
81
82
  rescue Timeout::Error => e
82
- raise "Query timed out after #{@timeout} seconds: #{e.message}"
83
+ raise "delete!() timed out after #{@timeout.seconds} (#{e.message}): #{@term}"
83
84
  end
84
85
  end
85
86
  end
@@ -40,6 +40,15 @@ module Factbase::IndexedTerm
40
40
  end
41
41
  end
42
42
  (maps & []) | @idx[key]
43
+ when :absent
44
+ if @idx[key].nil?
45
+ @idx[key] = []
46
+ prop = @operands.first.to_s
47
+ maps.to_a.each do |m|
48
+ @idx[key].append(m) if m[prop].nil?
49
+ end
50
+ end
51
+ (maps & []) | @idx[key]
43
52
  when :eq
44
53
  if @operands.first.is_a?(Symbol) && _scalar?(@operands[1])
45
54
  if @idx[key].nil?
@@ -59,7 +68,7 @@ module Factbase::IndexedTerm
59
68
  [@operands[1]]
60
69
  end
61
70
  if vv.empty?
62
- nil
71
+ (maps & [])
63
72
  else
64
73
  j = vv.map { |v| @idx[key][v] || [] }.reduce(&:|)
65
74
  (maps & []) | j
@@ -94,10 +103,7 @@ module Factbase::IndexedTerm
94
103
  else
95
104
  @operands.each do |o|
96
105
  n = o.predict(maps, params)
97
- if n.nil?
98
- r = nil
99
- break
100
- end
106
+ break if n.nil?
101
107
  if r.nil?
102
108
  r = n
103
109
  else
@@ -45,7 +45,8 @@ class Factbase::Query
45
45
  yielded = 0
46
46
  params = params.transform_keys(&:to_s) if params.is_a?(Hash)
47
47
  maybe = @term.predict(@maps, Factbase::Tee.new({}, params))
48
- (maybe || @maps).each do |m|
48
+ maybe ||= @maps unless maybe.equal?(@maps)
49
+ maybe.each do |m|
49
50
  extras = {}
50
51
  f = Factbase::Fact.new(m)
51
52
  f = Factbase::Tee.new(f, params)
data/lib/factbase/term.rb CHANGED
@@ -90,6 +90,7 @@ class Factbase::Term
90
90
  # @param [Hash] args Attributes to set
91
91
  def redress!(type, **args)
92
92
  extend type
93
+
93
94
  args.each { |k, v| send(:instance_variable_set, :"@#{k}", v) }
94
95
  @operands.map do |op|
95
96
  if op.is_a?(Factbase::Term)
@@ -163,19 +164,22 @@ class Factbase::Term
163
164
  # Turns it into a string.
164
165
  # @return [String] The string of it
165
166
  def to_s
166
- items = []
167
- items << @op
168
- items +=
169
- @operands.map do |o|
170
- if o.is_a?(String)
171
- "'#{o.gsub("'", "\\\\'").gsub('"', '\\\\"')}'"
172
- elsif o.is_a?(Time)
173
- o.utc.iso8601
174
- else
175
- o.to_s
176
- end
167
+ @to_s ||=
168
+ begin
169
+ items = []
170
+ items << @op
171
+ items +=
172
+ @operands.map do |o|
173
+ if o.is_a?(String)
174
+ "'#{o.gsub("'", "\\\\'").gsub('"', '\\\\"')}'"
175
+ elsif o.is_a?(Time)
176
+ o.utc.iso8601
177
+ else
178
+ o.to_s
179
+ end
180
+ end
181
+ "(#{items.join(' ')})"
177
182
  end
178
- "(#{items.join(' ')})"
179
183
  end
180
184
 
181
185
  def at(fact, maps, fb)
@@ -9,5 +9,5 @@
9
9
  # License:: MIT
10
10
  class Factbase
11
11
  # Current version of the gem (changed by .rultor.yml on every release)
12
- VERSION = '0.14.0' unless const_defined?(:VERSION)
12
+ VERSION = '0.14.2' unless const_defined?(:VERSION)
13
13
  end
@@ -3,6 +3,9 @@
3
3
  # SPDX-FileCopyrightText: Copyright (c) 2024-2025 Yegor Bugayenko
4
4
  # SPDX-License-Identifier: MIT
5
5
 
6
+ require 'elapsed'
7
+ require 'loog'
8
+ require 'timeout'
6
9
  require_relative '../../test__helper'
7
10
  require_relative '../../../lib/factbase'
8
11
  require_relative '../../../lib/factbase/indexed/indexed_factbase'
@@ -58,4 +61,36 @@ class TestIndexedFactbase < Factbase::Test
58
61
  fb.txn(&:insert)
59
62
  refute_empty(fb.query('(always)').each.to_a)
60
63
  end
64
+
65
+ def test_works_with_huge_dataset
66
+ fb = Factbase.new
67
+ fb = Factbase::IndexedFactbase.new(fb)
68
+ 10_000.times do |i|
69
+ fb.insert.then do |f|
70
+ f.id = i
71
+ f.foo = [42, 1, 256, 7, 99].sample
72
+ f.bar = [42, 13, 88, 19, 93].sample
73
+ f.rarely = rand if rand > 0.95
74
+ f.often = rand if rand > 0.05
75
+ end
76
+ end
77
+ [
78
+ '(and (eq foo 42) (exists bar))',
79
+ '(and (eq foo 42) (exists rarely))',
80
+ '(and (eq foo 42) (exists often))',
81
+ '(and (eq foo 42) (exists often) (exists bar) (absent rarely))',
82
+ '(and (eq foo 42) (empty (eq foo 888)))',
83
+ '(and (eq foo 42) (empty (eq foo $id)))',
84
+ '(and (eq foo 42) (empty (eq foo $often)))',
85
+ '(and (eq foo 42) (empty (and (eq foo $often) (gt foo 43))))',
86
+ '(and (eq foo 42) (empty (and (eq foo 42) (eq bar 42) (eq id -1))))',
87
+ '(and (eq foo 42) (empty (exists another)))'
88
+ ].each do |q|
89
+ Timeout.timeout(4) do
90
+ elapsed(Loog::NULL, good: q) do
91
+ refute_empty(fb.query(q).each.to_a)
92
+ end
93
+ end
94
+ end
95
+ end
61
96
  end
@@ -4,7 +4,9 @@
4
4
  # SPDX-License-Identifier: MIT
5
5
 
6
6
  require_relative '../../test__helper'
7
+ require_relative '../../../lib/factbase/accum'
7
8
  require_relative '../../../lib/factbase/term'
9
+ require_relative '../../../lib/factbase/syntax'
8
10
 
9
11
  # Logical test.
10
12
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
@@ -92,33 +92,33 @@ class TestImpatient < Factbase::Test
92
92
  sleep 0.2
93
93
  end
94
94
  end
95
- assert_includes(ex.message, 'Query timed out after 0.1 seconds')
95
+ assert_includes(ex.message, 'timed out after')
96
96
  end
97
97
 
98
98
  def test_query_one_timeout
99
- slow_fb = SlowFactbase.new
100
- 1000.times do
101
- slow_fb.insert.value = rand(1000)
99
+ slow = SlowFactbase.new
100
+ 10_000.times do
101
+ slow.insert.value = rand(1000)
102
102
  end
103
- fb = Factbase::Impatient.new(slow_fb, timeout: 0.1)
103
+ fb = Factbase::Impatient.new(slow, timeout: 0.01)
104
104
  ex =
105
105
  assert_raises(StandardError) do
106
- fb.query('(always)').one
106
+ fb.query('(agg (min value))').one
107
107
  end
108
- assert_includes(ex.message, 'Query timed out after 0.1 seconds')
108
+ assert_includes(ex.message, 'timed out after')
109
109
  end
110
110
 
111
111
  def test_delete_timeout
112
- slow_fb = SlowDeleteFactbase.new
112
+ slow = SlowDeleteFactbase.new
113
113
  1000.times do |i|
114
- slow_fb.insert.value = i
114
+ slow.insert.value = i
115
115
  end
116
- fb = Factbase::Impatient.new(slow_fb, timeout: 0.1)
116
+ fb = Factbase::Impatient.new(slow, timeout: 0.01)
117
117
  ex =
118
118
  assert_raises(StandardError) do
119
119
  fb.query('(gt value 500)').delete!
120
120
  end
121
- assert_includes(ex.message, 'Query timed out after 0.1 seconds')
121
+ assert_includes(ex.message, 'timed out after')
122
122
  end
123
123
 
124
124
  def test_with_txn
@@ -141,7 +141,7 @@ class TestImpatient < Factbase::Test
141
141
  sleep 0.2
142
142
  end
143
143
  end
144
- assert_includes(ex.message, 'Query timed out after 0.1 seconds')
144
+ assert_includes(ex.message, 'timed out after')
145
145
  end
146
146
  end
147
147
 
@@ -184,9 +184,9 @@ class TestImpatient < Factbase::Test
184
184
  end
185
185
 
186
186
  def test_custom_timeout
187
- slow_fb = SlowEnoughFactbase.new
188
- slow_fb.insert.value = 42
189
- fb = Factbase::Impatient.new(slow_fb, timeout: 2)
187
+ slow = SlowEnoughFactbase.new
188
+ slow.insert.value = 42
189
+ fb = Factbase::Impatient.new(slow, timeout: 2)
190
190
  start = Time.now
191
191
  result = fb.query('(agg (eq value 42) (first value))').one
192
192
  elapsed = Time.now - start
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: factbase
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.14.0
4
+ version: 0.14.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yegor Bugayenko