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.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -1
- data/Gemfile +1 -1
- data/Gemfile.lock +3 -3
- data/README.md +24 -27
- data/REUSE.toml +7 -2
- data/Rakefile +8 -1
- data/benchmark/bench_factbase.rb +1 -1
- data/fixtures/stories/agg.yml +17 -0
- data/fixtures/stories/always.yml +16 -0
- data/fixtures/stories/as.yml +16 -0
- data/fixtures/stories/count.yml +18 -0
- data/fixtures/stories/eq.yml +30 -0
- data/fixtures/stories/gt.yml +18 -0
- data/fixtures/stories/join.yml +19 -0
- data/fixtures/stories/max.yml +14 -0
- data/fixtures/stories/min.yml +14 -0
- data/fixtures/stories/nth.yml +14 -0
- data/fixtures/stories/or.yml +18 -0
- data/fixtures/stories/sprintf.yml +12 -0
- data/fixtures/stories/sum.yml +14 -0
- data/lib/factbase/cached/cached_fact.rb +28 -0
- data/lib/factbase/cached/cached_factbase.rb +64 -0
- data/lib/factbase/cached/cached_query.rb +61 -0
- data/lib/factbase/cached/cached_term.rb +25 -0
- data/lib/factbase/fact.rb +13 -13
- data/lib/factbase/indexed/indexed_fact.rb +28 -0
- data/lib/factbase/indexed/indexed_factbase.rb +64 -0
- data/lib/factbase/indexed/indexed_query.rb +56 -0
- data/lib/factbase/indexed/indexed_term.rb +60 -0
- data/lib/factbase/light.rb +7 -6
- data/lib/factbase/logged.rb +70 -35
- data/lib/factbase/query.rb +29 -34
- data/lib/factbase/rules.rb +15 -14
- data/lib/factbase/sync/sync_factbase.rb +57 -0
- data/lib/factbase/sync/sync_query.rb +61 -0
- data/lib/factbase/syntax.rb +16 -26
- data/lib/factbase/tallied.rb +10 -9
- data/lib/factbase/taped.rb +8 -0
- data/lib/factbase/tee.rb +2 -0
- data/lib/factbase/term.rb +45 -17
- data/lib/factbase/terms/aggregates.rb +17 -15
- data/lib/factbase/terms/aliases.rb +4 -4
- data/lib/factbase/terms/casting.rb +8 -8
- data/lib/factbase/terms/debug.rb +2 -2
- data/lib/factbase/terms/defn.rb +3 -3
- data/lib/factbase/terms/logical.rb +53 -14
- data/lib/factbase/terms/math.rb +26 -26
- data/lib/factbase/terms/meta.rb +14 -14
- data/lib/factbase/terms/ordering.rb +4 -4
- data/lib/factbase/terms/strings.rb +8 -8
- data/lib/factbase/terms/system.rb +3 -3
- data/lib/factbase.rb +67 -55
- data/test/factbase/cached/test_cached_factbase.rb +22 -0
- data/test/factbase/cached/test_cached_query.rb +79 -0
- data/test/factbase/indexed/test_indexed_query.rb +175 -0
- data/test/factbase/sync/test_sync_query.rb +30 -0
- data/test/factbase/terms/test_aggregates.rb +5 -5
- data/test/factbase/terms/test_aliases.rb +7 -7
- data/test/factbase/terms/test_casting.rb +8 -8
- data/test/factbase/terms/test_debug.rb +6 -6
- data/test/factbase/terms/test_defn.rb +14 -14
- data/test/factbase/terms/test_logical.rb +17 -19
- data/test/factbase/terms/test_math.rb +63 -61
- data/test/factbase/terms/test_meta.rb +36 -36
- data/test/factbase/terms/test_ordering.rb +9 -9
- data/test/factbase/terms/test_strings.rb +10 -10
- data/test/factbase/terms/test_system.rb +6 -6
- data/test/factbase/test_accum.rb +5 -5
- data/test/factbase/test_fact.rb +12 -12
- data/test/factbase/test_logged.rb +7 -0
- data/test/factbase/test_query.rb +99 -37
- data/test/factbase/test_rules.rb +1 -1
- data/test/factbase/test_syntax.rb +24 -11
- data/test/factbase/test_tee.rb +8 -8
- data/test/factbase/test_term.rb +39 -30
- data/test/test__helper.rb +2 -2
- data/test/test_factbase.rb +6 -0
- metadata +29 -4
- data/lib/factbase/query_once.rb +0 -54
- 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
|
-
|
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
|
-
|
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
|
-
|
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(
|
38
|
+
!_only_bool(_values(0, fact, maps, fb), 0)
|
27
39
|
end
|
28
40
|
|
29
|
-
|
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(
|
47
|
+
return true if _only_bool(_values(i, fact, maps, fb), i)
|
32
48
|
end
|
33
49
|
false
|
34
50
|
end
|
35
51
|
|
36
|
-
|
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(
|
58
|
+
return false unless _only_bool(_values(i, fact, maps, fb), i)
|
39
59
|
end
|
40
60
|
true
|
41
61
|
end
|
42
62
|
|
43
|
-
|
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
|
-
|
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 =
|
80
|
+
v = _values(0, fact, maps, fb)
|
53
81
|
return v unless v.nil?
|
54
|
-
|
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
|
-
|
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?
|
data/lib/factbase/terms/math.rb
CHANGED
@@ -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
|
-
|
14
|
+
def plus(fact, maps, fb)
|
15
|
+
_arithmetic(:+, fact, maps, fb)
|
16
16
|
end
|
17
17
|
|
18
|
-
def minus(fact, maps)
|
19
|
-
|
18
|
+
def minus(fact, maps, fb)
|
19
|
+
_arithmetic(:-, fact, maps, fb)
|
20
20
|
end
|
21
21
|
|
22
|
-
def times(fact, maps)
|
23
|
-
|
22
|
+
def times(fact, maps, fb)
|
23
|
+
_arithmetic(:*, fact, maps, fb)
|
24
24
|
end
|
25
25
|
|
26
|
-
def div(fact, maps)
|
27
|
-
|
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 =
|
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
|
-
|
37
|
+
def eq(fact, maps, fb)
|
38
|
+
_cmp(:==, fact, maps, fb)
|
39
39
|
end
|
40
40
|
|
41
|
-
def lt(fact, maps)
|
42
|
-
|
41
|
+
def lt(fact, maps, fb)
|
42
|
+
_cmp(:<, fact, maps, fb)
|
43
43
|
end
|
44
44
|
|
45
|
-
def gt(fact, maps)
|
46
|
-
|
45
|
+
def gt(fact, maps, fb)
|
46
|
+
_cmp(:>, fact, maps, fb)
|
47
47
|
end
|
48
48
|
|
49
|
-
def lte(fact, maps)
|
50
|
-
|
49
|
+
def lte(fact, maps, fb)
|
50
|
+
_cmp(:<=, fact, maps, fb)
|
51
51
|
end
|
52
52
|
|
53
|
-
def gte(fact, maps)
|
54
|
-
|
53
|
+
def gte(fact, maps, fb)
|
54
|
+
_cmp(:>=, fact, maps, fb)
|
55
55
|
end
|
56
56
|
|
57
|
-
def
|
57
|
+
def _cmp(op, fact, maps, fb)
|
58
58
|
assert_args(2)
|
59
|
-
lefts =
|
59
|
+
lefts = _values(0, fact, maps, fb)
|
60
60
|
return false if lefts.nil?
|
61
|
-
rights =
|
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
|
72
|
+
def _arithmetic(op, fact, maps, fb)
|
73
73
|
assert_args(2)
|
74
|
-
lefts =
|
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 =
|
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]
|
data/lib/factbase/terms/meta.rb
CHANGED
@@ -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
|
-
!
|
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
|
-
|
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 =
|
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 =
|
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
|
-
|
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 =
|
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 =
|
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 =
|
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 =
|
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|
|
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 =
|
20
|
-
ops = (1..@operands.length - 1).map { |i|
|
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 =
|
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 =
|
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 =
|
17
|
-
ENV.fetch(n.upcase) {
|
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
|
-
#
|
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.
|
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 = []
|
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
|
-
@
|
99
|
-
@maps << map
|
100
|
-
end
|
101
|
-
@cache.clear
|
112
|
+
@maps << map
|
102
113
|
require_relative 'factbase/fact'
|
103
|
-
Factbase::Fact.new(
|
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]
|
124
|
-
# @param [Array<Hash
|
125
|
-
def query(
|
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
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
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
|
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
|
-
#
|
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
|
-
@
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
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
|
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
|
-
|
174
|
-
taped.
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
taped.
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
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 [
|
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 [
|
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
|