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
data/lib/factbase/fact.rb
CHANGED
@@ -10,28 +10,27 @@ require_relative '../factbase'
|
|
10
10
|
|
11
11
|
# A single fact in a factbase.
|
12
12
|
#
|
13
|
-
# This is an internal class, it is
|
14
|
-
#
|
13
|
+
# This is an internal class, it is supposed to be instantiated only by the
|
14
|
+
# +Factbase+ class.
|
15
15
|
# However, it is possible to use it for testing directly, for example to make a
|
16
16
|
# fact with a single key/value pair inside:
|
17
17
|
#
|
18
18
|
# require 'factbase/fact'
|
19
|
-
# f = Factbase::Fact.new(
|
19
|
+
# f = Factbase::Fact.new({ 'foo' => [42, 256, 'Hello, world!'] })
|
20
20
|
# assert_equal(42, f.foo)
|
21
21
|
#
|
22
22
|
# A fact is basically a key/value hash map, where values are non-empty
|
23
23
|
# sets of values.
|
24
24
|
#
|
25
|
+
# It is NOT thread-safe!
|
26
|
+
#
|
25
27
|
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
26
28
|
# Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
|
27
29
|
# License:: MIT
|
28
30
|
class Factbase::Fact
|
29
31
|
# Ctor.
|
30
|
-
# @param [Mutex] mutex A mutex to use for maps synchronization
|
31
32
|
# @param [Hash] map A map of key/value pairs
|
32
|
-
def initialize(
|
33
|
-
@fb = fb
|
34
|
-
@mutex = mutex
|
33
|
+
def initialize(map)
|
35
34
|
@map = map
|
36
35
|
end
|
37
36
|
|
@@ -48,6 +47,10 @@ class Factbase::Fact
|
|
48
47
|
end
|
49
48
|
|
50
49
|
# When a method is missing, this method is called.
|
50
|
+
# Method missing handler for dynamic property access and setting
|
51
|
+
# @param [Symbol] method The method name being called
|
52
|
+
# @param [Array] args Method arguments
|
53
|
+
# @return [Object] The value retrieved or nil if setting a value
|
51
54
|
others do |*args|
|
52
55
|
k = args[0].to_s
|
53
56
|
if k.end_with?('=')
|
@@ -59,12 +62,9 @@ class Factbase::Fact
|
|
59
62
|
raise "The value of '#{kk}' can't be empty" if v == ''
|
60
63
|
raise "The type '#{v.class}' of '#{kk}' is not allowed" unless [String, Integer, Float, Time].include?(v.class)
|
61
64
|
v = v.utc if v.is_a?(Time)
|
62
|
-
@
|
63
|
-
|
64
|
-
|
65
|
-
@map[kk].uniq!
|
66
|
-
end
|
67
|
-
@fb.cache.clear
|
65
|
+
@map[kk] = [] if @map[kk].nil?
|
66
|
+
@map[kk] << v
|
67
|
+
@map[kk].uniq!
|
68
68
|
nil
|
69
69
|
elsif k == '[]'
|
70
70
|
@map[args[1].to_s]
|
@@ -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,73 +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 = {})
|
102
|
-
|
133
|
+
def one(fb = @fb, params = {})
|
134
|
+
start = Time.now
|
135
|
+
q = Factbase::Syntax.new(@expr).to_term.to_s
|
103
136
|
r = nil
|
104
137
|
tail =
|
105
138
|
Factbase::Logged.elapsed do
|
106
|
-
r =
|
139
|
+
r = fb.query(@expr).one(fb, params)
|
107
140
|
end
|
108
141
|
if r.nil?
|
109
|
-
@
|
142
|
+
@tube.say(start, "Nothing found by '#{q}' #{tail}")
|
110
143
|
else
|
111
|
-
@
|
144
|
+
@tube.say(start, "Found #{r} (#{r.class}) by '#{q}' #{tail}")
|
112
145
|
end
|
113
146
|
r
|
114
147
|
end
|
115
148
|
|
116
|
-
def each(params = {}, &)
|
117
|
-
|
149
|
+
def each(fb = @fb, params = {}, &)
|
150
|
+
start = Time.now
|
151
|
+
q = Factbase::Syntax.new(@expr).to_term.to_s
|
118
152
|
if block_given?
|
119
153
|
r = nil
|
120
154
|
tail =
|
121
155
|
Factbase::Logged.elapsed do
|
122
|
-
r =
|
156
|
+
r = fb.query(@expr).each(fb, params, &)
|
123
157
|
end
|
124
158
|
raise ".each of #{@expr.class} returned #{r.class}" unless r.is_a?(Integer)
|
125
159
|
if r.zero?
|
126
|
-
@
|
160
|
+
@tube.say(start, "Nothing found by '#{q}' #{tail}")
|
127
161
|
else
|
128
|
-
@
|
162
|
+
@tube.say(start, "Found #{r} fact(s) by '#{q}' #{tail}")
|
129
163
|
end
|
130
164
|
r
|
131
165
|
else
|
132
166
|
array = []
|
133
167
|
tail =
|
134
168
|
Factbase::Logged.elapsed do
|
135
|
-
|
169
|
+
fb.query(@expr).each(fb, params) do |f|
|
136
170
|
array << f
|
137
171
|
end
|
138
172
|
end
|
139
173
|
if array.empty?
|
140
|
-
@
|
174
|
+
@tube.say(start, "Nothing found by '#{q}' #{tail}")
|
141
175
|
else
|
142
|
-
@
|
176
|
+
@tube.say(start, "Found #{array.size} fact(s) by '#{q}' #{tail}")
|
143
177
|
end
|
144
178
|
array
|
145
179
|
end
|
146
180
|
end
|
147
181
|
|
148
|
-
def delete!
|
182
|
+
def delete!(fb = @fb)
|
149
183
|
r = nil
|
150
|
-
|
184
|
+
start = Time.now
|
185
|
+
before = fb.size
|
151
186
|
tail =
|
152
187
|
Factbase::Logged.elapsed do
|
153
|
-
r = @fb.query(@expr).delete!
|
188
|
+
r = @fb.query(@expr).delete!(fb)
|
154
189
|
end
|
155
190
|
raise ".delete! of #{@expr.class} returned #{r.class}" unless r.is_a?(Integer)
|
156
191
|
if before.zero?
|
157
|
-
@
|
192
|
+
@tube.say(start, "There were no facts, nothing deleted by '#{@expr}' #{tail}")
|
158
193
|
elsif r.zero?
|
159
|
-
@
|
194
|
+
@tube.say(start, "No facts out of #{before} deleted by '#{@expr}' #{tail}")
|
160
195
|
else
|
161
|
-
@
|
196
|
+
@tube.say(start, "Deleted #{r} fact(s) out of #{before} by '#{@expr}' #{tail}")
|
162
197
|
end
|
163
198
|
r
|
164
199
|
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
|