factbase 0.9.3 → 0.9.4

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: 39eb84c924fd5a4c42a73f3a9cc077d3d48d2973d513602c81b7cebb2ed9c35f
4
+ data.tar.gz: 9187cac9070ee7444a0849eef2a48ce78118840bc53c2e1e01f642924c69ea91
5
5
  SHA512:
6
- metadata.gz: 6dd94399d95fd3b6e3654786436d63f37ab650d47261b79ea09c5012e81273579afc53285310dc57e877018faffd40609dea2156597d49157b2caa2eeac91353
7
- data.tar.gz: eaea23931db92c65643e0617617246b8d7b7aebca40c11f4eae158fefe9ad0cb169bae5cac05155288e62b112d52eae407e2ced76043f76e24d7091ec64efa28
6
+ metadata.gz: c27c52ab2a3c7e9e26b2964d6cecb504249d9d3be4ac1206367dbc885d9e7892671f89c37c681218818849189ccf672348b4478c7c93fa7117883e0e49cd63a4
7
+ data.tar.gz: b1a7b747bf06380b76369e73bed096a7805f100872809b61f12d3dcf6bec60871e69a822da739b961b62c4d6d3542d68229a403522f24121cee2b8b93765e6c6
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,43 @@ 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)
15
23
  case @op
24
+ when :one
25
+ key = [maps.object_id, @operands.first, @op]
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
+ key = [maps.object_id, @operands.first, @op]
36
+ if @idx[key].nil?
37
+ @idx[key] = []
38
+ prop = @operands.first.to_s
39
+ maps.to_a.each do |m|
40
+ @idx[key].append(m) unless m[prop].nil?
41
+ end
42
+ end
43
+ (maps & []) | @idx[key]
16
44
  when :eq
17
- if @operands[0].is_a?(Symbol) && _scalar?(@operands[1])
18
- key = [maps.object_id, @operands[0], @op]
45
+ if @operands.first.is_a?(Symbol) && _scalar?(@operands[1])
46
+ key = [maps.object_id, @operands.first, @op]
19
47
  if @idx[key].nil?
20
48
  @idx[key] = {}
49
+ prop = @operands.first.to_s
21
50
  maps.to_a.each do |m|
22
- m[@operands[0].to_s]&.each do |v|
51
+ m[prop]&.each do |v|
23
52
  @idx[key][v] = [] if @idx[key][v].nil?
24
53
  @idx[key][v].append(m)
25
54
  end
@@ -32,23 +61,48 @@ module Factbase::IndexedTerm
32
61
  else
33
62
  [@operands[1]]
34
63
  end
35
- vv.map { |v| @idx[key][v] || [] }.reduce(&:|)
36
- else
37
- maps.to_a
64
+ if vv.empty?
65
+ nil
66
+ else
67
+ j = vv.map { |v| @idx[key][v] || [] }.reduce(&:|)
68
+ (maps & []) | j
69
+ end
38
70
  end
39
71
  when :and
40
- parts = @operands.map { |o| o.predict(maps, params) }
41
- if parts.include?(nil)
42
- maps
43
- else
44
- parts.reduce(&:&)
72
+ r = nil
73
+ @operands.each do |o|
74
+ n = o.predict(maps, params)
75
+ if n.nil?
76
+ r = nil
77
+ break
78
+ end
79
+ if r.nil?
80
+ r = n
81
+ else
82
+ r &= n
83
+ end
84
+ break if r.empty?
45
85
  end
86
+ r
46
87
  when :or
47
- @operands.map { |o| o.predict(maps, params) }.reduce(&:|)
48
- when :join, :as
49
- nil
50
- else
51
- maps.to_a
88
+ r = nil
89
+ @operands.each do |o|
90
+ n = o.predict(maps, params)
91
+ if n.nil?
92
+ r = nil
93
+ break
94
+ end
95
+ r = maps & [] if r.nil?
96
+ r |= n
97
+ end
98
+ r
99
+ when :not
100
+ r = @operands.first.predict(maps, params)
101
+ if r.nil?
102
+ nil
103
+ else
104
+ (maps & []) | (maps.to_a - r.to_a)
105
+ end
52
106
  end
53
107
  end
54
108
 
@@ -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
@@ -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,13 @@ 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
+ join(other, &:|)
68
77
  end
69
78
 
70
79
  # Decorator of Hash.
@@ -129,4 +138,25 @@ class Factbase::Taped
129
138
  @origin.uniq!
130
139
  end
131
140
  end
141
+
142
+ private
143
+
144
+ def join(other)
145
+ n = yield @origin.to_a, other.to_a
146
+ if other.respond_to?(:inserted)
147
+ Factbase::Taped.new(
148
+ n,
149
+ inserted: @inserted | other.inserted,
150
+ deleted: @deleted | other.deleted,
151
+ added: @added | other.added
152
+ )
153
+ else
154
+ Factbase::Taped.new(
155
+ n,
156
+ inserted: @inserted,
157
+ deleted: @deleted,
158
+ added: @added
159
+ )
160
+ end
161
+ end
132
162
  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.4' 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,44 @@
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
+ origin = Factbase.new
28
+ fb = Factbase::IndexedFactbase.new(origin)
29
+ fb.insert.foo = 42
30
+ fb.txn do |fbt|
31
+ fbt.query('(exists foo)').each do |f|
32
+ f.bar = f.foo + 1
33
+ end
34
+ end
35
+ refute_empty(origin.query('(exists bar)').each.to_a)
36
+ refute_empty(fb.query('(exists bar)').each.to_a)
37
+ end
38
+
39
+ def test_queries_after_insert_in_txn
40
+ fb = Factbase::IndexedFactbase.new(Factbase.new)
41
+ fb.txn(&:insert)
42
+ refute_empty(fb.query('(always)').each.to_a)
43
+ end
44
+ 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,92 @@
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
+ end
25
+
26
+ def test_predicts_on_exists
27
+ term = Factbase::Term.new(:exists, [:foo])
28
+ idx = {}
29
+ term.redress!(Factbase::IndexedTerm, idx:)
30
+ maps = Factbase::Taped.new([{ 'foo' => [42] }, { 'bar' => [7] }, { 'foo' => [22, 42] }, { 'foo' => [] }])
31
+ n = term.predict(maps, { a: 1 })
32
+ assert_equal(3, n.size)
33
+ end
34
+
35
+ def test_predicts_on_one
36
+ term = Factbase::Term.new(:one, [:foo])
37
+ idx = {}
38
+ term.redress!(Factbase::IndexedTerm, idx:)
39
+ maps = Factbase::Taped.new([{ 'foo' => [42] }, { 'bar' => [7] }, { 'foo' => [22, 42] }, { 'foo' => [] }])
40
+ n = term.predict(maps, { a: 1 })
41
+ assert_equal(1, n.size)
42
+ end
43
+
44
+ def test_predicts_on_not
45
+ term = Factbase::Term.new(:not, [Factbase::Term.new(:eq, [:foo, 42])])
46
+ idx = {}
47
+ term.redress!(Factbase::IndexedTerm, idx:)
48
+ maps = Factbase::Taped.new([{ 'foo' => [42] }, { 'bar' => [7], 'foo' => [22, 42] }, { 'foo' => [22] }])
49
+ n = term.predict(maps, { a: 1 })
50
+ assert_equal(1, n.size)
51
+ end
52
+
53
+ def test_predicts_on_and
54
+ term = Factbase::Term.new(
55
+ :and,
56
+ [
57
+ Factbase::Term.new(:eq, [:foo, 42]),
58
+ Factbase::Term.new(:eq, [:bar, 7])
59
+ ]
60
+ )
61
+ idx = {}
62
+ term.redress!(Factbase::IndexedTerm, idx:)
63
+ maps = Factbase::Taped.new([{ 'foo' => [42] }, { 'bar' => [7], 'foo' => [22, 42] }, { 'foo' => [22, 42] }])
64
+ n = term.predict(maps, { a: 1 })
65
+ assert_equal(1, n.size)
66
+ end
67
+
68
+ def test_predicts_on_or
69
+ term = Factbase::Term.new(
70
+ :or,
71
+ [
72
+ Factbase::Term.new(:exists, [:bar]),
73
+ Factbase::Term.new(:eq, [:foo, 42]),
74
+ Factbase::Term.new(:eq, [:bar, 7])
75
+ ]
76
+ )
77
+ idx = {}
78
+ term.redress!(Factbase::IndexedTerm, idx:)
79
+ maps = Factbase::Taped.new([{ 'foo' => [42] }, { 'bar' => [7], 'foo' => [22, 42] }, { 'foo' => [22, 42] }])
80
+ n = term.predict(maps, { a: 1 })
81
+ assert_equal(3, n.size)
82
+ end
83
+
84
+ def test_predicts_on_others
85
+ term = Factbase::Term.new(:boom, [])
86
+ idx = {}
87
+ term.redress!(Factbase::IndexedTerm, idx:)
88
+ maps = Factbase::Taped.new([{ 'foo' => [42] }, { 'alpha' => [] }, {}])
89
+ n = term.predict(maps, { a: 1 })
90
+ assert_nil(n)
91
+ end
92
+ 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)
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] }
@@ -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.4
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-18 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