factbase 0.9.3 → 0.9.5

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: 20eec19cec2db862d93eb5efc62e1eda1bb01dcc5edc2e96318d6536afcfffd3
4
- data.tar.gz: 63ce57fb3245985e615abd82c10a1c61e72fe4fbd1987c5781b1937ab8bf4ed4
3
+ metadata.gz: '05496abcbec07a37de19ac7f1218943d1d50d1f0b3611ab48e6b8489675d274c'
4
+ data.tar.gz: b8f33d58069745e5405d6f1714c80f6fb3badc437333ce1fe685af5fb245bc42
5
5
  SHA512:
6
- metadata.gz: 6dd94399d95fd3b6e3654786436d63f37ab650d47261b79ea09c5012e81273579afc53285310dc57e877018faffd40609dea2156597d49157b2caa2eeac91353
7
- data.tar.gz: eaea23931db92c65643e0617617246b8d7b7aebca40c11f4eae158fefe9ad0cb169bae5cac05155288e62b112d52eae407e2ced76043f76e24d7091ec64efa28
6
+ metadata.gz: 1475969fc18741368eff7ababc1ff5764f36244f3e11276d21a5d4f03f9d5c3ce444cf9ecd0482be3e9f3a3b3fadbc11cba5a711244dd26b39fc4be60a6915f9
7
+ data.tar.gz: 2e30a6d6cc39ada5038f98d8b6112250e527b756a0f9d5f5a8aebecd661d8de8e022d4d5659065ec0273035fbe740f5ea655b3f82e701993084a6755c96700f4
data/.rubocop.yml CHANGED
@@ -29,9 +29,9 @@ Metrics/AbcSize:
29
29
  Metrics/BlockLength:
30
30
  Max: 50
31
31
  Metrics/CyclomaticComplexity:
32
- Max: 25
32
+ Max: 40
33
33
  Metrics/PerceivedComplexity:
34
- Max: 30
34
+ Max: 50
35
35
  Metrics/ClassLength:
36
36
  Enabled: false
37
37
  Layout/EmptyLineAfterGuardClause:
@@ -44,4 +44,3 @@ Security/MarshalLoad:
44
44
  Enabled: false
45
45
  Layout/MultilineAssignmentLayout:
46
46
  Enabled: true
47
- require: []
data/README.md CHANGED
@@ -209,33 +209,33 @@ This is the result of the benchmark:
209
209
  <!-- benchmark_begin -->
210
210
  ```text
211
211
  user system total real
212
- insert 20000 facts 0.584859 0.005297 0.590156 ( 0.590186)
213
- export 20000 facts 0.018342 0.005935 0.024277 ( 0.024282)
214
- import 410974 bytes (20000 facts) 0.028027 0.004994 0.033021 ( 0.033026)
215
- insert 10 facts 0.041771 0.000989 0.042760 ( 0.042763)
216
- query 10 times w/txn 2.020294 0.038146 2.058440 ( 2.059555)
217
- query 10 times w/o txn 0.727753 0.001014 0.728767 ( 0.728806)
218
- modify 10 attrs w/txn 1.919959 0.014025 1.933984 ( 1.934328)
219
- delete 10 facts w/txn 0.739267 0.000000 0.739267 ( 0.739299)
220
- (and (eq what 'issue-was-closed') (exists... -> 200 2.717595 0.003012 2.720607 ( 2.720777)
221
- (and (eq what 'issue-was-closed') (exists... -> 200/txn 2.732913 0.002012 2.734925 ( 2.735227)
222
- (and (eq what 'issue-was-closed') (exists... -> zero 4.149053 0.001010 4.150063 ( 4.150436)
223
- (and (eq what 'issue-was-closed') (exists... -> zero/txn 4.116972 0.001001 4.117973 ( 4.119644)
224
- (gt time '2024-03-23T03:21:43Z') 0.107332 0.000002 0.107334 ( 0.107336)
225
- (gt cost 50) 0.094454 0.000000 0.094454 ( 0.094458)
226
- (eq title 'Object Thinking 5000') 0.003828 0.000000 0.003828 ( 0.003830)
227
- (and (eq foo 42.998) (or (gt bar 200) (absent zzz))) 0.055983 0.000000 0.055983 ( 0.055986)
228
- (eq id (agg (always) (max id))) 0.329920 0.001995 0.331915 ( 0.332005)
229
- (join "c<=cost,b<=bar" (eq id (agg (always) (max id)))) 1.448262 0.002005 1.450267 ( 1.450350)
230
- delete! 0.056928 0.000000 0.056928 ( 0.056932)
231
- Taped.append() x50000 0.035070 0.001000 0.036070 ( 0.036074)
232
- Taped.each() x125 1.310450 0.000000 1.310450 ( 1.310521)
233
- Taped.delete_if() x375 0.819224 0.000001 0.819225 ( 0.819270)
212
+ insert 20000 facts 0.571788 0.003758 0.575546 ( 0.575570)
213
+ export 20000 facts 0.020920 0.002996 0.023916 ( 0.023920)
214
+ import 410695 bytes (20000 facts) 0.027100 0.007016 0.034116 ( 0.034121)
215
+ insert 10 facts 0.041616 0.004054 0.045670 ( 0.045682)
216
+ query 10 times w/txn 1.631523 0.024886 1.656409 ( 1.656459)
217
+ query 10 times w/o txn 0.108811 0.000990 0.109801 ( 0.109804)
218
+ modify 10 attrs w/txn 1.207344 0.019000 1.226344 ( 1.226358)
219
+ delete 10 facts w/txn 0.734874 0.001988 0.736862 ( 0.736890)
220
+ (and (eq what 'issue-was-closed') (exists... -> 200 5.046822 0.008973 5.055795 ( 5.056175)
221
+ (and (eq what 'issue-was-closed') (exists... -> 200/txn 5.006788 0.009980 5.016768 ( 5.017401)
222
+ (and (eq what 'issue-was-closed') (exists... -> zero 7.793528 0.003992 7.797520 ( 7.797774)
223
+ (and (eq what 'issue-was-closed') (exists... -> zero/txn 7.741986 0.001995 7.743981 ( 7.744395)
224
+ (gt time '2024-03-23T03:21:43Z') 0.084850 0.000000 0.084850 ( 0.084850)
225
+ (gt cost 50) 0.084610 0.001002 0.085612 ( 0.085614)
226
+ (eq title 'Object Thinking 5000') 0.002526 0.000000 0.002526 ( 0.002526)
227
+ (and (eq foo 42.998) (or (gt bar 200) (absent zzz))) 0.163174 0.000000 0.163174 ( 0.163207)
228
+ (eq id (agg (always) (max id))) 0.247507 0.001001 0.248508 ( 0.248508)
229
+ (join "c<=cost,b<=bar" (eq id (agg (always) (max id)))) 0.622045 0.004995 0.627040 ( 0.627095)
230
+ delete! 0.061154 0.000001 0.061155 ( 0.061156)
231
+ Taped.append() x50000 0.023736 0.003999 0.027735 ( 0.027739)
232
+ Taped.each() x125 1.350286 0.000002 1.350288 ( 1.350416)
233
+ Taped.delete_if() x375 0.803790 0.000000 0.803790 ( 0.803852)
234
234
  ```
235
235
 
236
236
  The results were calculated in [this GHA job][benchmark-gha]
237
- on 2025-03-14 at 09:14,
237
+ on 2025-03-18 at 12:45,
238
238
  on Linux with 4 CPUs.
239
239
  <!-- benchmark_end -->
240
240
 
241
- [benchmark-gha]: https://github.com/yegor256/factbase/actions/runs/13853425019
241
+ [benchmark-gha]: https://github.com/yegor256/factbase/actions/runs/13923519605
@@ -0,0 +1,19 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2024-2025 Yegor Bugayenko
2
+ # SPDX-License-Identifier: MIT
3
+ ---
4
+ # yamllint disable rule:line-length
5
+ facts:
6
+ - name: Jeff
7
+ age: 41
8
+ movie: Big Lebowski
9
+ - name: Walter
10
+ age: 43
11
+ friend: Donny
12
+ movie: Big Lebowski
13
+ queries:
14
+ - query: (and (eq movie "Big Lebowski") (exists age) (not (exists friend)))
15
+ size: 1
16
+ - query: (and (exists age) (not (exists foo)))
17
+ size: 2
18
+ - query: (and (exists age) (exists foo))
19
+ size: 0
@@ -0,0 +1,21 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2024-2025 Yegor Bugayenko
2
+ # SPDX-License-Identifier: MIT
3
+ ---
4
+ # yamllint disable rule:line-length
5
+ facts:
6
+ - man: Jeff
7
+ wife: Sarah
8
+ - man: Walter
9
+ wife: Lucy
10
+ - man: Donny
11
+ wife: Nicole
12
+ - man: Peter
13
+ wife: Dorah
14
+ - woman: Lucy
15
+ - woman: Nicole
16
+ - woman: Dorah
17
+ queries:
18
+ - query: (and (exists man) (not (empty (and (exists woman) (eq woman $wife)))))
19
+ size: 3
20
+ - query: (and (exists man) (empty (and (exists woman) (eq woman $wife))))
21
+ size: 1
@@ -0,0 +1,14 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2024-2025 Yegor Bugayenko
2
+ # SPDX-License-Identifier: MIT
3
+ ---
4
+ # yamllint disable rule:line-length
5
+ facts:
6
+ - name: Jeff
7
+ friends: [Walter, Donny, Maude]
8
+ - name: Walter
9
+ friends: Donny
10
+ - name: Donny
11
+ friends: Walter
12
+ queries:
13
+ - query: (one friends)
14
+ size: 2
@@ -0,0 +1,16 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2024-2025 Yegor Bugayenko
2
+ # SPDX-License-Identifier: MIT
3
+ ---
4
+ # yamllint disable rule:line-length
5
+ facts:
6
+ - name: Volvo
7
+ - name: Ford
8
+ - name: Toyota
9
+ price: 5900
10
+ - name: Toyota
11
+ price: 4300
12
+ - name: BMW
13
+ price: 4400
14
+ queries:
15
+ - query: (and (exists price) (unique name))
16
+ size: 2
@@ -4,6 +4,7 @@
4
4
  # SPDX-License-Identifier: MIT
5
5
 
6
6
  require_relative '../../factbase'
7
+ require_relative 'cached_fact'
7
8
 
8
9
  # Query with a cache, a decorator of another query.
9
10
  #
@@ -30,13 +31,12 @@ class Factbase::CachedQuery
30
31
  # @param [Hash] params Optional params accessible in the query via the "$" symbol
31
32
  # @yield [Fact] Facts one-by-one
32
33
  # @return [Integer] Total number of facts yielded
33
- def each(fb = @fb, params = nil)
34
+ def each(fb = @fb, params = {})
34
35
  return to_enum(__method__, fb, params) unless block_given?
35
- key = "each #{@origin}"
36
+ key = "each #{@origin}" # params are ignored!
36
37
  before = @cache[key]
37
- @cache[key] = @origin.each(fb).to_a if before.nil?
38
+ @cache[key] = @origin.each(fb, params).to_a if before.nil?
38
39
  @cache[key].each do |f|
39
- require_relative 'cached_fact'
40
40
  yield Factbase::CachedFact.new(f, @cache)
41
41
  end
42
42
  end
@@ -45,10 +45,10 @@ class Factbase::CachedQuery
45
45
  # @param [Hash] fb The factbase
46
46
  # @param [Hash] _params Optional params accessible in the query via the "$" symbol (unused)
47
47
  # @return The value evaluated
48
- def one(fb = @fb, _params = nil)
49
- key = "one: #{@origin}"
48
+ def one(fb = @fb, params = {})
49
+ key = "one: #{@origin} #{params}"
50
50
  before = @cache[key]
51
- @cache[key] = @origin.one(fb) if before.nil?
51
+ @cache[key] = @origin.one(fb, params) if before.nil?
52
52
  @cache[key]
53
53
  end
54
54
 
@@ -33,7 +33,7 @@ class Factbase::IndexedQuery
33
33
  # @return [Integer] Total number of facts yielded
34
34
  def each(fb = @fb, params = {})
35
35
  return to_enum(__method__, fb, params) unless block_given?
36
- @origin.each(fb, params) do |f|
36
+ @origin.each(fb, params).to_a.each do |f|
37
37
  yield Factbase::IndexedFact.new(f, @idx)
38
38
  end
39
39
  end
@@ -3,6 +3,7 @@
3
3
  # SPDX-FileCopyrightText: Copyright (c) 2024-2025 Yegor Bugayenko
4
4
  # SPDX-License-Identifier: MIT
5
5
 
6
+ require 'tago'
6
7
  require_relative '../../factbase'
7
8
 
8
9
  # Term with an index.
@@ -11,15 +12,41 @@ require_relative '../../factbase'
11
12
  # Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
12
13
  # License:: MIT
13
14
  module Factbase::IndexedTerm
15
+ # Reduces the provided list of facts (maps) to a smaller array, if it's possible.
16
+ #
17
+ # NIL must be returned if indexing is prohibited in this case.
18
+ #
19
+ # @param [Array<Hash>] maps Array of facts
20
+ # @param [Hash] params Key/value params to use
21
+ # @return [Array<Hash>|nil] Returns a new array, or NIL if the original array must be used
14
22
  def predict(maps, params)
23
+ key = [maps.object_id, @operands.first, @op]
15
24
  case @op
25
+ when :one
26
+ if @idx[key].nil?
27
+ @idx[key] = []
28
+ prop = @operands.first.to_s
29
+ maps.to_a.each do |m|
30
+ @idx[key].append(m) if !m[prop].nil? && m[prop].size == 1
31
+ end
32
+ end
33
+ (maps & []) | @idx[key]
34
+ when :exists
35
+ if @idx[key].nil?
36
+ @idx[key] = []
37
+ prop = @operands.first.to_s
38
+ maps.to_a.each do |m|
39
+ @idx[key].append(m) unless m[prop].nil?
40
+ end
41
+ end
42
+ (maps & []) | @idx[key]
16
43
  when :eq
17
- if @operands[0].is_a?(Symbol) && _scalar?(@operands[1])
18
- key = [maps.object_id, @operands[0], @op]
44
+ if @operands.first.is_a?(Symbol) && _scalar?(@operands[1])
19
45
  if @idx[key].nil?
20
46
  @idx[key] = {}
47
+ prop = @operands.first.to_s
21
48
  maps.to_a.each do |m|
22
- m[@operands[0].to_s]&.each do |v|
49
+ m[prop]&.each do |v|
23
50
  @idx[key][v] = [] if @idx[key][v].nil?
24
51
  @idx[key][v].append(m)
25
52
  end
@@ -32,23 +59,48 @@ module Factbase::IndexedTerm
32
59
  else
33
60
  [@operands[1]]
34
61
  end
35
- vv.map { |v| @idx[key][v] || [] }.reduce(&:|)
36
- else
37
- maps.to_a
62
+ if vv.empty?
63
+ nil
64
+ else
65
+ j = vv.map { |v| @idx[key][v] || [] }.reduce(&:|)
66
+ (maps & []) | j
67
+ end
38
68
  end
39
69
  when :and
40
- parts = @operands.map { |o| o.predict(maps, params) }
41
- if parts.include?(nil)
42
- maps
43
- else
44
- parts.reduce(&:&)
70
+ r = nil
71
+ @operands.each do |o|
72
+ n = o.predict(maps, params)
73
+ if n.nil?
74
+ r = nil
75
+ break
76
+ end
77
+ if r.nil?
78
+ r = n
79
+ else
80
+ r &= n.to_a
81
+ end
82
+ break if r.empty?
45
83
  end
84
+ r
46
85
  when :or
47
- @operands.map { |o| o.predict(maps, params) }.reduce(&:|)
48
- when :join, :as
49
- nil
50
- else
51
- maps.to_a
86
+ r = nil
87
+ @operands.each do |o|
88
+ n = o.predict(maps, params)
89
+ if n.nil?
90
+ r = nil
91
+ break
92
+ end
93
+ r = maps & [] if r.nil?
94
+ r |= n.to_a
95
+ end
96
+ r
97
+ when :not
98
+ r = @operands.first.predict(maps, params)
99
+ if r.nil?
100
+ nil
101
+ else
102
+ (maps & []) | (maps.to_a - r.to_a)
103
+ end
52
104
  end
53
105
  end
54
106
 
@@ -143,9 +143,9 @@ class Factbase::Logged
143
143
  end
144
144
  raise ".each of #{@term.class} returned #{r.class}" unless r.is_a?(Integer)
145
145
  if r.zero?
146
- @tube.say(start, "Nothing found by '#{q}' #{tail}")
146
+ @tube.say(start, "Zero/#{@fb.size} facts found by '#{q}' #{tail}")
147
147
  else
148
- @tube.say(start, "Found #{r} fact(s) by '#{q}' #{tail}")
148
+ @tube.say(start, "Found #{r}/#{@fb.size} fact(s) by '#{q}' #{tail}")
149
149
  end
150
150
  r
151
151
  else
@@ -157,9 +157,9 @@ class Factbase::Logged
157
157
  end
158
158
  end
159
159
  if array.empty?
160
- @tube.say(start, "Nothing found by '#{q}' #{tail}")
160
+ @tube.say(start, "Zero/#{@fb.size} found by '#{q}' #{tail}")
161
161
  else
162
- @tube.say(start, "Found #{array.size} fact(s) by '#{q}' #{tail}")
162
+ @tube.say(start, "Found #{array.size}/#{@fb.size} fact(s) by '#{q}' #{tail}")
163
163
  end
164
164
  array
165
165
  end
@@ -44,7 +44,8 @@ class Factbase::Query
44
44
  return to_enum(__method__, fb, params) unless block_given?
45
45
  yielded = 0
46
46
  params = params.transform_keys(&:to_s) if params.is_a?(Hash)
47
- (@term.predict(@maps, params) || @maps).each do |m|
47
+ maybe = @term.predict(@maps, params)
48
+ (maybe || @maps).each do |m|
48
49
  extras = {}
49
50
  f = Factbase::Fact.new(m)
50
51
  f = Factbase::Tee.new(f, params)
@@ -12,11 +12,11 @@ require_relative '../factbase'
12
12
  # Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
13
13
  # License:: MIT
14
14
  class Factbase::Taped
15
- def initialize(origin)
15
+ def initialize(origin, inserted: [], deleted: [], added: [])
16
16
  @origin = origin
17
- @inserted = []
18
- @deleted = []
19
- @added = []
17
+ @inserted = inserted
18
+ @deleted = deleted
19
+ @added = added
20
20
  end
21
21
 
22
22
  def inserted
@@ -39,6 +39,10 @@ class Factbase::Taped
39
39
  @origin.size
40
40
  end
41
41
 
42
+ def empty?
43
+ @origin.empty?
44
+ end
45
+
42
46
  def <<(map)
43
47
  @origin << (map)
44
48
  @inserted.append(map.object_id)
@@ -63,8 +67,14 @@ class Factbase::Taped
63
67
  @origin.to_a
64
68
  end
65
69
 
66
- def group_by(&)
67
- @origin.group_by(&)
70
+ def &(other)
71
+ return Factbase::Taped.new([], inserted: @inserted, deleted: @deleted, added: @added) if other == []
72
+ join(other, &:&)
73
+ end
74
+
75
+ def |(other)
76
+ return Factbase::Taped.new(to_a, inserted: @inserted, deleted: @deleted, added: @added) if other == []
77
+ join(other, &:|)
68
78
  end
69
79
 
70
80
  # Decorator of Hash.
@@ -129,4 +139,18 @@ class Factbase::Taped
129
139
  @origin.uniq!
130
140
  end
131
141
  end
142
+
143
+ private
144
+
145
+ def join(other)
146
+ n = yield @origin.to_a, other.to_a
147
+ raise 'Cannot join with another Taped' if other.respond_to?(:inserted)
148
+ raise 'Can only join with array' unless other.is_a?(Array)
149
+ Factbase::Taped.new(
150
+ n,
151
+ inserted: @inserted,
152
+ deleted: @deleted,
153
+ added: @added
154
+ )
155
+ end
132
156
  end
data/lib/factbase/term.rb CHANGED
@@ -85,6 +85,9 @@ class Factbase::Term
85
85
  @operands = operands
86
86
  end
87
87
 
88
+ # Extend it with the module.
89
+ # @param [Module] type The type to extend with
90
+ # @param [Hash] args Attributes to set
88
91
  def redress!(type, **args)
89
92
  extend type
90
93
  args.each { |k, v| send(:instance_variable_set, :"@#{k}", v) }
@@ -74,7 +74,12 @@ module Factbase::Aggregates
74
74
  assert_args(1)
75
75
  term = @operands[0]
76
76
  raise "A term expected, but '#{term}' provided" unless term.is_a?(Factbase::Term)
77
- fb.query(term, maps).each(fb, fact).to_a.empty?
77
+ # rubocop:disable Lint/UnreachableLoop
78
+ fb.query(term, maps).each(fb, fact) do
79
+ return false
80
+ end
81
+ # rubocop:enable Lint/UnreachableLoop
82
+ true
78
83
  end
79
84
 
80
85
  def _best(maps)
@@ -42,12 +42,14 @@ module Factbase::Meta
42
42
  _values(0, fact, maps, fb).nil?
43
43
  end
44
44
 
45
+ # The property has many (more than one) values.
45
46
  def many(fact, maps, fb)
46
47
  assert_args(1)
47
48
  v = _values(0, fact, maps, fb)
48
49
  !v.nil? && v.size > 1
49
50
  end
50
51
 
52
+ # The property has exactly one value.
51
53
  def one(fact, maps, fb)
52
54
  assert_args(1)
53
55
  v = _values(0, fact, maps, fb)
data/lib/factbase.rb CHANGED
@@ -82,7 +82,7 @@ require 'yaml'
82
82
  # License:: MIT
83
83
  class Factbase
84
84
  # Current version of the gem (changed by .rultor.yml on every release)
85
- VERSION = '0.9.3' unless const_defined?(:VERSION)
85
+ VERSION = '0.9.5' unless const_defined?(:VERSION)
86
86
 
87
87
  # An exception that may be thrown in a transaction, to roll it back.
88
88
  class Rollback < StandardError; end
@@ -19,4 +19,17 @@ class TestCachedFactbase < Factbase::Test
19
19
  f.bar = 'test'
20
20
  assert_equal(1, fb.query('(and (eq foo 1) (eq bar "test"))').each.to_a.size)
21
21
  end
22
+
23
+ def test_queries_after_update_in_txn
24
+ origin = Factbase.new
25
+ fb = Factbase::CachedFactbase.new(origin)
26
+ fb.insert.foo = 42
27
+ fb.txn do |fbt|
28
+ fbt.query('(exists foo)').each do |f|
29
+ f.bar = 33
30
+ end
31
+ end
32
+ refute_empty(origin.query('(exists bar)').each.to_a)
33
+ refute_empty(fb.query('(exists bar)').each.to_a)
34
+ end
22
35
  end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2024-2025 Yegor Bugayenko
4
+ # SPDX-License-Identifier: MIT
5
+
6
+ require_relative '../../test__helper'
7
+ require_relative '../../../lib/factbase'
8
+ require_relative '../../../lib/factbase/fact'
9
+ require_relative '../../../lib/factbase/indexed/indexed_fact'
10
+
11
+ # Fact test.
12
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
13
+ # Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
14
+ # License:: MIT
15
+ class TestIndexedFact < Factbase::Test
16
+ def test_updates_origin
17
+ origin = Factbase::Fact.new({})
18
+ fact = Factbase::IndexedFact.new(origin, {})
19
+ fact.foo = 42
20
+ refute_nil(origin['foo'])
21
+ assert_equal(42, origin.foo)
22
+ end
23
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2024-2025 Yegor Bugayenko
4
+ # SPDX-License-Identifier: MIT
5
+
6
+ require_relative '../../test__helper'
7
+ require_relative '../../../lib/factbase'
8
+ require_relative '../../../lib/factbase/indexed/indexed_factbase'
9
+
10
+ # Factbase test.
11
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
12
+ # Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
13
+ # License:: MIT
14
+ class TestIndexedFactbase < Factbase::Test
15
+ def test_queries_after_update
16
+ origin = Factbase.new
17
+ fb = Factbase::IndexedFactbase.new(origin)
18
+ fb.insert.foo = 42
19
+ fb.query('(exists foo)').each do |f|
20
+ f.bar = 33
21
+ end
22
+ refute_empty(origin.query('(exists bar)').each.to_a)
23
+ refute_empty(fb.query('(exists bar)').each.to_a)
24
+ end
25
+
26
+ def test_queries_after_update_in_txn
27
+ [
28
+ '(exists boom)',
29
+ '(one boom)',
30
+ '(and (exists boom) (exists boom))',
31
+ '(and (exists boom) (exists boom) (exists boom))',
32
+ '(and (one boom) (one boom))',
33
+ '(and (one boom) (one foo))',
34
+ '(and (one boom) (one boom) (one boom))',
35
+ '(and (one boom) (one boom) (one boom) (one foo))',
36
+ '(and (one boom) (exists boom))',
37
+ '(and (exists boom) (one boom) (one boom))',
38
+ '(and (exists boom) (exists boom) (one boom))',
39
+ '(and (eq foo 42) (exists boom) (one boom) (not (exists bar)))'
40
+ ].each do |q|
41
+ origin = Factbase.new
42
+ fb = Factbase::IndexedFactbase.new(origin)
43
+ f = fb.insert
44
+ f.foo = 42
45
+ f.boom = 33
46
+ fb.txn do |fbt|
47
+ fbt.query(q).each do |n|
48
+ n.bar = n.foo + 1
49
+ end
50
+ end
51
+ refute_empty(origin.query('(exists bar)').each.to_a, q)
52
+ refute_empty(fb.query('(exists bar)').each.to_a, q)
53
+ end
54
+ end
55
+
56
+ def test_queries_after_insert_in_txn
57
+ fb = Factbase::IndexedFactbase.new(Factbase.new)
58
+ fb.txn(&:insert)
59
+ refute_empty(fb.query('(always)').each.to_a)
60
+ end
61
+ end
@@ -13,6 +13,15 @@ require_relative '../../../lib/factbase/indexed/indexed_factbase'
13
13
  # Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
14
14
  # License:: MIT
15
15
  class TestIndexedQuery < Factbase::Test
16
+ def test_queries_and_updates_origin
17
+ fb = Factbase.new
18
+ fb.insert.foo = 42
19
+ Factbase::IndexedQuery.new(fb.query('(exists foo)'), {}, fb).each do |f|
20
+ f.bar = 33
21
+ end
22
+ refute_empty(fb.query('(exists bar)').each.to_a)
23
+ end
24
+
16
25
  def test_queries_many_times
17
26
  fb = Factbase::IndexedFactbase.new(Factbase.new)
18
27
  total = 5
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2024-2025 Yegor Bugayenko
4
+ # SPDX-License-Identifier: MIT
5
+
6
+ require_relative '../../test__helper'
7
+ require_relative '../../../lib/factbase'
8
+ require_relative '../../../lib/factbase/term'
9
+ require_relative '../../../lib/factbase/taped'
10
+ require_relative '../../../lib/factbase/indexed/indexed_term'
11
+
12
+ # Term test.
13
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
14
+ # Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
15
+ # License:: MIT
16
+ class TestIndexedTerm < Factbase::Test
17
+ def test_predicts_on_eq
18
+ term = Factbase::Term.new(:eq, [:foo, 42])
19
+ idx = {}
20
+ term.redress!(Factbase::IndexedTerm, idx:)
21
+ maps = Factbase::Taped.new([{ 'foo' => [42] }, { 'bar' => [7] }, { 'foo' => [22, 42] }, { 'foo' => [] }])
22
+ n = term.predict(maps, { a: 1 })
23
+ assert_equal(2, n.size)
24
+ assert_kind_of(Factbase::Taped, n)
25
+ end
26
+
27
+ def test_predicts_on_exists
28
+ term = Factbase::Term.new(:exists, [:foo])
29
+ idx = {}
30
+ term.redress!(Factbase::IndexedTerm, idx:)
31
+ maps = Factbase::Taped.new([{ 'foo' => [42] }, { 'bar' => [7] }, { 'foo' => [22, 42] }, { 'foo' => [] }])
32
+ n = term.predict(maps, { a: 1 })
33
+ assert_equal(3, n.size)
34
+ assert_kind_of(Factbase::Taped, n)
35
+ end
36
+
37
+ def test_predicts_on_one
38
+ term = Factbase::Term.new(:one, [:foo])
39
+ idx = {}
40
+ term.redress!(Factbase::IndexedTerm, idx:)
41
+ maps = Factbase::Taped.new([{ 'foo' => [42] }, { 'bar' => [7] }, { 'foo' => [22, 42] }, { 'foo' => [] }])
42
+ n = term.predict(maps, { a: 1 })
43
+ assert_equal(1, n.size)
44
+ assert_kind_of(Factbase::Taped, n)
45
+ end
46
+
47
+ def test_predicts_on_not
48
+ term = Factbase::Term.new(:not, [Factbase::Term.new(:eq, [:foo, 42])])
49
+ idx = {}
50
+ term.redress!(Factbase::IndexedTerm, idx:)
51
+ maps = Factbase::Taped.new([{ 'foo' => [42] }, { 'bar' => [7], 'foo' => [22, 42] }, { 'foo' => [22] }])
52
+ n = term.predict(maps, { a: 1 })
53
+ assert_equal(1, n.size)
54
+ assert_kind_of(Factbase::Taped, n)
55
+ end
56
+
57
+ def test_predicts_on_and
58
+ term = Factbase::Term.new(
59
+ :and,
60
+ [
61
+ Factbase::Term.new(:eq, [:foo, 42]),
62
+ Factbase::Term.new(:eq, [:bar, 7])
63
+ ]
64
+ )
65
+ idx = {}
66
+ term.redress!(Factbase::IndexedTerm, idx:)
67
+ maps = Factbase::Taped.new([{ 'foo' => [42] }, { 'bar' => [7], 'foo' => [22, 42] }, { 'foo' => [22, 42] }])
68
+ n = term.predict(maps, { a: 1 })
69
+ assert_equal(1, n.size)
70
+ assert_kind_of(Factbase::Taped, n)
71
+ end
72
+
73
+ def test_predicts_on_or
74
+ term = Factbase::Term.new(
75
+ :or,
76
+ [
77
+ Factbase::Term.new(:exists, [:bar]),
78
+ Factbase::Term.new(:eq, [:foo, 42]),
79
+ Factbase::Term.new(:eq, [:bar, 7])
80
+ ]
81
+ )
82
+ idx = {}
83
+ term.redress!(Factbase::IndexedTerm, idx:)
84
+ maps = Factbase::Taped.new([{ 'foo' => [42] }, { 'bar' => [7], 'foo' => [22, 42] }, { 'foo' => [22, 42] }])
85
+ n = term.predict(maps, { a: 1 })
86
+ assert_equal(3, n.size)
87
+ assert_kind_of(Factbase::Taped, n)
88
+ end
89
+
90
+ def test_predicts_on_others
91
+ term = Factbase::Term.new(:boom, [])
92
+ idx = {}
93
+ term.redress!(Factbase::IndexedTerm, idx:)
94
+ maps = Factbase::Taped.new([{ 'foo' => [42] }, { 'alpha' => [] }, {}])
95
+ n = term.predict(maps, { a: 1 })
96
+ assert_nil(n)
97
+ end
98
+ end
@@ -50,4 +50,14 @@ class TestAggregates < Factbase::Test
50
50
  assert_equal(r, t.evaluate(Factbase::Fact.new({}), maps, Factbase.new), q)
51
51
  end
52
52
  end
53
+
54
+ def test_empty_with_params
55
+ maps = [
56
+ { 'a' => [3], 'b' => [44] },
57
+ { 'a' => [4], 'b' => [55] }
58
+ ]
59
+ t = Factbase::Syntax.new('(empty (eq b $x))').to_term
60
+ assert(t.evaluate(Factbase::Fact.new({ 'x' => 42 }), maps, Factbase.new))
61
+ refute(t.evaluate(Factbase::Fact.new({ 'x' => 44 }), maps, Factbase.new))
62
+ end
53
63
  end
@@ -127,7 +127,7 @@ class TestLogged < Factbase::Test
127
127
  'Inserted new fact #2',
128
128
  'Set \'bar\' to 3 (Integer)',
129
129
  'Set \'str\' to "Он поскорей звонит. Вбегает\n ... Отъехать в поле к двум дубкам." (String)',
130
- 'Found 1 fact(s) by \'(exists bar)\'',
130
+ 'Found 1/4 fact(s) by \'(exists bar)\'',
131
131
  'Deleted 3 fact(s) out of 4 by \'(not (exists bar))\''
132
132
  ].each do |s|
133
133
  assert_includes(log.to_s, s, "#{log}\n")
@@ -188,6 +188,63 @@ class TestQuery < Factbase::Test
188
188
  end
189
189
  end
190
190
 
191
+ def test_scans_and_inserts
192
+ with_factbases do |_, fb|
193
+ fb.insert.foo = 42
194
+ before = fb.size
195
+ more = 0
196
+ fb.query('(exists foo)').each do |f|
197
+ fb.insert.bar = f.foo
198
+ more += 1
199
+ end
200
+ assert_equal(before + more, fb.size)
201
+ end
202
+ end
203
+
204
+ def test_scans_and_inserts_in_txn
205
+ with_factbases do |_, fb|
206
+ fb.insert.foo = 42
207
+ before = fb.size
208
+ more = 0
209
+ fb.query('(exists foo)').each do |f|
210
+ fb.txn do |fbt|
211
+ fbt.insert.bar = f.foo
212
+ more += 1
213
+ end
214
+ end
215
+ assert_equal(before + more, fb.size)
216
+ end
217
+ end
218
+
219
+ def test_scans_and_inserts_in_queried_txn
220
+ with_factbases do |_, fb|
221
+ fb.insert.foo = 42
222
+ before = fb.size
223
+ more = 0
224
+ fb.txn do |fbt|
225
+ fbt.query('(exists foo)').each do |f|
226
+ fbt.insert.bar = f.foo
227
+ more += 1
228
+ end
229
+ end
230
+ assert_equal(before + more, fb.size)
231
+ end
232
+ end
233
+
234
+ def test_scans_and_appends_in_queried_txn
235
+ with_factbases do |badge, fb|
236
+ fb.insert.foo = 42
237
+ fb.txn do |fbt|
238
+ fbt.query('(exists foo)').each do |f|
239
+ f.bar = 33
240
+ end
241
+ refute_empty(fbt.query('(exists bar)').each.to_a, "in #{badge}")
242
+ end
243
+ refute_empty(fb.query('(exists bar)').each.to_a, "in #{badge}")
244
+ assert_empty(fb.query('(and (exists foo) (not (exists bar)))').each.to_a, "in #{badge}")
245
+ end
246
+ end
247
+
191
248
  def test_to_array
192
249
  maps = []
193
250
  maps << { 'foo' => [42] }
@@ -258,7 +315,10 @@ class TestQuery < Factbase::Test
258
315
  Factbase::SyncFactbase.new(
259
316
  Factbase::CachedFactbase.new(
260
317
  Factbase::IndexedFactbase.new(
261
- Factbase.new(maps)
318
+ Factbase::Logged.new(
319
+ Factbase.new(maps),
320
+ Loog::NULL
321
+ )
262
322
  )
263
323
  )
264
324
  )
@@ -43,4 +43,18 @@ class TestTaped < Factbase::Test
43
43
  end
44
44
  assert_equal(1, t.added.size)
45
45
  end
46
+
47
+ def test_tracks_factbase
48
+ t = Factbase::Taped.new([])
49
+ fb = Factbase.new(t)
50
+ fb.insert
51
+ fb.query('(always)').each do |f|
52
+ f.foo = 42
53
+ f.foo = 5
54
+ end
55
+ fb.query('(always)').delete!
56
+ assert_equal(1, t.inserted.size)
57
+ assert_equal(1, t.added.size)
58
+ assert_equal(1, t.deleted.size)
59
+ end
46
60
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: factbase
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.3
4
+ version: 0.9.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yegor Bugayenko
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-03-17 00:00:00.000000000 Z
10
+ date: 2025-03-19 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: backtrace
@@ -164,17 +164,21 @@ files:
164
164
  - factbase.gemspec
165
165
  - fixtures/stories/agg.yml
166
166
  - fixtures/stories/always.yml
167
+ - fixtures/stories/and.yml
167
168
  - fixtures/stories/as.yml
168
169
  - fixtures/stories/count.yml
170
+ - fixtures/stories/empty.yml
169
171
  - fixtures/stories/eq.yml
170
172
  - fixtures/stories/gt.yml
171
173
  - fixtures/stories/join.yml
172
174
  - fixtures/stories/max.yml
173
175
  - fixtures/stories/min.yml
174
176
  - fixtures/stories/nth.yml
177
+ - fixtures/stories/one.yml
175
178
  - fixtures/stories/or.yml
176
179
  - fixtures/stories/sprintf.yml
177
180
  - fixtures/stories/sum.yml
181
+ - fixtures/stories/unique.yml
178
182
  - lib/factbase.rb
179
183
  - lib/factbase/accum.rb
180
184
  - lib/factbase/cached/cached_fact.rb
@@ -218,7 +222,10 @@ files:
218
222
  - renovate.json
219
223
  - test/factbase/cached/test_cached_factbase.rb
220
224
  - test/factbase/cached/test_cached_query.rb
225
+ - test/factbase/indexed/test_indexed_fact.rb
226
+ - test/factbase/indexed/test_indexed_factbase.rb
221
227
  - test/factbase/indexed/test_indexed_query.rb
228
+ - test/factbase/indexed/test_indexed_term.rb
222
229
  - test/factbase/sync/test_sync_factbase.rb
223
230
  - test/factbase/sync/test_sync_query.rb
224
231
  - test/factbase/terms/test_aggregates.rb