factbase 0.7.5 → 0.9.0

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 (81) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -1
  3. data/Gemfile +1 -1
  4. data/Gemfile.lock +3 -3
  5. data/README.md +24 -27
  6. data/REUSE.toml +7 -2
  7. data/Rakefile +8 -1
  8. data/benchmark/bench_factbase.rb +1 -1
  9. data/fixtures/stories/agg.yml +17 -0
  10. data/fixtures/stories/always.yml +16 -0
  11. data/fixtures/stories/as.yml +16 -0
  12. data/fixtures/stories/count.yml +18 -0
  13. data/fixtures/stories/eq.yml +30 -0
  14. data/fixtures/stories/gt.yml +18 -0
  15. data/fixtures/stories/join.yml +19 -0
  16. data/fixtures/stories/max.yml +14 -0
  17. data/fixtures/stories/min.yml +14 -0
  18. data/fixtures/stories/nth.yml +14 -0
  19. data/fixtures/stories/or.yml +18 -0
  20. data/fixtures/stories/sprintf.yml +12 -0
  21. data/fixtures/stories/sum.yml +14 -0
  22. data/lib/factbase/cached/cached_fact.rb +28 -0
  23. data/lib/factbase/cached/cached_factbase.rb +64 -0
  24. data/lib/factbase/cached/cached_query.rb +61 -0
  25. data/lib/factbase/cached/cached_term.rb +25 -0
  26. data/lib/factbase/fact.rb +13 -13
  27. data/lib/factbase/indexed/indexed_fact.rb +28 -0
  28. data/lib/factbase/indexed/indexed_factbase.rb +64 -0
  29. data/lib/factbase/indexed/indexed_query.rb +56 -0
  30. data/lib/factbase/indexed/indexed_term.rb +60 -0
  31. data/lib/factbase/light.rb +7 -6
  32. data/lib/factbase/logged.rb +70 -35
  33. data/lib/factbase/query.rb +29 -34
  34. data/lib/factbase/rules.rb +15 -14
  35. data/lib/factbase/sync/sync_factbase.rb +57 -0
  36. data/lib/factbase/sync/sync_query.rb +61 -0
  37. data/lib/factbase/syntax.rb +16 -26
  38. data/lib/factbase/tallied.rb +10 -9
  39. data/lib/factbase/taped.rb +8 -0
  40. data/lib/factbase/tee.rb +2 -0
  41. data/lib/factbase/term.rb +45 -17
  42. data/lib/factbase/terms/aggregates.rb +17 -15
  43. data/lib/factbase/terms/aliases.rb +4 -4
  44. data/lib/factbase/terms/casting.rb +8 -8
  45. data/lib/factbase/terms/debug.rb +2 -2
  46. data/lib/factbase/terms/defn.rb +3 -3
  47. data/lib/factbase/terms/logical.rb +53 -14
  48. data/lib/factbase/terms/math.rb +26 -26
  49. data/lib/factbase/terms/meta.rb +14 -14
  50. data/lib/factbase/terms/ordering.rb +4 -4
  51. data/lib/factbase/terms/strings.rb +8 -8
  52. data/lib/factbase/terms/system.rb +3 -3
  53. data/lib/factbase.rb +67 -55
  54. data/test/factbase/cached/test_cached_factbase.rb +22 -0
  55. data/test/factbase/cached/test_cached_query.rb +79 -0
  56. data/test/factbase/indexed/test_indexed_query.rb +175 -0
  57. data/test/factbase/sync/test_sync_query.rb +30 -0
  58. data/test/factbase/terms/test_aggregates.rb +5 -5
  59. data/test/factbase/terms/test_aliases.rb +7 -7
  60. data/test/factbase/terms/test_casting.rb +8 -8
  61. data/test/factbase/terms/test_debug.rb +6 -6
  62. data/test/factbase/terms/test_defn.rb +14 -14
  63. data/test/factbase/terms/test_logical.rb +17 -19
  64. data/test/factbase/terms/test_math.rb +63 -61
  65. data/test/factbase/terms/test_meta.rb +36 -36
  66. data/test/factbase/terms/test_ordering.rb +9 -9
  67. data/test/factbase/terms/test_strings.rb +10 -10
  68. data/test/factbase/terms/test_system.rb +6 -6
  69. data/test/factbase/test_accum.rb +5 -5
  70. data/test/factbase/test_fact.rb +12 -12
  71. data/test/factbase/test_logged.rb +7 -0
  72. data/test/factbase/test_query.rb +99 -37
  73. data/test/factbase/test_rules.rb +1 -1
  74. data/test/factbase/test_syntax.rb +24 -11
  75. data/test/factbase/test_tee.rb +8 -8
  76. data/test/factbase/test_term.rb +39 -30
  77. data/test/test__helper.rb +2 -2
  78. data/test/test_factbase.rb +6 -0
  79. metadata +29 -4
  80. data/lib/factbase/query_once.rb +0 -54
  81. data/lib/factbase/term_once.rb +0 -67
@@ -11,49 +11,79 @@ require_relative '../../factbase'
11
11
  # Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
12
12
  # License:: MIT
13
13
  module Factbase::Term::Logical
14
- def always(_fact, _maps)
14
+ # Always returns true, regardless of the fact
15
+ # @param [Factbase::Fact] _fact The fact (unused)
16
+ # @param [Array<Factbase::Fact>] _maps All maps available (unused)
17
+ # @return [Boolean] Always returns true
18
+ def always(_fact, _maps, _fb)
15
19
  assert_args(0)
16
20
  true
17
21
  end
18
22
 
19
- def never(_fact, _maps)
23
+ # Always returns false, regardless of the fact
24
+ # @param [Factbase::Fact] _fact The fact (unused)
25
+ # @param [Array<Factbase::Fact>] _maps All maps available (unused)
26
+ # @return [Boolean] Always returns false
27
+ def never(_fact, _maps, _fb)
20
28
  assert_args(0)
21
29
  false
22
30
  end
23
31
 
24
- def not(fact, maps)
32
+ # Logical negation (NOT) of an operand
33
+ # @param [Factbase::Fact] fact The fact
34
+ # @param [Array<Factbase::Fact>] maps All maps available
35
+ # @return [Boolean] Negated boolean result of the operand
36
+ def not(fact, maps, fb)
25
37
  assert_args(1)
26
- !_only_bool(the_values(0, fact, maps), 0)
38
+ !_only_bool(_values(0, fact, maps, fb), 0)
27
39
  end
28
40
 
29
- def or(fact, maps)
41
+ # Logical OR of multiple operands
42
+ # @param [Factbase::Fact] fact The fact
43
+ # @param [Array<Factbase::Fact>] maps All maps available
44
+ # @return [Boolean] True if any operand evaluates to true, false otherwise
45
+ def or(fact, maps, fb)
30
46
  (0..@operands.size - 1).each do |i|
31
- return true if _only_bool(the_values(i, fact, maps), i)
47
+ return true if _only_bool(_values(i, fact, maps, fb), i)
32
48
  end
33
49
  false
34
50
  end
35
51
 
36
- def and(fact, maps)
52
+ # Logical AND of multiple operands
53
+ # @param [Factbase::Fact] fact The fact
54
+ # @param [Array<Factbase::Fact>] maps All maps available
55
+ # @return [Boolean] True if all operands evaluate to true, false otherwise
56
+ def and(fact, maps, fb)
37
57
  (0..@operands.size - 1).each do |i|
38
- return false unless _only_bool(the_values(i, fact, maps), i)
58
+ return false unless _only_bool(_values(i, fact, maps, fb), i)
39
59
  end
40
60
  true
41
61
  end
42
62
 
43
- def when(fact, maps)
63
+ # Logical implication (IF...THEN)
64
+ # @param [Factbase::Fact] fact The fact
65
+ # @param [Array<Factbase::Fact>] maps All maps available
66
+ # @return [Boolean] True if first operand is false OR both are true
67
+ def when(fact, maps, fb)
44
68
  assert_args(2)
45
69
  a = @operands[0]
46
70
  b = @operands[1]
47
- !a.evaluate(fact, maps) || (a.evaluate(fact, maps) && b.evaluate(fact, maps))
71
+ !a.evaluate(fact, maps, fb) || (a.evaluate(fact, maps, fb) && b.evaluate(fact, maps, fb))
48
72
  end
49
73
 
50
- def either(fact, maps)
74
+ # Returns the first non-nil value or the second value
75
+ # @param [Factbase::Fact] fact The fact
76
+ # @param [Array<Factbase::Fact>] maps All maps available
77
+ # @return [Object] First operand if not nil, otherwise second operand
78
+ def either(fact, maps, fb)
51
79
  assert_args(2)
52
- v = the_values(0, fact, maps)
80
+ v = _values(0, fact, maps, fb)
53
81
  return v unless v.nil?
54
- the_values(1, fact, maps)
82
+ _values(1, fact, maps, fb)
55
83
  end
56
84
 
85
+ # Simplifies AND or OR expressions by removing duplicates
86
+ # @return [Factbase::Term] Simplified term
57
87
  def and_or_simplify
58
88
  strs = []
59
89
  ops = []
@@ -65,17 +95,26 @@ module Factbase::Term::Logical
65
95
  ops << o
66
96
  end
67
97
  return ops[0] if ops.size == 1
68
- Factbase::Term.new(@fb, @op, ops)
98
+ self.class.new(@op, ops)
69
99
  end
70
100
 
101
+ # Simplifies AND expressions by removing duplicates
102
+ # @return [Factbase::Term] Simplified term
71
103
  def and_simplify
72
104
  and_or_simplify
73
105
  end
74
106
 
107
+ # Simplifies OR expressions by removing duplicates
108
+ # @return [Factbase::Term] Simplified term
75
109
  def or_simplify
76
110
  and_or_simplify
77
111
  end
78
112
 
113
+ # Helper method to ensure a value is boolean
114
+ # @param [Object] val The value to check
115
+ # @param [Integer] pos The position of the operand
116
+ # @return [Boolean] The boolean value
117
+ # @raise [RuntimeError] If value is not a boolean
79
118
  def _only_bool(val, pos)
80
119
  val = val[0] if val.respond_to?(:each)
81
120
  return false if val.nil?
@@ -11,54 +11,54 @@ require_relative '../../factbase'
11
11
  # Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
12
12
  # License:: MIT
13
13
  module Factbase::Term::Math
14
- def plus(fact, maps)
15
- arithmetic(:+, fact, maps)
14
+ def plus(fact, maps, fb)
15
+ _arithmetic(:+, fact, maps, fb)
16
16
  end
17
17
 
18
- def minus(fact, maps)
19
- arithmetic(:-, fact, maps)
18
+ def minus(fact, maps, fb)
19
+ _arithmetic(:-, fact, maps, fb)
20
20
  end
21
21
 
22
- def times(fact, maps)
23
- arithmetic(:*, fact, maps)
22
+ def times(fact, maps, fb)
23
+ _arithmetic(:*, fact, maps, fb)
24
24
  end
25
25
 
26
- def div(fact, maps)
27
- arithmetic(:/, fact, maps)
26
+ def div(fact, maps, fb)
27
+ _arithmetic(:/, fact, maps, fb)
28
28
  end
29
29
 
30
- def zero(fact, maps)
30
+ def zero(fact, maps, fb)
31
31
  assert_args(1)
32
- vv = the_values(0, fact, maps)
32
+ vv = _values(0, fact, maps, fb)
33
33
  return false if vv.nil?
34
34
  vv.any? { |v| (v.is_a?(Integer) || v.is_a?(Float)) && v.zero? }
35
35
  end
36
36
 
37
- def eq(fact, maps)
38
- cmp(:==, fact, maps)
37
+ def eq(fact, maps, fb)
38
+ _cmp(:==, fact, maps, fb)
39
39
  end
40
40
 
41
- def lt(fact, maps)
42
- cmp(:<, fact, maps)
41
+ def lt(fact, maps, fb)
42
+ _cmp(:<, fact, maps, fb)
43
43
  end
44
44
 
45
- def gt(fact, maps)
46
- cmp(:>, fact, maps)
45
+ def gt(fact, maps, fb)
46
+ _cmp(:>, fact, maps, fb)
47
47
  end
48
48
 
49
- def lte(fact, maps)
50
- cmp(:<=, fact, maps)
49
+ def lte(fact, maps, fb)
50
+ _cmp(:<=, fact, maps, fb)
51
51
  end
52
52
 
53
- def gte(fact, maps)
54
- cmp(:>=, fact, maps)
53
+ def gte(fact, maps, fb)
54
+ _cmp(:>=, fact, maps, fb)
55
55
  end
56
56
 
57
- def cmp(op, fact, maps)
57
+ def _cmp(op, fact, maps, fb)
58
58
  assert_args(2)
59
- lefts = the_values(0, fact, maps)
59
+ lefts = _values(0, fact, maps, fb)
60
60
  return false if lefts.nil?
61
- rights = the_values(1, fact, maps)
61
+ rights = _values(1, fact, maps, fb)
62
62
  return false if rights.nil?
63
63
  lefts.any? do |l|
64
64
  l = l.floor if l.is_a?(Time) && op == :==
@@ -69,12 +69,12 @@ module Factbase::Term::Math
69
69
  end
70
70
  end
71
71
 
72
- def arithmetic(op, fact, maps)
72
+ def _arithmetic(op, fact, maps, fb)
73
73
  assert_args(2)
74
- lefts = the_values(0, fact, maps)
74
+ lefts = _values(0, fact, maps, fb)
75
75
  return nil if lefts.nil?
76
76
  raise 'Too many values at first position, one expected' unless lefts.size == 1
77
- rights = the_values(1, fact, maps)
77
+ rights = _values(1, fact, maps, fb)
78
78
  return nil if rights.nil?
79
79
  raise 'Too many values at second position, one expected' unless rights.size == 1
80
80
  v = lefts[0]
@@ -11,46 +11,46 @@ require_relative '../../factbase'
11
11
  # Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
12
12
  # License:: MIT
13
13
  module Factbase::Term::Meta
14
- def exists(fact, _maps)
14
+ def exists(fact, _maps, _fb)
15
15
  assert_args(1)
16
- !by_symbol(0, fact).nil?
16
+ !_by_symbol(0, fact).nil?
17
17
  end
18
18
 
19
- def absent(fact, _maps)
19
+ def absent(fact, _maps, _fb)
20
20
  assert_args(1)
21
- by_symbol(0, fact).nil?
21
+ _by_symbol(0, fact).nil?
22
22
  end
23
23
 
24
- def size(fact, _maps)
24
+ def size(fact, _maps, _fb)
25
25
  assert_args(1)
26
- v = by_symbol(0, fact)
26
+ v = _by_symbol(0, fact)
27
27
  return 0 if v.nil?
28
28
  return 1 unless v.respond_to?(:to_a)
29
29
  v.size
30
30
  end
31
31
 
32
- def type(fact, _maps)
32
+ def type(fact, _maps, _fb)
33
33
  assert_args(1)
34
- v = by_symbol(0, fact)
34
+ v = _by_symbol(0, fact)
35
35
  return 'nil' if v.nil?
36
36
  v = v[0] if v.respond_to?(:each) && v.size == 1
37
37
  v.class.to_s
38
38
  end
39
39
 
40
- def nil(fact, maps)
40
+ def nil(fact, maps, fb)
41
41
  assert_args(1)
42
- the_values(0, fact, maps).nil?
42
+ _values(0, fact, maps, fb).nil?
43
43
  end
44
44
 
45
- def many(fact, maps)
45
+ def many(fact, maps, fb)
46
46
  assert_args(1)
47
- v = the_values(0, fact, maps)
47
+ v = _values(0, fact, maps, fb)
48
48
  !v.nil? && v.size > 1
49
49
  end
50
50
 
51
- def one(fact, maps)
51
+ def one(fact, maps, fb)
52
52
  assert_args(1)
53
- v = the_values(0, fact, maps)
53
+ v = _values(0, fact, maps, fb)
54
54
  !v.nil? && v.size == 1
55
55
  end
56
56
  end
@@ -11,18 +11,18 @@ require_relative '../../factbase'
11
11
  # Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
12
12
  # License:: MIT
13
13
  module Factbase::Term::Ordering
14
- def prev(fact, maps)
14
+ def prev(fact, maps, fb)
15
15
  assert_args(1)
16
16
  before = @prev
17
- v = the_values(0, fact, maps)
17
+ v = _values(0, fact, maps, fb)
18
18
  @prev = v
19
19
  before
20
20
  end
21
21
 
22
- def unique(fact, maps)
22
+ def unique(fact, maps, fb)
23
23
  @uniques = [] if @uniques.nil?
24
24
  assert_args(1)
25
- vv = the_values(0, fact, maps)
25
+ vv = _values(0, fact, maps, fb)
26
26
  return false if vv.nil?
27
27
  vv = [vv] unless vv.respond_to?(:to_a)
28
28
  vv.each do |v|
@@ -11,22 +11,22 @@ require_relative '../../factbase'
11
11
  # Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
12
12
  # License:: MIT
13
13
  module Factbase::Term::Strings
14
- def concat(fact, maps)
15
- (0..@operands.length - 1).map { |i| the_values(i, fact, maps)&.first }.join
14
+ def concat(fact, maps, fb)
15
+ (0..@operands.length - 1).map { |i| _values(i, fact, maps, fb)&.first }.join
16
16
  end
17
17
 
18
- def sprintf(fact, maps)
19
- fmt = the_values(0, fact, maps)[0]
20
- ops = (1..@operands.length - 1).map { |i| the_values(i, fact, maps)&.first }
18
+ def sprintf(fact, maps, fb)
19
+ fmt = _values(0, fact, maps, fb)[0]
20
+ ops = (1..@operands.length - 1).map { |i| _values(i, fact, maps, fb)&.first }
21
21
  format(*([fmt] + ops))
22
22
  end
23
23
 
24
- def matches(fact, maps)
24
+ def matches(fact, maps, fb)
25
25
  assert_args(2)
26
- str = the_values(0, fact, maps)
26
+ str = _values(0, fact, maps, fb)
27
27
  return false if str.nil?
28
28
  raise 'Exactly one string expected' unless str.size == 1
29
- re = the_values(1, fact, maps)
29
+ re = _values(1, fact, maps, fb)
30
30
  raise 'Regexp is nil' if re.nil?
31
31
  raise 'Exactly one regexp expected' unless re.size == 1
32
32
  str[0].to_s.match?(re[0])
@@ -11,9 +11,9 @@ require_relative '../../factbase'
11
11
  # Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
12
12
  # License:: MIT
13
13
  module Factbase::Term::System
14
- def env(fact, maps)
14
+ def env(fact, maps, fb)
15
15
  assert_args(2)
16
- n = the_values(0, fact, maps)[0]
17
- ENV.fetch(n.upcase) { the_values(1, fact, maps)[0] }
16
+ n = _values(0, fact, maps, fb)[0]
17
+ ENV.fetch(n.upcase) { _values(1, fact, maps, fb)[0] }
18
18
  end
19
19
  end
data/lib/factbase.rb CHANGED
@@ -50,6 +50,24 @@ require 'yaml'
50
50
  # fb2 = Factbase.new # it's empty
51
51
  # fb2.import(File.binread(file))
52
52
  #
53
+ # Here's how to use transactions to ensure data consistency:
54
+ #
55
+ # fb = Factbase.new
56
+ # # Successful transaction
57
+ # fb.txn do |fbt|
58
+ # f = fbt.insert
59
+ # f.name = 'John'
60
+ # f.age = 30
61
+ # # If any error occurs here, all changes will be rolled back
62
+ # end
63
+ # # Transaction with rollback
64
+ # fb.txn do |fbt|
65
+ # f = fbt.insert
66
+ # f.name = 'Jane'
67
+ # f.age = 25
68
+ # raise Factbase::Rollback # This will undo all changes in this transaction
69
+ # end
70
+ #
53
71
  # It's impossible to delete properties of a fact. It is however possible to
54
72
  # delete the entire fact, with the help of the +query()+ and then +delete!()+
55
73
  # methods.
@@ -57,26 +75,22 @@ require 'yaml'
57
75
  # It's important to use +binwrite+ and +binread+, because the content is
58
76
  # a chain of bytes, not a text.
59
77
  #
60
- # Objects of this class are thread-safe.
78
+ # It is NOT thread-safe!
61
79
  #
62
80
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
63
81
  # Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
64
82
  # License:: MIT
65
83
  class Factbase
66
84
  # Current version of the gem (changed by .rultor.yml on every release)
67
- VERSION = '0.7.5'
85
+ VERSION = '0.9.0'
68
86
 
69
87
  # An exception that may be thrown in a transaction, to roll it back.
70
88
  class Rollback < StandardError; end
71
89
 
72
- attr_reader :cache
73
-
74
90
  # Constructor.
75
91
  # @param [Array<Hash>] maps Array of facts to start with
76
- def initialize(maps = [], cache: {})
92
+ def initialize(maps = [])
77
93
  @maps = maps
78
- @mutex = Mutex.new
79
- @cache = cache
80
94
  end
81
95
 
82
96
  # Size, the total number of facts in the factbase.
@@ -95,12 +109,9 @@ class Factbase
95
109
  # @return [Factbase::Fact] The fact just inserted
96
110
  def insert
97
111
  map = {}
98
- @mutex.synchronize do
99
- @maps << map
100
- end
101
- @cache.clear
112
+ @maps << map
102
113
  require_relative 'factbase/fact'
103
- Factbase::Fact.new(self, @mutex, map)
114
+ Factbase::Fact.new(map)
104
115
  end
105
116
 
106
117
  # Create a query capable of iterating.
@@ -120,22 +131,27 @@ class Factbase
120
131
  # The full list of terms available in the query you can find in the
121
132
  # +README.md+ file of the repository.
122
133
  #
123
- # @param [String] query The query to use for selections
124
- # @param [Array<Hash>] maps Custom maps
125
- def query(query, maps = @maps)
134
+ # @param [String|Factbase::Term] term The query to use for selections
135
+ # @param [Array<Hash>|nil] maps The subset of maps (if provided)
136
+ def query(term, maps = nil)
137
+ maps ||= @maps
138
+ term = to_term(term) if term.is_a?(String)
126
139
  require_relative 'factbase/query'
127
- require_relative 'factbase/query_once'
128
- Factbase::QueryOnce.new(
129
- self,
130
- Factbase::Query.new(self, maps, @mutex, query),
131
- maps
132
- )
140
+ Factbase::Query.new(maps, term, self)
141
+ end
142
+
143
+ # Convert a query to a term.
144
+ # @param [String] query The query to convert
145
+ # @return [Factbase::Term] The term
146
+ def to_term(query)
147
+ require_relative 'factbase/syntax'
148
+ Factbase::Syntax.new(query).to_term
133
149
  end
134
150
 
135
151
  # Run an ACID transaction, which will either modify the factbase
136
152
  # or rollback in case of an error.
137
153
  #
138
- # If necessary to terminate a transaction and roolback all changes,
154
+ # If necessary to terminate a transaction and rollback all changes,
139
155
  # you should raise the +Factbase::Rollback+ exception:
140
156
  #
141
157
  # fb = Factbase.new
@@ -144,53 +160,49 @@ class Factbase
144
160
  # raise Factbase::Rollback
145
161
  # end
146
162
  #
147
- # A the end of this script, the factbase will be empty. No facts will
163
+ # At the end of this script, the factbase will be empty. No facts will be
148
164
  # inserted and all changes that happened in the block will be rolled back.
149
165
  #
150
166
  # @return [Factbase::Churn] How many facts have been changed (zero if rolled back)
151
167
  def txn
152
168
  pairs = {}
153
169
  before =
154
- @mutex.synchronize do
155
- @maps.map do |m|
156
- n = m.transform_values(&:dup)
157
- # rubocop:disable Lint/HashCompareByIdentity
158
- pairs[n.object_id] = m.object_id
159
- # rubocop:enable Lint/HashCompareByIdentity
160
- n
161
- end
170
+ @maps.map do |m|
171
+ n = m.transform_values(&:dup)
172
+ # rubocop:disable Lint/HashCompareByIdentity
173
+ pairs[n.object_id] = m.object_id
174
+ # rubocop:enable Lint/HashCompareByIdentity
175
+ n
162
176
  end
163
177
  require_relative 'factbase/taped'
164
178
  taped = Factbase::Taped.new(before)
165
179
  begin
166
180
  require_relative 'factbase/light'
167
- yield Factbase::Light.new(Factbase.new(taped, cache: @cache), @cache)
181
+ yield Factbase::Light.new(Factbase.new(taped))
168
182
  rescue Factbase::Rollback
169
183
  return 0
170
184
  end
171
185
  require_relative 'factbase/churn'
172
186
  churn = Factbase::Churn.new
173
- @mutex.synchronize do
174
- taped.inserted.each do |oid|
175
- b = taped.find_by_object_id(oid)
176
- next if b.nil?
177
- @maps << b
178
- churn.append(1, 0, 0)
179
- end
180
- garbage = []
181
- taped.added.each do |oid|
182
- b = taped.find_by_object_id(oid)
183
- next if b.nil?
184
- garbage << pairs[oid]
185
- @maps << b
186
- churn.append(0, 0, 1)
187
- end
188
- taped.deleted.each do |oid|
189
- garbage << pairs[oid]
190
- churn.append(0, 1, 0)
191
- end
192
- @maps.delete_if { |m| garbage.include?(m.object_id) }
187
+ taped.inserted.each do |oid|
188
+ b = taped.find_by_object_id(oid)
189
+ next if b.nil?
190
+ @maps << b
191
+ churn.append(1, 0, 0)
192
+ end
193
+ garbage = []
194
+ taped.added.each do |oid|
195
+ b = taped.find_by_object_id(oid)
196
+ next if b.nil?
197
+ garbage << pairs[oid]
198
+ @maps << b
199
+ churn.append(0, 0, 1)
200
+ end
201
+ taped.deleted.each do |oid|
202
+ garbage << pairs[oid]
203
+ churn.append(0, 1, 0)
193
204
  end
205
+ @maps.delete_if { |m| garbage.include?(m.object_id) }
194
206
  churn
195
207
  end
196
208
 
@@ -204,7 +216,7 @@ class Factbase
204
216
  #
205
217
  # The data is binary, it's not a text!
206
218
  #
207
- # @return [Bytes] The chain of bytes
219
+ # @return [String] Binary string containing serialized data
208
220
  def export
209
221
  Marshal.dump(@maps)
210
222
  end
@@ -217,9 +229,9 @@ class Factbase
217
229
  # fb.import(File.binread("foo.fb"))
218
230
  #
219
231
  # The facts that existed in the factbase before importing will remain there.
220
- # The facts from the incoming byte stream will added to them.
232
+ # The facts from the incoming byte stream will be added to them.
221
233
  #
222
- # @param [Bytes] bytes Byte array to import
234
+ # @param [String] bytes Binary string to import
223
235
  def import(bytes)
224
236
  raise 'Empty input, cannot load a factbase' if bytes.empty?
225
237
  @maps += Marshal.load(bytes)
@@ -0,0 +1,22 @@
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/cached/cached_factbase'
9
+
10
+ # Query test.
11
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
12
+ # Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
13
+ # License:: MIT
14
+ class TestCachedFactbase < Factbase::Test
15
+ def test_inserts_and_queries
16
+ fb = Factbase::CachedFactbase.new(Factbase.new)
17
+ f = fb.insert
18
+ f.foo = 1
19
+ f.bar = 'test'
20
+ assert_equal(1, fb.query('(and (eq foo 1) (eq bar "test"))').each.to_a.size)
21
+ end
22
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2024-2025 Yegor Bugayenko
4
+ # SPDX-License-Identifier: MIT
5
+
6
+ require_relative '../../test__helper'
7
+ require_relative '../../../lib/factbase'
8
+ require_relative '../../../lib/factbase/cached/cached_factbase'
9
+ require_relative '../../../lib/factbase/indexed/indexed_factbase'
10
+ require_relative '../../../lib/factbase/sync/sync_factbase'
11
+
12
+ # Query test.
13
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
14
+ # Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
15
+ # License:: MIT
16
+ class TestCachedQuery < Factbase::Test
17
+ def test_queries_many_times
18
+ fb = Factbase::CachedFactbase.new(Factbase.new)
19
+ total = 5
20
+ total.times { fb.insert }
21
+ total.times do
22
+ assert_equal(5, fb.query('(always)').each.to_a.size)
23
+ end
24
+ end
25
+
26
+ def test_negates_correctly
27
+ fb = Factbase::CachedFactbase.new(Factbase.new)
28
+ fb.insert.foo = 42
29
+ 3.times do
30
+ assert_equal(1, fb.query('(always)').each.to_a.size)
31
+ assert_equal(0, fb.query('(not (always))').each.to_a.size)
32
+ end
33
+ end
34
+
35
+ def test_aggregates_too
36
+ fb = Factbase::IndexedFactbase.new(Factbase::CachedFactbase.new(Factbase.new))
37
+ 10_000.times do |i|
38
+ f = fb.insert
39
+ f.foo = i
40
+ f.hello = 1
41
+ end
42
+ 3.times do
43
+ q = fb.query('(eq foo (agg (exists hello) (min foo)))')
44
+ assert_equal(1, q.each.to_a.size)
45
+ end
46
+ end
47
+
48
+ def test_joins_too
49
+ fb = Factbase::IndexedFactbase.new(Factbase::CachedFactbase.new(Factbase.new))
50
+ total = 10
51
+ total.times do |i|
52
+ f = fb.insert
53
+ f.foo = i
54
+ f.hello = 1
55
+ end
56
+ 3.times do
57
+ assert_equal(total, fb.query('(join "bar<=foo" (eq foo (agg (exists hello) (min foo))))').each.to_a.size)
58
+ end
59
+ end
60
+
61
+ def test_caches_while_being_decorated
62
+ fb = Factbase::SyncFactbase.new(Factbase::CachedFactbase.new(Factbase.new))
63
+ 10_000.times do |i|
64
+ f = fb.insert
65
+ f.foo = i
66
+ f.hello = 1
67
+ end
68
+ 3.times do
69
+ assert_equal(1, fb.query('(eq foo (agg (exists hello) (min foo)))').each.to_a.size)
70
+ end
71
+ end
72
+
73
+ def test_deletes_too
74
+ fb = Factbase::CachedFactbase.new(Factbase.new)
75
+ fb.insert.foo = 1
76
+ fb.query('(eq foo 1)').delete!
77
+ assert_equal(0, fb.query('(always)').each.to_a.size)
78
+ end
79
+ end