factbase 0.8.0 → 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 -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/light.rb +7 -6
- data/lib/factbase/logged.rb +67 -44
- 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 +12 -25
- 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 +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
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) 2024-2025 Yegor Bugayenko
|
4
|
+
# SPDX-License-Identifier: MIT
|
5
|
+
|
6
|
+
require 'others'
|
7
|
+
require_relative '../../factbase'
|
8
|
+
|
9
|
+
# A single fact in a factbase, which is sentitive to changes.
|
10
|
+
#
|
11
|
+
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
12
|
+
# Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
|
13
|
+
# License:: MIT
|
14
|
+
class Factbase::IndexedFact
|
15
|
+
# Ctor.
|
16
|
+
# @param [Factbase::Fact] origin The original fact
|
17
|
+
# @param [Hash] idx The index
|
18
|
+
def initialize(origin, idx)
|
19
|
+
@origin = origin
|
20
|
+
@idx = idx
|
21
|
+
end
|
22
|
+
|
23
|
+
# When a method is missing, this method is called.
|
24
|
+
others do |*args|
|
25
|
+
@idx.clear if args[0].to_s.end_with?('=')
|
26
|
+
@origin.send(*args)
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,64 @@
|
|
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
|
+
require_relative '../../factbase/syntax'
|
9
|
+
require_relative 'indexed_fact'
|
10
|
+
require_relative 'indexed_query'
|
11
|
+
require_relative 'indexed_term'
|
12
|
+
|
13
|
+
# A factbase with an index.
|
14
|
+
#
|
15
|
+
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
16
|
+
# Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
|
17
|
+
# License:: MIT
|
18
|
+
class Factbase::IndexedFactbase
|
19
|
+
decoor(:origin)
|
20
|
+
|
21
|
+
# Constructor.
|
22
|
+
# @param [Factbase] origin Original factbase to decorate
|
23
|
+
# @param [Hash] idx Index to use
|
24
|
+
def initialize(origin, idx = {})
|
25
|
+
raise 'Wront type of original' unless origin.respond_to?(:query)
|
26
|
+
@origin = origin
|
27
|
+
raise 'Wront type of index' unless idx.is_a?(Hash)
|
28
|
+
@idx = idx
|
29
|
+
end
|
30
|
+
|
31
|
+
# Insert a new fact and return it.
|
32
|
+
# @return [Factbase::Fact] The fact just inserted
|
33
|
+
def insert
|
34
|
+
@idx.clear
|
35
|
+
Factbase::IndexedFact.new(@origin.insert, @idx)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Convert a query to a term.
|
39
|
+
# @param [String] query The query to convert
|
40
|
+
# @return [Factbase::Term] The term
|
41
|
+
def to_term(query)
|
42
|
+
t = @origin.to_term(query)
|
43
|
+
t.redress!(Factbase::IndexedTerm, idx: @idx)
|
44
|
+
t
|
45
|
+
end
|
46
|
+
|
47
|
+
# Create a query capable of iterating.
|
48
|
+
# @param [String] term The term to use
|
49
|
+
# @param [Array<Hash>] maps Possible maps to use
|
50
|
+
def query(term, maps = nil)
|
51
|
+
term = to_term(term) if term.is_a?(String)
|
52
|
+
q = @origin.query(term, maps)
|
53
|
+
q = Factbase::IndexedQuery.new(q, @idx, self) if term.abstract?
|
54
|
+
q
|
55
|
+
end
|
56
|
+
|
57
|
+
# Run an ACID transaction.
|
58
|
+
# @return [Factbase::Churn] How many facts have been changed (zero if rolled back)
|
59
|
+
def txn
|
60
|
+
@origin.txn do |fbt|
|
61
|
+
yield Factbase::IndexedFactbase.new(fbt, @idx)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,56 @@
|
|
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
|
+
require_relative 'indexed_fact'
|
8
|
+
|
9
|
+
# Query with an index, a decorator of another query.
|
10
|
+
#
|
11
|
+
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
12
|
+
# Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
|
13
|
+
# License:: MIT
|
14
|
+
class Factbase::IndexedQuery
|
15
|
+
# Constructor.
|
16
|
+
# @param [Factbase::Query] origin Original query
|
17
|
+
# @param [Hash] idx The index
|
18
|
+
def initialize(origin, idx, fb)
|
19
|
+
@origin = origin
|
20
|
+
@idx = idx
|
21
|
+
@fb = fb
|
22
|
+
end
|
23
|
+
|
24
|
+
# Print it as a string.
|
25
|
+
# @return [String] The query as a string
|
26
|
+
def to_s
|
27
|
+
@origin.to_s
|
28
|
+
end
|
29
|
+
|
30
|
+
# Iterate facts one by one.
|
31
|
+
# @param [Hash] params Optional params accessible in the query via the "$" symbol
|
32
|
+
# @yield [Fact] Facts one-by-one
|
33
|
+
# @return [Integer] Total number of facts yielded
|
34
|
+
def each(fb = @fb, params = {})
|
35
|
+
return to_enum(__method__, fb, params) unless block_given?
|
36
|
+
@origin.each(fb, params) do |f|
|
37
|
+
yield Factbase::IndexedFact.new(f, @idx)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Read a single value.
|
42
|
+
# @param [Factbase] fb The factbase
|
43
|
+
# @param [Hash] params Optional params accessible in the query via the "$" symbol
|
44
|
+
# @return [String|Integer|Float|Time|Array|NilClass] The value evaluated
|
45
|
+
def one(fb = @fb, params = nil)
|
46
|
+
@origin.one(fb, params)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Delete all facts that match the query.
|
50
|
+
# @param [Factbase] fb The factbase
|
51
|
+
# @return [Integer] Total number of facts deleted
|
52
|
+
def delete!(fb = @fb)
|
53
|
+
@idx.clear
|
54
|
+
@origin.delete!(fb)
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,60 @@
|
|
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
|
+
# Term with an index.
|
9
|
+
#
|
10
|
+
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
11
|
+
# Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
|
12
|
+
# License:: MIT
|
13
|
+
module Factbase::IndexedTerm
|
14
|
+
def predict(maps, params)
|
15
|
+
case @op
|
16
|
+
when :eq
|
17
|
+
if @operands[0].is_a?(Symbol) && _scalar?(@operands[1])
|
18
|
+
key = [maps.object_id, @operands[0], @op]
|
19
|
+
if @idx[key].nil?
|
20
|
+
@idx[key] = {}
|
21
|
+
maps.to_a.each do |m|
|
22
|
+
m[@operands[0].to_s]&.each do |v|
|
23
|
+
@idx[key][v] = [] if @idx[key][v].nil?
|
24
|
+
@idx[key][v].append(m)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
vv =
|
29
|
+
if @operands[1].is_a?(Symbol)
|
30
|
+
sym = @operands[1].to_s.gsub(/^\$/, '')
|
31
|
+
params[sym] || []
|
32
|
+
else
|
33
|
+
[@operands[1]]
|
34
|
+
end
|
35
|
+
vv.map { |v| @idx[key][v] || [] }.reduce(&:|)
|
36
|
+
else
|
37
|
+
maps.to_a
|
38
|
+
end
|
39
|
+
when :and
|
40
|
+
parts = @operands.map { |o| o.predict(maps, params) }
|
41
|
+
if parts.include?(nil)
|
42
|
+
maps
|
43
|
+
else
|
44
|
+
parts.reduce(&:&)
|
45
|
+
end
|
46
|
+
when :or
|
47
|
+
@operands.map { |o| o.predict(maps, params) }.reduce(&:|)
|
48
|
+
when :join, :as
|
49
|
+
nil
|
50
|
+
else
|
51
|
+
maps.to_a
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def _scalar?(item)
|
58
|
+
item.is_a?(String) || item.is_a?(Time) || item.is_a?(Integer) || item.is_a?(Float) || item.is_a?(Symbol)
|
59
|
+
end
|
60
|
+
end
|
data/lib/factbase/light.rb
CHANGED
@@ -11,11 +11,8 @@ require_relative '../factbase'
|
|
11
11
|
# Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
|
12
12
|
# License:: MIT
|
13
13
|
class Factbase::Light
|
14
|
-
|
15
|
-
|
16
|
-
def initialize(fb, cache)
|
14
|
+
def initialize(fb)
|
17
15
|
@fb = fb
|
18
|
-
@cache = cache
|
19
16
|
end
|
20
17
|
|
21
18
|
def size
|
@@ -26,7 +23,11 @@ class Factbase::Light
|
|
26
23
|
@fb.insert
|
27
24
|
end
|
28
25
|
|
29
|
-
def
|
30
|
-
@fb.
|
26
|
+
def to_term(query)
|
27
|
+
@fb.to_term(query)
|
28
|
+
end
|
29
|
+
|
30
|
+
def query(query, maps = nil)
|
31
|
+
@fb.query(query, maps)
|
31
32
|
end
|
32
33
|
end
|
data/lib/factbase/logged.rb
CHANGED
@@ -6,20 +6,29 @@
|
|
6
6
|
require 'decoor'
|
7
7
|
require 'others'
|
8
8
|
require 'time'
|
9
|
-
require 'loog'
|
10
9
|
require 'tago'
|
11
10
|
require_relative 'syntax'
|
12
11
|
|
13
12
|
# A decorator of a Factbase, that logs all operations.
|
13
|
+
#
|
14
14
|
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
15
15
|
# Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
|
16
16
|
# License:: MIT
|
17
17
|
class Factbase::Logged
|
18
|
-
|
18
|
+
# Ctor.
|
19
|
+
# @param [Factbase] fb The factbase to decorate
|
20
|
+
# @param [Object] log The logging facility
|
21
|
+
# @param [Integer] time_tolerate How many seconds are OK per request
|
22
|
+
# @param [Print] tube The tube to use, if log is NIL
|
23
|
+
def initialize(fb, log = nil, time_tolerate: 1, tube: nil)
|
19
24
|
raise 'The "fb" is nil' if fb.nil?
|
20
25
|
@fb = fb
|
21
|
-
|
22
|
-
|
26
|
+
if log.nil?
|
27
|
+
raise 'Either "log" or "tube" must be non-NIL' if tube.nil?
|
28
|
+
@tube = tube
|
29
|
+
else
|
30
|
+
@tube = Tube.new(log, time_tolerate:)
|
31
|
+
end
|
23
32
|
end
|
24
33
|
|
25
34
|
decoor(:fb)
|
@@ -27,12 +36,12 @@ class Factbase::Logged
|
|
27
36
|
def insert
|
28
37
|
start = Time.now
|
29
38
|
f = @fb.insert
|
30
|
-
@
|
31
|
-
Fact.new(f, @
|
39
|
+
@tube.say(start, "Inserted new fact ##{@fb.size} in #{start.ago}")
|
40
|
+
Fact.new(f, tube: @tube)
|
32
41
|
end
|
33
42
|
|
34
43
|
def query(query)
|
35
|
-
Query.new(
|
44
|
+
Query.new(query, @tube, @fb)
|
36
45
|
end
|
37
46
|
|
38
47
|
def txn
|
@@ -42,19 +51,36 @@ class Factbase::Logged
|
|
42
51
|
r =
|
43
52
|
@fb.txn do |fbt|
|
44
53
|
id = fbt.object_id
|
45
|
-
yield Factbase::Logged.new(fbt, @
|
54
|
+
yield Factbase::Logged.new(fbt, tube: @tube)
|
46
55
|
rescue Factbase::Rollback => e
|
47
56
|
rollback = true
|
48
57
|
raise e
|
49
58
|
end
|
50
59
|
if rollback
|
51
|
-
|
60
|
+
@tube.say(start, "Txn ##{id} rolled back in #{start.ago}")
|
52
61
|
else
|
53
|
-
|
62
|
+
@tube.say(start, "Txn ##{id} touched #{r} in #{start.ago}")
|
54
63
|
end
|
55
64
|
r
|
56
65
|
end
|
57
66
|
|
67
|
+
# Printer of log messages.
|
68
|
+
class Tube
|
69
|
+
def initialize(log, time_tolerate: 1)
|
70
|
+
@log = log
|
71
|
+
@time_tolerate = time_tolerate
|
72
|
+
end
|
73
|
+
|
74
|
+
def say(start, msg)
|
75
|
+
m = :debug
|
76
|
+
if Time.now - start > @time_tolerate
|
77
|
+
msg = "#{msg} (slow!)"
|
78
|
+
m = :warn
|
79
|
+
end
|
80
|
+
@log.send(m, msg)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
58
84
|
# Fact decorator.
|
59
85
|
#
|
60
86
|
# This is an internal class, it is not supposed to be instantiated directly.
|
@@ -62,9 +88,14 @@ class Factbase::Logged
|
|
62
88
|
class Fact
|
63
89
|
MAX_LENGTH = 64
|
64
90
|
|
65
|
-
def initialize(fact,
|
91
|
+
def initialize(fact, tube: nil, log: nil)
|
66
92
|
@fact = fact
|
67
|
-
@
|
93
|
+
@tube =
|
94
|
+
if log.nil?
|
95
|
+
tube
|
96
|
+
else
|
97
|
+
Tube.new(log)
|
98
|
+
end
|
68
99
|
end
|
69
100
|
|
70
101
|
def to_s
|
@@ -76,13 +107,14 @@ class Factbase::Logged
|
|
76
107
|
end
|
77
108
|
|
78
109
|
others do |*args|
|
110
|
+
start = Time.now
|
79
111
|
r = @fact.method_missing(*args)
|
80
112
|
k = args[0].to_s
|
81
113
|
v = args[1]
|
82
114
|
s = v.is_a?(Time) ? v.utc.iso8601 : v.to_s
|
83
115
|
s = v.to_s.inspect if v.is_a?(String)
|
84
116
|
s = "#{s[0..MAX_LENGTH / 2]}...#{s[-MAX_LENGTH / 2..]}" if s.length > MAX_LENGTH
|
85
|
-
@
|
117
|
+
@tube.say(start, "Set '#{k[0..-2]}' to #{s} (#{v.class})") if k.end_with?('=')
|
86
118
|
r
|
87
119
|
end
|
88
120
|
end
|
@@ -92,76 +124,76 @@ class Factbase::Logged
|
|
92
124
|
# This is an internal class, it is not supposed to be instantiated directly.
|
93
125
|
#
|
94
126
|
class Query
|
95
|
-
def initialize(
|
96
|
-
@fb = fb
|
127
|
+
def initialize(expr, tube, fb)
|
97
128
|
@expr = expr
|
98
|
-
@
|
129
|
+
@tube = tube
|
130
|
+
@fb = fb
|
99
131
|
end
|
100
132
|
|
101
|
-
def one(params = {})
|
133
|
+
def one(fb = @fb, params = {})
|
102
134
|
start = Time.now
|
103
|
-
q = Factbase::Syntax.new(@
|
135
|
+
q = Factbase::Syntax.new(@expr).to_term.to_s
|
104
136
|
r = nil
|
105
137
|
tail =
|
106
138
|
Factbase::Logged.elapsed do
|
107
|
-
r =
|
139
|
+
r = fb.query(@expr).one(fb, params)
|
108
140
|
end
|
109
141
|
if r.nil?
|
110
|
-
|
142
|
+
@tube.say(start, "Nothing found by '#{q}' #{tail}")
|
111
143
|
else
|
112
|
-
|
144
|
+
@tube.say(start, "Found #{r} (#{r.class}) by '#{q}' #{tail}")
|
113
145
|
end
|
114
146
|
r
|
115
147
|
end
|
116
148
|
|
117
|
-
def each(params = {}, &)
|
149
|
+
def each(fb = @fb, params = {}, &)
|
118
150
|
start = Time.now
|
119
|
-
q = Factbase::Syntax.new(@
|
151
|
+
q = Factbase::Syntax.new(@expr).to_term.to_s
|
120
152
|
if block_given?
|
121
153
|
r = nil
|
122
154
|
tail =
|
123
155
|
Factbase::Logged.elapsed do
|
124
|
-
r =
|
156
|
+
r = fb.query(@expr).each(fb, params, &)
|
125
157
|
end
|
126
158
|
raise ".each of #{@expr.class} returned #{r.class}" unless r.is_a?(Integer)
|
127
159
|
if r.zero?
|
128
|
-
|
160
|
+
@tube.say(start, "Nothing found by '#{q}' #{tail}")
|
129
161
|
else
|
130
|
-
|
162
|
+
@tube.say(start, "Found #{r} fact(s) by '#{q}' #{tail}")
|
131
163
|
end
|
132
164
|
r
|
133
165
|
else
|
134
166
|
array = []
|
135
167
|
tail =
|
136
168
|
Factbase::Logged.elapsed do
|
137
|
-
|
169
|
+
fb.query(@expr).each(fb, params) do |f|
|
138
170
|
array << f
|
139
171
|
end
|
140
172
|
end
|
141
173
|
if array.empty?
|
142
|
-
|
174
|
+
@tube.say(start, "Nothing found by '#{q}' #{tail}")
|
143
175
|
else
|
144
|
-
|
176
|
+
@tube.say(start, "Found #{array.size} fact(s) by '#{q}' #{tail}")
|
145
177
|
end
|
146
178
|
array
|
147
179
|
end
|
148
180
|
end
|
149
181
|
|
150
|
-
def delete!
|
182
|
+
def delete!(fb = @fb)
|
151
183
|
r = nil
|
152
184
|
start = Time.now
|
153
|
-
before =
|
185
|
+
before = fb.size
|
154
186
|
tail =
|
155
187
|
Factbase::Logged.elapsed do
|
156
|
-
r = @fb.query(@expr).delete!
|
188
|
+
r = @fb.query(@expr).delete!(fb)
|
157
189
|
end
|
158
190
|
raise ".delete! of #{@expr.class} returned #{r.class}" unless r.is_a?(Integer)
|
159
191
|
if before.zero?
|
160
|
-
|
192
|
+
@tube.say(start, "There were no facts, nothing deleted by '#{@expr}' #{tail}")
|
161
193
|
elsif r.zero?
|
162
|
-
|
194
|
+
@tube.say(start, "No facts out of #{before} deleted by '#{@expr}' #{tail}")
|
163
195
|
else
|
164
|
-
|
196
|
+
@tube.say(start, "Deleted #{r} fact(s) out of #{before} by '#{@expr}' #{tail}")
|
165
197
|
end
|
166
198
|
r
|
167
199
|
end
|
@@ -172,13 +204,4 @@ class Factbase::Logged
|
|
172
204
|
yield
|
173
205
|
"in #{start.ago}"
|
174
206
|
end
|
175
|
-
|
176
|
-
def self.log_it(loog, start, msg)
|
177
|
-
m = :debug
|
178
|
-
if Time.now - start > 1
|
179
|
-
msg = "#{msg} (slow!)"
|
180
|
-
m = :warn
|
181
|
-
end
|
182
|
-
loog.send(m, msg)
|
183
|
-
end
|
184
207
|
end
|
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
43
|
def query(query)
|
44
|
-
Query.new(@fb.query(query), @check)
|
44
|
+
Query.new(@fb.query(query), @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?
|