factbase 0.8.0 → 0.9.1
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 +13 -13
- data/README.md +24 -24
- 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/inv.rb +18 -6
- data/lib/factbase/light.rb +7 -6
- data/lib/factbase/logged.rb +87 -62
- data/lib/factbase/query.rb +29 -34
- data/lib/factbase/rules.rb +16 -15
- data/lib/factbase/sync/sync_factbase.rb +57 -0
- data/lib/factbase/sync/sync_query.rb +61 -0
- data/lib/factbase/syntax.rb +12 -25
- data/lib/factbase/tallied.rb +11 -10
- 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 +110 -37
- data/test/factbase/test_rules.rb +1 -1
- data/test/factbase/test_syntax.rb +12 -12
- 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
data/lib/factbase/query.rb
CHANGED
@@ -4,9 +4,9 @@
|
|
4
4
|
# SPDX-License-Identifier: MIT
|
5
5
|
|
6
6
|
require_relative '../factbase'
|
7
|
-
require_relative 'syntax'
|
8
|
-
require_relative 'fact'
|
9
7
|
require_relative 'accum'
|
8
|
+
require_relative 'fact'
|
9
|
+
require_relative 'syntax'
|
10
10
|
require_relative 'tee'
|
11
11
|
|
12
12
|
# Query.
|
@@ -21,40 +21,37 @@ require_relative 'tee'
|
|
21
21
|
# License:: MIT
|
22
22
|
class Factbase::Query
|
23
23
|
# Constructor.
|
24
|
-
# @param [Factbase] fb Factbase
|
25
24
|
# @param [Array<Fact>] maps Array of facts to start with
|
26
|
-
# @param [
|
27
|
-
|
28
|
-
def initialize(fb, maps, mutex, query)
|
29
|
-
@fb = fb
|
25
|
+
# @param [String|Factbase::Term] term The query term
|
26
|
+
def initialize(maps, term, fb)
|
30
27
|
@maps = maps
|
31
|
-
@
|
32
|
-
@
|
28
|
+
@term = term.is_a?(String) ? Factbase::Syntax.new(term).to_term : term
|
29
|
+
@fb = fb
|
33
30
|
end
|
34
31
|
|
35
32
|
# Print it as a string.
|
36
33
|
# @return [String] The query as a string
|
37
34
|
def to_s
|
38
|
-
@
|
35
|
+
@term.to_s
|
39
36
|
end
|
40
37
|
|
41
38
|
# Iterate facts one by one.
|
39
|
+
# @param [Factbase] fb The factbase
|
42
40
|
# @param [Hash] params Optional params accessible in the query via the "$" symbol
|
43
41
|
# @yield [Fact] Facts one-by-one
|
44
42
|
# @return [Integer] Total number of facts yielded
|
45
|
-
def each(params = {})
|
46
|
-
return to_enum(__method__, params) unless block_given?
|
47
|
-
term = Factbase::Syntax.new(@fb, @query).to_term
|
43
|
+
def each(fb = @fb, params = {})
|
44
|
+
return to_enum(__method__, fb, params) unless block_given?
|
48
45
|
yielded = 0
|
49
|
-
|
46
|
+
params = params.transform_keys(&:to_s) if params.is_a?(Hash)
|
47
|
+
(@term.predict(@maps, params) || @maps).each do |m|
|
50
48
|
extras = {}
|
51
|
-
f = Factbase::Fact.new(
|
52
|
-
params = params.transform_keys(&:to_s) if params.is_a?(Hash)
|
49
|
+
f = Factbase::Fact.new(m)
|
53
50
|
f = Factbase::Tee.new(f, params)
|
54
51
|
a = Factbase::Accum.new(f, extras, false)
|
55
|
-
r = term.evaluate(a, @maps)
|
52
|
+
r = @term.evaluate(a, @maps, fb)
|
56
53
|
unless r.is_a?(TrueClass) || r.is_a?(FalseClass)
|
57
|
-
raise "Unexpected evaluation result of type #{r.class}, must be Boolean at #{@
|
54
|
+
raise "Unexpected evaluation result of type #{r.class}, must be Boolean at #{@term.inspect}"
|
58
55
|
end
|
59
56
|
next unless r
|
60
57
|
yield Factbase::Accum.new(f, extras, true)
|
@@ -64,32 +61,30 @@ class Factbase::Query
|
|
64
61
|
end
|
65
62
|
|
66
63
|
# Read a single value.
|
64
|
+
# @param [Factbase] fb The factbase
|
67
65
|
# @param [Hash] params Optional params accessible in the query via the "$" symbol
|
68
|
-
# @return The value evaluated
|
69
|
-
def one(params = {})
|
70
|
-
term = Factbase::Syntax.new(@fb, @query).to_term
|
66
|
+
# @return [String|Integer|Float|Time|Array|NilClass] The value evaluated
|
67
|
+
def one(fb = @fb, params = {})
|
71
68
|
params = params.transform_keys(&:to_s) if params.is_a?(Hash)
|
72
|
-
r = term.evaluate(Factbase::Tee.new(
|
69
|
+
r = @term.evaluate(Factbase::Tee.new(Factbase::Fact.new({}), params), @maps, fb)
|
73
70
|
unless %w[String Integer Float Time Array NilClass].include?(r.class.to_s)
|
74
|
-
raise "Incorrect type #{r.class} returned by #{@
|
71
|
+
raise "Incorrect type #{r.class} returned by #{@term.inspect}"
|
75
72
|
end
|
76
73
|
r
|
77
74
|
end
|
78
75
|
|
79
76
|
# Delete all facts that match the query.
|
77
|
+
# @param [Factbase] fb The factbase to delete from
|
80
78
|
# @return [Integer] Total number of facts deleted
|
81
|
-
def delete!
|
82
|
-
term = Factbase::Syntax.new(@fb, @query).to_term
|
79
|
+
def delete!(fb = @fb)
|
83
80
|
deleted = 0
|
84
|
-
@
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
false
|
92
|
-
end
|
81
|
+
@maps.delete_if do |m|
|
82
|
+
f = Factbase::Fact.new(m)
|
83
|
+
if @term.evaluate(f, @maps, fb)
|
84
|
+
deleted += 1
|
85
|
+
true
|
86
|
+
else
|
87
|
+
false
|
93
88
|
end
|
94
89
|
end
|
95
90
|
deleted
|
data/lib/factbase/rules.rb
CHANGED
@@ -37,11 +37,11 @@ class Factbase::Rules
|
|
37
37
|
end
|
38
38
|
|
39
39
|
def insert
|
40
|
-
Fact.new(@fb.insert, @check)
|
40
|
+
Fact.new(@fb.insert, @check, @fb)
|
41
41
|
end
|
42
42
|
|
43
|
-
def query(query)
|
44
|
-
Query.new(@fb.query(query), @check)
|
43
|
+
def query(query, maps = nil)
|
44
|
+
Query.new(@fb.query(query, maps), @check, @fb)
|
45
45
|
end
|
46
46
|
|
47
47
|
def txn
|
@@ -53,7 +53,7 @@ class Factbase::Rules
|
|
53
53
|
@check = before
|
54
54
|
fbt.query('(always)').each do |f|
|
55
55
|
next unless later.include?(f)
|
56
|
-
@check.it(f)
|
56
|
+
@check.it(f, @fb)
|
57
57
|
end
|
58
58
|
end
|
59
59
|
end
|
@@ -63,9 +63,10 @@ class Factbase::Rules
|
|
63
63
|
# This is an internal class, it is not supposed to be instantiated directly.
|
64
64
|
#
|
65
65
|
class Fact
|
66
|
-
def initialize(fact, check)
|
66
|
+
def initialize(fact, check, fb)
|
67
67
|
@fact = fact
|
68
68
|
@check = check
|
69
|
+
@fb = fb
|
69
70
|
end
|
70
71
|
|
71
72
|
def to_s
|
@@ -79,7 +80,7 @@ class Factbase::Rules
|
|
79
80
|
others do |*args|
|
80
81
|
r = @fact.method_missing(*args)
|
81
82
|
k = args[0].to_s
|
82
|
-
@check.it(@fact) if k.end_with?('=')
|
83
|
+
@check.it(@fact, @fb) if k.end_with?('=')
|
83
84
|
r
|
84
85
|
end
|
85
86
|
end
|
@@ -87,19 +88,19 @@ class Factbase::Rules
|
|
87
88
|
# Query decorator.
|
88
89
|
#
|
89
90
|
# This is an internal class, it is not supposed to be instantiated directly.
|
90
|
-
#
|
91
91
|
class Query
|
92
92
|
decoor(:query)
|
93
93
|
|
94
|
-
def initialize(query, check)
|
94
|
+
def initialize(query, check, fb)
|
95
95
|
@query = query
|
96
96
|
@check = check
|
97
|
+
@fb = fb
|
97
98
|
end
|
98
99
|
|
99
|
-
def each(params = {})
|
100
|
-
return to_enum(__method__, params) unless block_given?
|
101
|
-
@query.each do |f|
|
102
|
-
yield Fact.new(f, @check)
|
100
|
+
def each(fb = @fb, params = {})
|
101
|
+
return to_enum(__method__, fb, params) unless block_given?
|
102
|
+
@query.each(fb, params) do |f|
|
103
|
+
yield Fact.new(f, @check, fb)
|
103
104
|
end
|
104
105
|
end
|
105
106
|
end
|
@@ -112,8 +113,8 @@ class Factbase::Rules
|
|
112
113
|
@expr = expr
|
113
114
|
end
|
114
115
|
|
115
|
-
def it(fact)
|
116
|
-
return if Factbase::Syntax.new(
|
116
|
+
def it(fact, fb)
|
117
|
+
return if Factbase::Syntax.new(@expr).to_term.evaluate(fact, [], fb)
|
117
118
|
e = "#{@expr[0..32]}..." if @expr.length > 32
|
118
119
|
raise "The fact doesn't match the #{e.inspect} rule: #{fact}"
|
119
120
|
end
|
@@ -128,7 +129,7 @@ class Factbase::Rules
|
|
128
129
|
@facts = Set.new
|
129
130
|
end
|
130
131
|
|
131
|
-
def it(fact)
|
132
|
+
def it(fact, _fb)
|
132
133
|
a = fact[@uid]
|
133
134
|
return if a.nil?
|
134
135
|
@facts << a[0] unless @uid.nil?
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) 2024-2025 Yegor Bugayenko
|
4
|
+
# SPDX-License-Identifier: MIT
|
5
|
+
|
6
|
+
require 'decoor'
|
7
|
+
require_relative '../../factbase'
|
8
|
+
|
9
|
+
# A synchronous thread-safe factbase.
|
10
|
+
#
|
11
|
+
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
12
|
+
# Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
|
13
|
+
# License:: MIT
|
14
|
+
class Factbase::SyncFactbase
|
15
|
+
decoor(:origin)
|
16
|
+
|
17
|
+
# Constructor.
|
18
|
+
# @param [Factbase] origin Original factbase to decorate
|
19
|
+
# @param [Mutex] mutex Mutex to use for synchronization
|
20
|
+
def initialize(origin, mutex = Mutex.new)
|
21
|
+
@origin = origin
|
22
|
+
@mutex = mutex
|
23
|
+
end
|
24
|
+
|
25
|
+
# Insert a new fact and return it.
|
26
|
+
# @return [Factbase::Fact] The fact just inserted
|
27
|
+
def insert
|
28
|
+
@mutex.synchronize do
|
29
|
+
@origin.insert
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Convert a query to a term.
|
34
|
+
# @param [String] query The query to convert
|
35
|
+
# @return [Factbase::Term] The term
|
36
|
+
def to_term(query)
|
37
|
+
@origin.to_term(query)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Create a query capable of iterating.
|
41
|
+
# @param [String] term The query to use for selections
|
42
|
+
# @param [Array<Hash>] maps Possible maps to use
|
43
|
+
def query(term, maps = nil)
|
44
|
+
term = to_term(term) if term.is_a?(String)
|
45
|
+
require_relative 'sync_query'
|
46
|
+
Factbase::SyncQuery.new(@origin.query(term, maps), @mutex, self)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Run an ACID transaction.
|
50
|
+
# @return [Factbase::Churn] How many facts have been changed (zero if rolled back)
|
51
|
+
# @yield [Factbase] Block to execute in transaction
|
52
|
+
def txn
|
53
|
+
@origin.txn do |fbt|
|
54
|
+
yield Factbase::SyncFactbase.new(fbt, @mutex)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
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 '../../factbase'
|
7
|
+
|
8
|
+
# Synchronized thread-safe query.
|
9
|
+
#
|
10
|
+
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
11
|
+
# Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
|
12
|
+
# License:: MIT
|
13
|
+
class Factbase::SyncQuery
|
14
|
+
# Constructor.
|
15
|
+
# @param [Factbase::Query] origin Original query
|
16
|
+
# @param [Mutex] mutex The mutex
|
17
|
+
def initialize(origin, mutex, fb)
|
18
|
+
@origin = origin
|
19
|
+
@mutex = mutex
|
20
|
+
@fb = fb
|
21
|
+
end
|
22
|
+
|
23
|
+
# Iterate facts one by one.
|
24
|
+
# @param [Hash] params Optional params accessible in the query via the "$" symbol
|
25
|
+
# @yield [Fact] Facts one-by-one
|
26
|
+
# @return [Integer] Total number of facts yielded
|
27
|
+
def each(fb = @fb, params = {}, &)
|
28
|
+
return to_enum(__method__, fb, params) unless block_given?
|
29
|
+
try_lock do
|
30
|
+
@origin.each(fb, params, &)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Read a single value.
|
35
|
+
# @param [Factbase] fb The factbase
|
36
|
+
# @param [Hash] params Optional params accessible in the query via the "$" symbol
|
37
|
+
# @return [String|Integer|Float|Time|Array|NilClass] The value evaluated
|
38
|
+
def one(fb = @fb, params = {})
|
39
|
+
try_lock do
|
40
|
+
@origin.one(fb, params)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Delete all facts that match the query.
|
45
|
+
# @param [Factbase] fb The factbase
|
46
|
+
# @return [Integer] Total number of facts deleted
|
47
|
+
def delete!(fb = @fb)
|
48
|
+
try_lock do
|
49
|
+
@origin.delete!(fb)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def try_lock
|
56
|
+
locked = @mutex.try_lock
|
57
|
+
r = yield
|
58
|
+
@mutex.unlock if locked
|
59
|
+
r
|
60
|
+
end
|
61
|
+
end
|
data/lib/factbase/syntax.rb
CHANGED
@@ -8,22 +8,11 @@ require 'time'
|
|
8
8
|
require_relative '../factbase'
|
9
9
|
require_relative 'fact'
|
10
10
|
require_relative 'term'
|
11
|
-
require_relative 'term_once'
|
12
11
|
|
13
|
-
# Syntax.
|
12
|
+
# Syntax parser.
|
14
13
|
#
|
15
14
|
# This is an internal class, it is not supposed to be instantiated directly.
|
16
15
|
#
|
17
|
-
# However, you can use it directly, if you need a parser of our syntax. You can
|
18
|
-
# create your own "Term" class and let this parser make instances of it for
|
19
|
-
# every term it meets in the query:
|
20
|
-
#
|
21
|
-
# require 'factbase/syntax'
|
22
|
-
# t = Factbase::Syntax.new(Factbase.new, '(hello world)', MyTerm).to_term
|
23
|
-
#
|
24
|
-
# The +MyTerm+ class should have a constructor with two arguments:
|
25
|
-
# the operator and the list of operands (Array).
|
26
|
-
#
|
27
16
|
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
28
17
|
# Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
|
29
18
|
# License:: MIT
|
@@ -33,19 +22,13 @@ class Factbase::Syntax
|
|
33
22
|
|
34
23
|
# Ctor.
|
35
24
|
#
|
36
|
-
# The class provided as the +term+ argument must have a
|
37
|
-
#
|
25
|
+
# The class provided as the +term+ argument must have a constructor that accepts
|
26
|
+
# an operator, operands array, and a keyword argument fb. Also, it must be
|
38
27
|
# a child of +Factbase::Term+.
|
39
28
|
#
|
40
|
-
# @param [Factbase] fb Factbase
|
41
29
|
# @param [String] query The query, for example "(eq id 42)"
|
42
|
-
|
43
|
-
def initialize(fb, query, term: Factbase::Term)
|
44
|
-
@fb = fb
|
30
|
+
def initialize(query)
|
45
31
|
@query = query
|
46
|
-
raise "Term must be a Class, while #{term.class.name} provided" unless term.is_a?(Class)
|
47
|
-
raise "The 'term' must be a child of Factbase::Term, while #{term.name} provided" unless term <= Factbase::Term
|
48
|
-
@term = term
|
49
32
|
end
|
50
33
|
|
51
34
|
# Convert it to a term.
|
@@ -74,7 +57,7 @@ class Factbase::Syntax
|
|
74
57
|
raise "Too many terms (#{@ast[1]} != #{@tokens.size})" if @ast[1] != @tokens.size
|
75
58
|
t = @ast[0]
|
76
59
|
raise 'No terms found in the AST' if t.nil?
|
77
|
-
raise "#{t.class.name} is not an instance of
|
60
|
+
raise "#{t.class.name} is not an instance of Term" unless t.is_a?(Factbase::Term)
|
78
61
|
t
|
79
62
|
end
|
80
63
|
|
@@ -82,9 +65,13 @@ class Factbase::Syntax
|
|
82
65
|
# token at the position is not a literal (like 42 or "Hello") but a term,
|
83
66
|
# the function recursively calls itself.
|
84
67
|
#
|
85
|
-
# The function returns
|
68
|
+
# The function returns a two-element array, where the first element
|
86
69
|
# is the term/literal and the second one is the position where the
|
87
70
|
# scanning should continue.
|
71
|
+
#
|
72
|
+
# @param [Array] tokens Array of tokens
|
73
|
+
# @param [Integer] at Position to start parsing from
|
74
|
+
# @return [Array<Factbase::Term,Integer>] The term detected and ending position
|
88
75
|
def to_ast(tokens, at)
|
89
76
|
raise "Closing too soon at ##{at}" if tokens[at] == :close
|
90
77
|
return [tokens[at], at + 1] unless tokens[at] == :open
|
@@ -103,12 +90,12 @@ class Factbase::Syntax
|
|
103
90
|
operands << operand
|
104
91
|
break if tokens[at] == :close
|
105
92
|
end
|
106
|
-
t =
|
107
|
-
t = Factbase::TermOnce.new(t, @fb.cache) if t.instance_of?(Factbase::Term)
|
93
|
+
t = Factbase::Term.new(op, operands)
|
108
94
|
[t, at + 1]
|
109
95
|
end
|
110
96
|
|
111
97
|
# Turns a query into an array of tokens.
|
98
|
+
# @return [Array] Array of tokens
|
112
99
|
def to_tokens
|
113
100
|
list = []
|
114
101
|
acc = ''
|
data/lib/factbase/tallied.rb
CHANGED
@@ -31,8 +31,8 @@ class Factbase::Tallied
|
|
31
31
|
f
|
32
32
|
end
|
33
33
|
|
34
|
-
def query(query)
|
35
|
-
Query.new(@fb.query(query), @churn)
|
34
|
+
def query(query, maps = nil)
|
35
|
+
Query.new(@fb.query(query, maps), @churn, @fb)
|
36
36
|
end
|
37
37
|
|
38
38
|
def txn
|
@@ -69,24 +69,25 @@ class Factbase::Tallied
|
|
69
69
|
#
|
70
70
|
# This is an internal class, it is not supposed to be instantiated directly.
|
71
71
|
class Query
|
72
|
-
def initialize(query, churn)
|
72
|
+
def initialize(query, churn, fb)
|
73
73
|
@query = query
|
74
74
|
@churn = churn
|
75
|
+
@fb = fb
|
75
76
|
end
|
76
77
|
|
77
|
-
def one(params = {})
|
78
|
-
@query.one(params)
|
78
|
+
def one(fb = @fb, params = {})
|
79
|
+
@query.one(fb, params)
|
79
80
|
end
|
80
81
|
|
81
|
-
def each(params = {}, &)
|
82
|
-
return to_enum(__method__, params) unless block_given?
|
83
|
-
@query.each(params) do |f|
|
82
|
+
def each(fb = @fb, params = {}, &)
|
83
|
+
return to_enum(__method__, fb, params) unless block_given?
|
84
|
+
@query.each(fb, params) do |f|
|
84
85
|
yield Fact.new(f, @churn)
|
85
86
|
end
|
86
87
|
end
|
87
88
|
|
88
|
-
def delete!
|
89
|
-
c = @query.delete!
|
89
|
+
def delete!(fb = @fb)
|
90
|
+
c = @query.delete!(fb)
|
90
91
|
@churn.append(0, c, 0)
|
91
92
|
c
|
92
93
|
end
|
data/lib/factbase/taped.rb
CHANGED
data/lib/factbase/tee.rb
CHANGED
@@ -16,7 +16,9 @@ class Factbase::Tee
|
|
16
16
|
# @param [Factbase::Fact] fact Primary fact to use for reading
|
17
17
|
# @param [Factbase::Fact] upper Fact to access with a "$" prefix
|
18
18
|
def initialize(fact, upper)
|
19
|
+
raise 'Fact is nil' if fact.nil?
|
19
20
|
@fact = fact
|
21
|
+
raise 'Upper is nil' if upper.nil?
|
20
22
|
@upper = upper
|
21
23
|
end
|
22
24
|
|
data/lib/factbase/term.rb
CHANGED
@@ -17,8 +17,8 @@ require_relative 'tee'
|
|
17
17
|
#
|
18
18
|
# require 'factbase/fact'
|
19
19
|
# require 'factbase/term'
|
20
|
-
# f = Factbase::Fact.new(
|
21
|
-
# t = Factbase::Term.new(
|
20
|
+
# f = Factbase::Fact.new({ 'foo' => [42, 256, 'Hello, world!'] })
|
21
|
+
# t = Factbase::Term.new(:lt, [:foo, 50])
|
22
22
|
# assert(t.evaluate(f))
|
23
23
|
#
|
24
24
|
# The design of this class may look ugly, since it has a large number of
|
@@ -30,11 +30,19 @@ require_relative 'tee'
|
|
30
30
|
# Moreover, it looks like the number of possible term types is rather limited
|
31
31
|
# and currently we implement most of them.
|
32
32
|
#
|
33
|
+
# It is NOT thread-safe!
|
34
|
+
#
|
33
35
|
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
34
36
|
# Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
|
35
37
|
# License:: MIT
|
36
38
|
class Factbase::Term
|
37
|
-
|
39
|
+
# The operator of this term
|
40
|
+
# @return [Symbol] The operator
|
41
|
+
attr_reader :op
|
42
|
+
|
43
|
+
# The operands of this term
|
44
|
+
# @return [Array] The operands
|
45
|
+
attr_reader :operands
|
38
46
|
|
39
47
|
require_relative 'terms/math'
|
40
48
|
include Factbase::Term::Math
|
@@ -70,21 +78,41 @@ class Factbase::Term
|
|
70
78
|
include Factbase::Term::Debug
|
71
79
|
|
72
80
|
# Ctor.
|
73
|
-
# @param [Factbase] fb Factbase
|
74
81
|
# @param [Symbol] operator Operator
|
75
82
|
# @param [Array] operands Operands
|
76
|
-
def initialize(
|
77
|
-
@fb = fb
|
83
|
+
def initialize(operator, operands)
|
78
84
|
@op = operator
|
79
85
|
@operands = operands
|
80
86
|
end
|
81
87
|
|
88
|
+
def redress!(type, **args)
|
89
|
+
extend type
|
90
|
+
args.each { |k, v| send(:instance_variable_set, :"@#{k}", v) }
|
91
|
+
@operands.map do |op|
|
92
|
+
if op.is_a?(Factbase::Term)
|
93
|
+
op.redress!(type, **args)
|
94
|
+
else
|
95
|
+
op
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Try to predict which facts from the provided list
|
101
|
+
# should be evaluated. If no prediction can be made,
|
102
|
+
# the same list is returned.
|
103
|
+
# @param [Array<Hash>] maps Records to iterate, maybe
|
104
|
+
# @return [Array<Hash>] Records to iterate
|
105
|
+
def predict(maps, _params)
|
106
|
+
maps
|
107
|
+
end
|
108
|
+
|
82
109
|
# Does it match the fact?
|
83
110
|
# @param [Factbase::Fact] fact The fact
|
84
111
|
# @param [Array<Factbase::Fact>] maps All maps available
|
85
|
-
# @
|
86
|
-
|
87
|
-
|
112
|
+
# @param [Factbase] fb Factbase to use for sub-queries
|
113
|
+
# @return [Boolean] TRUE if matches
|
114
|
+
def evaluate(fact, maps, fb)
|
115
|
+
send(@op, fact, maps, fb)
|
88
116
|
rescue NoMethodError => e
|
89
117
|
raise "Probably the term '#{@op}' is not defined at #{self}:\n#{Backtrace.new(e)}"
|
90
118
|
rescue StandardError => e
|
@@ -146,26 +174,26 @@ class Factbase::Term
|
|
146
174
|
"(#{items.join(' ')})"
|
147
175
|
end
|
148
176
|
|
149
|
-
|
150
|
-
|
151
|
-
def at(fact, maps)
|
177
|
+
def at(fact, maps, fb)
|
152
178
|
assert_args(2)
|
153
|
-
i =
|
179
|
+
i = _values(0, fact, maps, fb)
|
154
180
|
raise "Too many values (#{i.size}) at first position, one expected" unless i.size == 1
|
155
181
|
i = i[0]
|
156
182
|
return nil if i.nil?
|
157
|
-
v =
|
183
|
+
v = _values(1, fact, maps, fb)
|
158
184
|
return nil if v.nil?
|
159
185
|
v[i]
|
160
186
|
end
|
161
187
|
|
188
|
+
private
|
189
|
+
|
162
190
|
def assert_args(num)
|
163
191
|
c = @operands.size
|
164
192
|
raise "Too many (#{c}) operands for '#{@op}' (#{num} expected)" if c > num
|
165
193
|
raise "Too few (#{c}) operands for '#{@op}' (#{num} expected)" if c < num
|
166
194
|
end
|
167
195
|
|
168
|
-
def
|
196
|
+
def _by_symbol(pos, fact)
|
169
197
|
o = @operands[pos]
|
170
198
|
raise "A symbol expected at ##{pos}, but '#{o}' (#{o.class}) provided" unless o.is_a?(Symbol)
|
171
199
|
k = o.to_s
|
@@ -173,9 +201,9 @@ class Factbase::Term
|
|
173
201
|
end
|
174
202
|
|
175
203
|
# @return [Array|nil] Either array of values or NIL
|
176
|
-
def
|
204
|
+
def _values(pos, fact, maps, fb)
|
177
205
|
v = @operands[pos]
|
178
|
-
v = v.evaluate(fact, maps) if v.is_a?(Factbase::Term)
|
206
|
+
v = v.evaluate(fact, maps, fb) if v.is_a?(Factbase::Term)
|
179
207
|
v = fact[v.to_s] if v.is_a?(Symbol)
|
180
208
|
return v if v.nil?
|
181
209
|
unless v.is_a?(Array)
|
@@ -11,30 +11,32 @@ require_relative '../../factbase'
|
|
11
11
|
# Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
|
12
12
|
# License:: MIT
|
13
13
|
module Factbase::Term::Aggregates
|
14
|
-
def min(_fact, maps)
|
14
|
+
def min(_fact, maps, _fb)
|
15
15
|
assert_args(1)
|
16
|
-
|
16
|
+
_best(maps) { |v, b| v < b }
|
17
17
|
end
|
18
18
|
|
19
|
-
def max(_fact, maps)
|
19
|
+
def max(_fact, maps, _fb)
|
20
20
|
assert_args(1)
|
21
|
-
|
21
|
+
_best(maps) { |v, b| v > b }
|
22
22
|
end
|
23
23
|
|
24
|
-
def count(_fact, maps)
|
24
|
+
def count(_fact, maps, _fb)
|
25
25
|
maps.size
|
26
26
|
end
|
27
27
|
|
28
|
-
def nth(_fact, maps)
|
28
|
+
def nth(_fact, maps, _fb)
|
29
29
|
assert_args(2)
|
30
30
|
pos = @operands[0]
|
31
31
|
raise "An integer expected, but #{pos} provided" unless pos.is_a?(Integer)
|
32
32
|
k = @operands[1]
|
33
33
|
raise "A symbol expected, but #{k} provided" unless k.is_a?(Symbol)
|
34
|
-
maps[pos]
|
34
|
+
m = maps[pos]
|
35
|
+
return nil if m.nil?
|
36
|
+
m[k.to_s]
|
35
37
|
end
|
36
38
|
|
37
|
-
def first(_fact, maps)
|
39
|
+
def first(_fact, maps, _fb)
|
38
40
|
assert_args(1)
|
39
41
|
k = @operands[0]
|
40
42
|
raise "A symbol expected, but #{k} provided" unless k.is_a?(Symbol)
|
@@ -43,7 +45,7 @@ module Factbase::Term::Aggregates
|
|
43
45
|
first[k.to_s]
|
44
46
|
end
|
45
47
|
|
46
|
-
def sum(_fact, maps)
|
48
|
+
def sum(_fact, maps, _fb)
|
47
49
|
k = @operands[0]
|
48
50
|
raise "A symbol expected, but '#{k}' provided" unless k.is_a?(Symbol)
|
49
51
|
sum = 0
|
@@ -58,24 +60,24 @@ module Factbase::Term::Aggregates
|
|
58
60
|
sum
|
59
61
|
end
|
60
62
|
|
61
|
-
def agg(fact, maps)
|
63
|
+
def agg(fact, maps, fb)
|
62
64
|
assert_args(2)
|
63
65
|
selector = @operands[0]
|
64
66
|
raise "A term expected, but '#{selector}' provided" unless selector.is_a?(Factbase::Term)
|
65
67
|
term = @operands[1]
|
66
68
|
raise "A term expected, but '#{term}' provided" unless term.is_a?(Factbase::Term)
|
67
|
-
subset =
|
68
|
-
term.evaluate(nil, subset)
|
69
|
+
subset = fb.query(selector, maps).each(fb, fact).to_a
|
70
|
+
term.evaluate(nil, subset, fb)
|
69
71
|
end
|
70
72
|
|
71
|
-
def empty(fact, maps)
|
73
|
+
def empty(fact, maps, fb)
|
72
74
|
assert_args(1)
|
73
75
|
term = @operands[0]
|
74
76
|
raise "A term expected, but '#{term}' provided" unless term.is_a?(Factbase::Term)
|
75
|
-
|
77
|
+
fb.query(term, maps).each(fb, fact).to_a.empty?
|
76
78
|
end
|
77
79
|
|
78
|
-
def
|
80
|
+
def _best(maps)
|
79
81
|
k = @operands[0]
|
80
82
|
raise "A symbol expected, but #{k} provided" unless k.is_a?(Symbol)
|
81
83
|
best = nil
|