factbase 0.19.10 → 0.19.12
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/Gemfile +5 -4
- data/Gemfile.lock +18 -14
- data/README.md +127 -20
- data/Rakefile +2 -7
- data/factbase.gemspec +11 -11
- data/lib/factbase/accum.rb +1 -1
- data/lib/factbase/cached/cached_fact.rb +1 -2
- data/lib/factbase/cached/cached_factbase.rb +3 -3
- data/lib/factbase/cached/cached_query.rb +4 -6
- data/lib/factbase/cached/cached_term.rb +1 -2
- data/lib/factbase/churn.rb +4 -8
- data/lib/factbase/fact.rb +12 -9
- data/lib/factbase/flatten.rb +2 -2
- data/lib/factbase/impatient.rb +14 -13
- data/lib/factbase/indexed/indexed_and.rb +14 -20
- data/lib/factbase/indexed/indexed_eq.rb +5 -1
- data/lib/factbase/indexed/indexed_fact.rb +1 -4
- data/lib/factbase/indexed/indexed_factbase.rb +4 -4
- data/lib/factbase/indexed/indexed_gt.rb +3 -1
- data/lib/factbase/indexed/indexed_gte.rb +51 -0
- data/lib/factbase/indexed/indexed_lt.rb +3 -1
- data/lib/factbase/indexed/indexed_lte.rb +51 -0
- data/lib/factbase/indexed/indexed_not.rb +1 -1
- data/lib/factbase/indexed/indexed_or.rb +2 -2
- data/lib/factbase/indexed/indexed_query.rb +6 -7
- data/lib/factbase/indexed/indexed_term.rb +10 -6
- data/lib/factbase/indexed/indexed_unique.rb +4 -2
- data/lib/factbase/inv.rb +3 -3
- data/lib/factbase/lazy_taped.rb +10 -13
- data/lib/factbase/lazy_taped_hash.rb +2 -1
- data/lib/factbase/light.rb +1 -1
- data/lib/factbase/logged.rb +37 -34
- data/lib/factbase/pre.rb +3 -3
- data/lib/factbase/query.rb +4 -5
- data/lib/factbase/rules.rb +8 -8
- data/lib/factbase/sync/sync_factbase.rb +2 -2
- data/lib/factbase/syntax.rb +19 -20
- data/lib/factbase/tallied.rb +7 -8
- data/lib/factbase/taped.rb +5 -11
- data/lib/factbase/tee.rb +2 -2
- data/lib/factbase/term.rb +58 -59
- data/lib/factbase/terms/agg.rb +3 -4
- data/lib/factbase/terms/arithmetic.rb +7 -7
- data/lib/factbase/terms/as.rb +2 -2
- data/lib/factbase/terms/assert.rb +5 -13
- data/lib/factbase/terms/base.rb +7 -10
- data/lib/factbase/terms/best.rb +1 -1
- data/lib/factbase/terms/boolean.rb +1 -1
- data/lib/factbase/terms/compare.rb +17 -1
- data/lib/factbase/terms/contains.rb +28 -0
- data/lib/factbase/terms/defn.rb +8 -6
- data/lib/factbase/terms/empty.rb +1 -1
- data/lib/factbase/terms/ends_with.rb +27 -0
- data/lib/factbase/terms/first.rb +2 -2
- data/lib/factbase/terms/head.rb +3 -3
- data/lib/factbase/terms/inverted.rb +2 -2
- data/lib/factbase/terms/join.rb +8 -7
- data/lib/factbase/terms/matches.rb +14 -4
- data/lib/factbase/terms/max.rb +1 -1
- data/lib/factbase/terms/min.rb +1 -1
- data/lib/factbase/terms/nth.rb +3 -3
- data/lib/factbase/terms/plus.rb +1 -1
- data/lib/factbase/terms/prev.rb +3 -6
- data/lib/factbase/terms/sorted.rb +2 -2
- data/lib/factbase/terms/sprintf.rb +11 -2
- data/lib/factbase/terms/starts_with.rb +27 -0
- data/lib/factbase/terms/sum.rb +2 -2
- data/lib/factbase/terms/to_float.rb +2 -2
- data/lib/factbase/terms/to_integer.rb +2 -2
- data/lib/factbase/terms/to_string.rb +1 -1
- data/lib/factbase/terms/to_time.rb +10 -2
- data/lib/factbase/terms/traced.rb +2 -2
- data/lib/factbase/terms/undef.rb +2 -2
- data/lib/factbase/terms/unique.rb +3 -7
- data/lib/factbase/terms/when.rb +2 -3
- data/lib/factbase/to_json.rb +2 -2
- data/lib/factbase/to_xml.rb +6 -10
- data/lib/factbase/to_yaml.rb +1 -1
- data/lib/factbase/version.rb +1 -2
- data/lib/factbase.rb +27 -13
- data/lib/fuzz.rb +3 -3
- metadata +6 -1
|
@@ -11,12 +11,12 @@ class Factbase::IndexedAnd
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def predict(maps, fb, params)
|
|
14
|
-
return
|
|
14
|
+
return if @idx.nil?
|
|
15
15
|
key = [maps.object_id, @term.operands.first, @term.op]
|
|
16
16
|
r = nil
|
|
17
17
|
if @term.operands.all? { |o| o.op == :eq } && @term.operands.size > 1 \
|
|
18
18
|
&& @term.operands.all? { |o| o.operands.first.is_a?(Symbol) && _scalar?(o.operands[1]) }
|
|
19
|
-
props = @term.operands.map { |o| o.operands.first }.sort
|
|
19
|
+
props = @term.operands.map { |o| o.operands.first }.sort!
|
|
20
20
|
key = [maps.object_id, props, :multi_and_eq]
|
|
21
21
|
entry = @idx[key]
|
|
22
22
|
maps_array = maps.to_a
|
|
@@ -45,19 +45,24 @@ class Factbase::IndexedAnd
|
|
|
45
45
|
j = tuples.flat_map { |t| entry[:index][t] || [] }.uniq(&:object_id)
|
|
46
46
|
r = maps.respond_to?(:repack) ? maps.repack(j) : j
|
|
47
47
|
else
|
|
48
|
+
fail = false
|
|
48
49
|
@term.operands.each do |o|
|
|
49
50
|
n = o.predict(maps, fb, params)
|
|
50
|
-
|
|
51
|
+
if n.nil?
|
|
52
|
+
fail = true
|
|
53
|
+
break
|
|
54
|
+
end
|
|
51
55
|
if r.nil?
|
|
52
56
|
r = n
|
|
53
|
-
elsif n.size < r.size * 8
|
|
57
|
+
elsif n.size < r.size * 8
|
|
54
58
|
small, large = n.size < r.size ? [n.to_a, r.to_a] : [r.to_a, n.to_a]
|
|
55
59
|
ids = Set.new(small.map(&:object_id))
|
|
56
60
|
r = large.select { |f| ids.include?(f.object_id) }
|
|
57
61
|
end
|
|
58
|
-
break if r.size < maps.size / 32
|
|
59
|
-
break if r.size < 128
|
|
62
|
+
break if r.size < maps.size / 32
|
|
63
|
+
break if r.size < 128
|
|
60
64
|
end
|
|
65
|
+
return if fail
|
|
61
66
|
end
|
|
62
67
|
r
|
|
63
68
|
end
|
|
@@ -69,19 +74,8 @@ class Factbase::IndexedAnd
|
|
|
69
74
|
end
|
|
70
75
|
|
|
71
76
|
def _all_tuples(fact, props)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if props.size > 1
|
|
76
|
-
tails = _all_tuples(fact, props[1..])
|
|
77
|
-
ext = []
|
|
78
|
-
tuples.each do |t|
|
|
79
|
-
tails.each do |tail|
|
|
80
|
-
ext << (t + tail)
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
tuples = ext
|
|
84
|
-
end
|
|
85
|
-
tuples
|
|
77
|
+
values = props.map { |p| fact[p.to_s] || [] }
|
|
78
|
+
return [] if values.any?(&:empty?)
|
|
79
|
+
values[0].product(*values[1..])
|
|
86
80
|
end
|
|
87
81
|
end
|
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
# SPDX-FileCopyrightText: Copyright (c) 2024-2026 Yegor Bugayenko
|
|
4
4
|
# SPDX-License-Identifier: MIT
|
|
5
5
|
|
|
6
|
-
# Indexed term 'eq'.
|
|
6
|
+
# Indexed term 'eq' that uses the hash-based inverted index for fast equality lookups.
|
|
7
|
+
#
|
|
8
|
+
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
|
9
|
+
# Copyright:: Copyright (c) 2024-2026 Yegor Bugayenko
|
|
10
|
+
# License:: MIT
|
|
7
11
|
class Factbase::IndexedEq
|
|
8
12
|
def initialize(term, idx)
|
|
9
13
|
@term = term
|
|
@@ -26,11 +26,8 @@ class Factbase::IndexedFact
|
|
|
26
26
|
@origin.to_s
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
# When a method is missing, this method is called.
|
|
30
29
|
others do |*args|
|
|
31
|
-
# Only clear index when modifying properties on existing (non-fresh) facts
|
|
32
|
-
# Fresh facts are not in the index yet, so modifications don't affect it
|
|
33
30
|
@idx.clear if args[0].to_s.end_with?('=') && !@fresh.include?(object_id)
|
|
34
|
-
@origin.
|
|
31
|
+
@origin.__send__(*args)
|
|
35
32
|
end
|
|
36
33
|
end
|
|
@@ -23,9 +23,9 @@ class Factbase::IndexedFactbase
|
|
|
23
23
|
# @param [Hash] idx Index to use
|
|
24
24
|
# @param [Set] fresh The set of IDs of newly inserted facts
|
|
25
25
|
def initialize(origin, idx = {}, fresh = Set.new)
|
|
26
|
-
raise 'Wrong type of original' unless origin.respond_to?(:query)
|
|
26
|
+
raise(ArgumentError, 'Wrong type of original') unless origin.respond_to?(:query)
|
|
27
27
|
@origin = origin
|
|
28
|
-
raise 'Wrong type of index' unless idx.is_a?(Hash)
|
|
28
|
+
raise(ArgumentError, 'Wrong type of index') unless idx.is_a?(Hash)
|
|
29
29
|
@idx = idx
|
|
30
30
|
@fresh = fresh
|
|
31
31
|
end
|
|
@@ -64,7 +64,7 @@ class Factbase::IndexedFactbase
|
|
|
64
64
|
inner_idx = {}
|
|
65
65
|
result =
|
|
66
66
|
@origin.txn do |fbt|
|
|
67
|
-
yield
|
|
67
|
+
yield(Factbase::IndexedFactbase.new(fbt, inner_idx, @fresh))
|
|
68
68
|
end
|
|
69
69
|
@idx.clear if result.deleted.positive? || result.added.positive?
|
|
70
70
|
@fresh.clear
|
|
@@ -100,7 +100,7 @@ class Factbase::IndexedFactbase
|
|
|
100
100
|
#
|
|
101
101
|
# @param [String] bytes Binary string to import
|
|
102
102
|
def import(bytes)
|
|
103
|
-
raise 'Empty input, cannot load a factbase' if bytes.empty?
|
|
103
|
+
raise(StandardError, 'Empty input, cannot load a factbase') if bytes.empty?
|
|
104
104
|
data = Marshal.load(bytes)
|
|
105
105
|
if data.is_a?(Hash) && data.key?(:maps)
|
|
106
106
|
@origin.import(data[:maps])
|
|
@@ -44,6 +44,8 @@ class Factbase::IndexedGt
|
|
|
44
44
|
def _search(entry, target)
|
|
45
45
|
idx = entry[:facts].bsearch_index { |v, _| v > target }
|
|
46
46
|
return [] if idx.nil?
|
|
47
|
-
entry[:facts][idx..].map { |_, f| f }
|
|
47
|
+
facts = entry[:facts][idx..].map { |_, f| f }
|
|
48
|
+
facts.uniq!(&:object_id)
|
|
49
|
+
facts
|
|
48
50
|
end
|
|
49
51
|
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) 2024-2026 Yegor Bugayenko
|
|
4
|
+
# SPDX-License-Identifier: MIT
|
|
5
|
+
|
|
6
|
+
# Indexed term 'gte'.
|
|
7
|
+
class Factbase::IndexedGte
|
|
8
|
+
def initialize(term, idx)
|
|
9
|
+
@term = term
|
|
10
|
+
@idx = idx
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def predict(maps, _fb, params)
|
|
14
|
+
op1, op2 = @term.operands
|
|
15
|
+
return unless op1.is_a?(Symbol) && _scalar?(op2)
|
|
16
|
+
prop = op1.to_s
|
|
17
|
+
target = op2.is_a?(Symbol) ? params[op2.to_s]&.first : op2
|
|
18
|
+
return maps || [] if target.nil?
|
|
19
|
+
key = [maps.object_id, prop, :facts]
|
|
20
|
+
@idx[key] ||= { facts: [], count: 0 }
|
|
21
|
+
entry = @idx[key]
|
|
22
|
+
_feed(maps.to_a, entry, prop)
|
|
23
|
+
matched = _search(entry, target)
|
|
24
|
+
maps.respond_to?(:repack) ? maps.repack(matched) : matched
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def _scalar?(item)
|
|
30
|
+
item.is_a?(String) || item.is_a?(Time) || item.is_a?(Integer) || item.is_a?(Float) || item.is_a?(Symbol)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def _feed(facts, entry, prop)
|
|
34
|
+
return unless entry[:count] < facts.size
|
|
35
|
+
facts[entry[:count]..].each do |fact|
|
|
36
|
+
fact[prop]&.each do |v|
|
|
37
|
+
entry[:facts] << [v, fact]
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
entry[:facts].sort_by! { |pair| pair[0] }
|
|
41
|
+
entry[:count] = facts.size
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def _search(entry, target)
|
|
45
|
+
idx = entry[:facts].bsearch_index { |v, _| v >= target }
|
|
46
|
+
return [] if idx.nil?
|
|
47
|
+
facts = entry[:facts][idx..].map { |_, f| f }
|
|
48
|
+
facts.uniq!(&:object_id)
|
|
49
|
+
facts
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -44,6 +44,8 @@ class Factbase::IndexedLt
|
|
|
44
44
|
def _search(entry, target)
|
|
45
45
|
idx = entry[:facts].bsearch_index { |v, _| v >= target }
|
|
46
46
|
res = idx.nil? ? entry[:facts] : entry[:facts][0...idx]
|
|
47
|
-
res.map { |_, f| f }
|
|
47
|
+
facts = res.map { |_, f| f }
|
|
48
|
+
facts.uniq!(&:object_id)
|
|
49
|
+
facts
|
|
48
50
|
end
|
|
49
51
|
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) 2024-2026 Yegor Bugayenko
|
|
4
|
+
# SPDX-License-Identifier: MIT
|
|
5
|
+
|
|
6
|
+
# Indexed term 'lte'.
|
|
7
|
+
class Factbase::IndexedLte
|
|
8
|
+
def initialize(term, idx)
|
|
9
|
+
@term = term
|
|
10
|
+
@idx = idx
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def predict(maps, _fb, params)
|
|
14
|
+
op1, op2 = @term.operands
|
|
15
|
+
return unless op1.is_a?(Symbol) && _scalar?(op2)
|
|
16
|
+
prop = op1.to_s
|
|
17
|
+
target = op2.is_a?(Symbol) ? params[op2.to_s]&.first : op2
|
|
18
|
+
return maps || [] if target.nil?
|
|
19
|
+
key = [maps.object_id, prop, :facts]
|
|
20
|
+
@idx[key] ||= { facts: [], count: 0 }
|
|
21
|
+
entry = @idx[key]
|
|
22
|
+
_feed(maps.to_a, entry, prop)
|
|
23
|
+
matched = _search(entry, target)
|
|
24
|
+
maps.respond_to?(:repack) ? maps.repack(matched) : matched
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def _scalar?(item)
|
|
30
|
+
item.is_a?(String) || item.is_a?(Time) || item.is_a?(Integer) || item.is_a?(Float) || item.is_a?(Symbol)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def _feed(facts, entry, prop)
|
|
34
|
+
return unless entry[:count] < facts.size
|
|
35
|
+
facts[entry[:count]..].each do |fact|
|
|
36
|
+
fact[prop]&.each do |v|
|
|
37
|
+
entry[:facts] << [v, fact]
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
entry[:facts].sort_by! { |pair| pair[0] }
|
|
41
|
+
entry[:count] = facts.size
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def _search(entry, target)
|
|
45
|
+
idx = entry[:facts].bsearch_index { |v, _| v > target }
|
|
46
|
+
res = idx.nil? ? entry[:facts] : entry[:facts][0...idx]
|
|
47
|
+
facts = res.map { |_, f| f }
|
|
48
|
+
facts.uniq!(&:object_id)
|
|
49
|
+
facts
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -11,7 +11,7 @@ class Factbase::IndexedOr
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def predict(maps, fb, params)
|
|
14
|
-
return
|
|
14
|
+
return if @idx.nil?
|
|
15
15
|
r = nil
|
|
16
16
|
@term.operands.each do |o|
|
|
17
17
|
n = o.predict(maps, fb, params)
|
|
@@ -21,7 +21,7 @@ class Factbase::IndexedOr
|
|
|
21
21
|
end
|
|
22
22
|
r = maps & [] if r.nil?
|
|
23
23
|
r |= n.to_a
|
|
24
|
-
return maps if r.size > maps.size / 4
|
|
24
|
+
return maps if r.size > maps.size / 4
|
|
25
25
|
end
|
|
26
26
|
r
|
|
27
27
|
end
|
|
@@ -37,11 +37,12 @@ class Factbase::IndexedQuery
|
|
|
37
37
|
# @return [Integer] Total number of facts yielded
|
|
38
38
|
def each(fb = @fb, params = {})
|
|
39
39
|
return to_enum(__method__, fb, params) unless block_given?
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
yield
|
|
40
|
+
n = 0
|
|
41
|
+
@origin.each(fb, params) do |f|
|
|
42
|
+
yield(Factbase::IndexedFact.new(f, @idx, @fresh))
|
|
43
|
+
n += 1
|
|
43
44
|
end
|
|
44
|
-
|
|
45
|
+
n
|
|
45
46
|
end
|
|
46
47
|
|
|
47
48
|
# Read a single value.
|
|
@@ -56,8 +57,6 @@ class Factbase::IndexedQuery
|
|
|
56
57
|
# @param [Factbase] fb The factbase
|
|
57
58
|
# @return [Integer] Total number of facts deleted
|
|
58
59
|
def delete!(fb = @fb)
|
|
59
|
-
|
|
60
|
-
@idx.clear
|
|
61
|
-
result
|
|
60
|
+
@origin.delete!(fb).tap { @idx.clear }
|
|
62
61
|
end
|
|
63
62
|
end
|
|
@@ -5,15 +5,17 @@
|
|
|
5
5
|
|
|
6
6
|
require 'tago'
|
|
7
7
|
require_relative '../../factbase'
|
|
8
|
+
require_relative '../indexed/indexed_absent'
|
|
9
|
+
require_relative '../indexed/indexed_and'
|
|
8
10
|
require_relative '../indexed/indexed_eq'
|
|
9
|
-
require_relative '../indexed/
|
|
11
|
+
require_relative '../indexed/indexed_exists'
|
|
10
12
|
require_relative '../indexed/indexed_gt'
|
|
11
|
-
require_relative '../indexed/
|
|
13
|
+
require_relative '../indexed/indexed_gte'
|
|
14
|
+
require_relative '../indexed/indexed_lt'
|
|
15
|
+
require_relative '../indexed/indexed_lte'
|
|
12
16
|
require_relative '../indexed/indexed_not'
|
|
13
|
-
require_relative '../indexed/
|
|
14
|
-
require_relative '../indexed/indexed_and'
|
|
17
|
+
require_relative '../indexed/indexed_one'
|
|
15
18
|
require_relative '../indexed/indexed_or'
|
|
16
|
-
require_relative '../indexed/indexed_absent'
|
|
17
19
|
require_relative '../indexed/indexed_unique'
|
|
18
20
|
|
|
19
21
|
# Term with an index.
|
|
@@ -35,7 +37,7 @@ module Factbase::IndexedTerm
|
|
|
35
37
|
return t.predict(maps, fb, params) if t.respond_to?(:predict)
|
|
36
38
|
end
|
|
37
39
|
m = :"#{@op}_predict"
|
|
38
|
-
return
|
|
40
|
+
return __send__(m, maps, fb, params) if respond_to?(m)
|
|
39
41
|
_init_indexes unless @indexes
|
|
40
42
|
@indexes[@op].predict(maps, fb, params) if @indexes.key?(@op)
|
|
41
43
|
end
|
|
@@ -46,7 +48,9 @@ module Factbase::IndexedTerm
|
|
|
46
48
|
@indexes = {
|
|
47
49
|
eq: Factbase::IndexedEq.new(self, @idx),
|
|
48
50
|
lt: Factbase::IndexedLt.new(self, @idx),
|
|
51
|
+
lte: Factbase::IndexedLte.new(self, @idx),
|
|
49
52
|
gt: Factbase::IndexedGt.new(self, @idx),
|
|
53
|
+
gte: Factbase::IndexedGte.new(self, @idx),
|
|
50
54
|
one: Factbase::IndexedOne.new(self, @idx),
|
|
51
55
|
exists: Factbase::IndexedExists.new(self, @idx),
|
|
52
56
|
absent: Factbase::IndexedAbsent.new(self, @idx),
|
data/lib/factbase/inv.rb
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
# SPDX-FileCopyrightText: Copyright (c) 2024-2026 Yegor Bugayenko
|
|
4
4
|
# SPDX-License-Identifier: MIT
|
|
5
5
|
|
|
6
|
-
require 'others'
|
|
7
6
|
require 'decoor'
|
|
7
|
+
require 'others'
|
|
8
8
|
require_relative '../factbase'
|
|
9
9
|
|
|
10
10
|
# A decorator of a Factbase, that checks invariants on every set.
|
|
@@ -40,7 +40,7 @@ class Factbase::Inv
|
|
|
40
40
|
|
|
41
41
|
def txn
|
|
42
42
|
@fb.txn do |fbt|
|
|
43
|
-
yield
|
|
43
|
+
yield(Factbase::Inv.new(fbt, &@block))
|
|
44
44
|
end
|
|
45
45
|
end
|
|
46
46
|
|
|
@@ -89,7 +89,7 @@ class Factbase::Inv
|
|
|
89
89
|
def each(fb = @fb, params = {})
|
|
90
90
|
return to_enum(__method__, fb, params) unless block_given?
|
|
91
91
|
@query.each(fb, params) do |f|
|
|
92
|
-
yield
|
|
92
|
+
yield(Fact.new(f, @block))
|
|
93
93
|
end
|
|
94
94
|
end
|
|
95
95
|
end
|
data/lib/factbase/lazy_taped.rb
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
# SPDX-License-Identifier: MIT
|
|
5
5
|
|
|
6
6
|
require_relative '../factbase'
|
|
7
|
-
require_relative 'taped'
|
|
8
7
|
require_relative 'lazy_taped_hash'
|
|
8
|
+
require_relative 'taped'
|
|
9
9
|
|
|
10
10
|
# A lazy decorator of an Array with HashMaps that defers copying until modification.
|
|
11
11
|
class Factbase::LazyTaped
|
|
@@ -22,7 +22,7 @@ class Factbase::LazyTaped
|
|
|
22
22
|
# Returns the original map this copy was derived from.
|
|
23
23
|
# Returns nil if the base hasn't been copied yet or if the fact is new.
|
|
24
24
|
def source_of(copy)
|
|
25
|
-
return
|
|
25
|
+
return unless @copied
|
|
26
26
|
@copies.key(copy)
|
|
27
27
|
end
|
|
28
28
|
|
|
@@ -76,21 +76,21 @@ class Factbase::LazyTaped
|
|
|
76
76
|
is_copied = copied?
|
|
77
77
|
unless is_copied
|
|
78
78
|
@origin.each do |m|
|
|
79
|
-
yield
|
|
79
|
+
yield(_tape(m))
|
|
80
80
|
yielded_size += 1
|
|
81
81
|
end
|
|
82
82
|
end
|
|
83
83
|
staged = is_copied == copied? ? @staged : @staged[yielded_size..]
|
|
84
84
|
staged&.each do |f|
|
|
85
85
|
next if f.nil?
|
|
86
|
-
yield
|
|
86
|
+
yield(_tape(f))
|
|
87
87
|
end
|
|
88
88
|
end
|
|
89
89
|
|
|
90
90
|
def delete_if
|
|
91
91
|
ensure_copied!
|
|
92
92
|
@staged.delete_if do |m|
|
|
93
|
-
r = yield
|
|
93
|
+
r = yield(m)
|
|
94
94
|
@deleted.append(source_of(m).object_id) if r
|
|
95
95
|
r
|
|
96
96
|
end
|
|
@@ -102,8 +102,7 @@ class Factbase::LazyTaped
|
|
|
102
102
|
|
|
103
103
|
def repack(other)
|
|
104
104
|
ensure_copied!
|
|
105
|
-
|
|
106
|
-
Factbase::Taped.new(copied, inserted: @inserted, deleted: @deleted, added: @added)
|
|
105
|
+
Factbase::Taped.new(other.map { |o| @copies[o] || o }, inserted: @inserted, deleted: @deleted, added: @added)
|
|
107
106
|
end
|
|
108
107
|
|
|
109
108
|
def &(other)
|
|
@@ -135,10 +134,9 @@ class Factbase::LazyTaped
|
|
|
135
134
|
|
|
136
135
|
def _join(other)
|
|
137
136
|
ensure_copied!
|
|
138
|
-
|
|
139
|
-
raise '
|
|
140
|
-
|
|
141
|
-
Factbase::Taped.new(n, inserted: @inserted, deleted: @deleted, added: @added)
|
|
137
|
+
raise(ArgumentError, 'Cannot join with another Taped') if other.respond_to?(:inserted)
|
|
138
|
+
raise(ArgumentError, 'Can only join with array') unless other.is_a?(Array)
|
|
139
|
+
Factbase::Taped.new(yield(to_a, other.to_a), inserted: @inserted, deleted: @deleted, added: @added)
|
|
142
140
|
end
|
|
143
141
|
|
|
144
142
|
def _track(copy, original)
|
|
@@ -147,7 +145,6 @@ class Factbase::LazyTaped
|
|
|
147
145
|
|
|
148
146
|
def _tape(map)
|
|
149
147
|
return LazyTapedHash.new(map, self, @added) unless copied?
|
|
150
|
-
|
|
151
|
-
Factbase::Taped::TapedHash.new(copy, @added)
|
|
148
|
+
Factbase::Taped::TapedHash.new(@copies[map] || map, @added)
|
|
152
149
|
end
|
|
153
150
|
end
|
|
@@ -65,7 +65,8 @@ class Factbase::LazyTaped
|
|
|
65
65
|
end
|
|
66
66
|
|
|
67
67
|
def method_missing(method, *, &)
|
|
68
|
-
|
|
68
|
+
ensure_copied_map if method.to_s.end_with?('=', '!')
|
|
69
|
+
current_map.__send__(method, *, &)
|
|
69
70
|
end
|
|
70
71
|
|
|
71
72
|
def respond_to_missing?(method, include_private = false)
|
data/lib/factbase/light.rb
CHANGED
data/lib/factbase/logged.rb
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
require 'decoor'
|
|
7
7
|
require 'others'
|
|
8
|
-
require 'time'
|
|
9
8
|
require 'tago'
|
|
9
|
+
require 'time'
|
|
10
10
|
require_relative 'syntax'
|
|
11
11
|
|
|
12
12
|
# A decorator of a Factbase, that logs all operations.
|
|
@@ -15,16 +15,18 @@ require_relative 'syntax'
|
|
|
15
15
|
# Copyright:: Copyright (c) 2024-2026 Yegor Bugayenko
|
|
16
16
|
# License:: MIT
|
|
17
17
|
class Factbase::Logged
|
|
18
|
+
MONO = Process::CLOCK_MONOTONIC
|
|
19
|
+
|
|
18
20
|
# Ctor.
|
|
19
21
|
# @param [Factbase] fb The factbase to decorate
|
|
20
22
|
# @param [Object] log The logging facility
|
|
21
23
|
# @param [Integer] time_tolerate How many seconds are OK per request
|
|
22
24
|
# @param [Print] tube The tube to use, if log is NIL
|
|
23
25
|
def initialize(fb, log = nil, time_tolerate: 1, tube: nil)
|
|
24
|
-
raise 'The "fb" is nil' if fb.nil?
|
|
26
|
+
raise(ArgumentError, 'The "fb" is nil') if fb.nil?
|
|
25
27
|
@origin = fb
|
|
26
28
|
if log.nil?
|
|
27
|
-
raise 'Either "log" or "tube" must be non-NIL' if tube.nil?
|
|
29
|
+
raise(ArgumentError, 'Either "log" or "tube" must be non-NIL') if tube.nil?
|
|
28
30
|
@tube = tube
|
|
29
31
|
else
|
|
30
32
|
@tube = Tube.new(log, time_tolerate:)
|
|
@@ -34,10 +36,8 @@ class Factbase::Logged
|
|
|
34
36
|
decoor(:origin)
|
|
35
37
|
|
|
36
38
|
def insert
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
@tube.say(start, "Inserted new fact ##{@origin.size} in #{start.ago}")
|
|
40
|
-
Fact.new(f, tube: @tube)
|
|
39
|
+
@tube.say(Process.clock_gettime(MONO), "Inserted new fact ##{@origin.size} in #{Time.now.ago}")
|
|
40
|
+
Fact.new(@origin.insert, tube: @tube)
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
def query(term, maps = nil)
|
|
@@ -46,21 +46,21 @@ class Factbase::Logged
|
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
def txn
|
|
49
|
-
|
|
49
|
+
mono = Process.clock_gettime(MONO)
|
|
50
50
|
id = nil
|
|
51
51
|
rollback = false
|
|
52
52
|
r =
|
|
53
53
|
@origin.txn do |fbt|
|
|
54
54
|
id = fbt.object_id
|
|
55
|
-
yield
|
|
55
|
+
yield(Factbase::Logged.new(fbt, tube: @tube))
|
|
56
56
|
rescue Factbase::Rollback => e
|
|
57
57
|
rollback = true
|
|
58
|
-
raise
|
|
58
|
+
raise(e)
|
|
59
59
|
end
|
|
60
60
|
if rollback
|
|
61
|
-
@tube.say(
|
|
61
|
+
@tube.say(mono, "Txn ##{id} rolled back in #{Time.now.ago}")
|
|
62
62
|
else
|
|
63
|
-
@tube.say(
|
|
63
|
+
@tube.say(mono, "Txn ##{id} touched #{r} in #{Time.now.ago}")
|
|
64
64
|
end
|
|
65
65
|
r
|
|
66
66
|
end
|
|
@@ -72,13 +72,13 @@ class Factbase::Logged
|
|
|
72
72
|
@time_tolerate = time_tolerate
|
|
73
73
|
end
|
|
74
74
|
|
|
75
|
-
def say(
|
|
75
|
+
def say(start_mono, msg)
|
|
76
76
|
m = :debug
|
|
77
|
-
if
|
|
77
|
+
if Process.clock_gettime(Factbase::Logged::MONO) - start_mono > @time_tolerate
|
|
78
78
|
msg = "#{msg} (slow!)"
|
|
79
79
|
m = :warn
|
|
80
80
|
end
|
|
81
|
-
@log.
|
|
81
|
+
@log.__send__(m, msg)
|
|
82
82
|
end
|
|
83
83
|
end
|
|
84
84
|
|
|
@@ -108,14 +108,16 @@ class Factbase::Logged
|
|
|
108
108
|
end
|
|
109
109
|
|
|
110
110
|
others do |*args|
|
|
111
|
-
|
|
111
|
+
mono = Process.clock_gettime(Factbase::Logged::MONO) if args[0].to_s.end_with?('=')
|
|
112
112
|
r = @fact.method_missing(*args)
|
|
113
113
|
k = args[0].to_s
|
|
114
114
|
v = args[1]
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
115
|
+
if k.end_with?('=')
|
|
116
|
+
s = v.is_a?(Time) ? v.utc.iso8601 : v.to_s
|
|
117
|
+
s = v.to_s.inspect if v.is_a?(String)
|
|
118
|
+
s = "#{s[0..(MAX_LENGTH / 2)]}...#{s[(-MAX_LENGTH / 2)..]}" if s.length > MAX_LENGTH
|
|
119
|
+
@tube.say(mono, "Set '#{k[0..-2]}' to #{s} (#{v.class})")
|
|
120
|
+
end
|
|
119
121
|
r
|
|
120
122
|
end
|
|
121
123
|
end
|
|
@@ -137,26 +139,28 @@ class Factbase::Logged
|
|
|
137
139
|
|
|
138
140
|
def each(fb = @fb, params = {}, &)
|
|
139
141
|
return to_enum(__method__, fb, params) unless block_given?
|
|
140
|
-
|
|
142
|
+
mono = Process.clock_gettime(Factbase::Logged::MONO)
|
|
141
143
|
r = nil
|
|
142
144
|
qry = @fb.query(@term, @maps)
|
|
143
145
|
tail =
|
|
144
146
|
Factbase::Logged.elapsed do
|
|
145
147
|
r = qry.each(fb, params, &)
|
|
146
148
|
end
|
|
147
|
-
|
|
149
|
+
unless r.is_a?(Integer)
|
|
150
|
+
raise(StandardError, ".query(#{@term.to_s.inspect}).each() of #{qry.class} returned #{r.class}")
|
|
151
|
+
end
|
|
148
152
|
q = Factbase::Syntax.new(@term).to_term.to_s
|
|
149
153
|
q = "#{q} with {#{params.map { |k, v| "#{k}=#{v}" }.join(', ')}}" if params.is_a?(Hash) && !params.empty?
|
|
150
154
|
if r.zero?
|
|
151
|
-
@tube.say(
|
|
155
|
+
@tube.say(mono, "Zero/#{@fb.size} facts found by #{q} #{tail}")
|
|
152
156
|
else
|
|
153
|
-
@tube.say(
|
|
157
|
+
@tube.say(mono, "Found #{r}/#{@fb.size} fact(s) by #{q} #{tail}")
|
|
154
158
|
end
|
|
155
159
|
r
|
|
156
160
|
end
|
|
157
161
|
|
|
158
162
|
def one(fb = @fb, params = {})
|
|
159
|
-
|
|
163
|
+
mono = Process.clock_gettime(Factbase::Logged::MONO)
|
|
160
164
|
q = Factbase::Syntax.new(@term).to_term.to_s
|
|
161
165
|
r = nil
|
|
162
166
|
tail =
|
|
@@ -164,36 +168,35 @@ class Factbase::Logged
|
|
|
164
168
|
r = @fb.query(@term, @maps).one(fb, params)
|
|
165
169
|
end
|
|
166
170
|
if r.nil?
|
|
167
|
-
@tube.say(
|
|
171
|
+
@tube.say(mono, "Nothing found by '#{q}' #{tail}")
|
|
168
172
|
else
|
|
169
|
-
@tube.say(
|
|
173
|
+
@tube.say(mono, "Found #{r} (#{r.class}) by '#{q}' #{tail}")
|
|
170
174
|
end
|
|
171
175
|
r
|
|
172
176
|
end
|
|
173
177
|
|
|
174
178
|
def delete!(fb = @fb)
|
|
175
179
|
r = nil
|
|
176
|
-
|
|
180
|
+
mono = Process.clock_gettime(Factbase::Logged::MONO)
|
|
177
181
|
before = @fb.size
|
|
178
182
|
tail =
|
|
179
183
|
Factbase::Logged.elapsed do
|
|
180
184
|
r = @fb.query(@term, @maps).delete!(fb)
|
|
181
185
|
end
|
|
182
|
-
raise ".delete! of #{@term.class} returned #{r.class}" unless r.is_a?(Integer)
|
|
186
|
+
raise(StandardError, ".delete! of #{@term.class} returned #{r.class}") unless r.is_a?(Integer)
|
|
183
187
|
if before.zero?
|
|
184
|
-
@tube.say(
|
|
188
|
+
@tube.say(mono, "There were no facts, nothing deleted by #{@term} #{tail}")
|
|
185
189
|
elsif r.zero?
|
|
186
|
-
@tube.say(
|
|
190
|
+
@tube.say(mono, "No facts out of #{before} deleted by #{@term} #{tail}")
|
|
187
191
|
else
|
|
188
|
-
@tube.say(
|
|
192
|
+
@tube.say(mono, "Deleted #{r} fact(s) out of #{before} by #{@term} #{tail}")
|
|
189
193
|
end
|
|
190
194
|
r
|
|
191
195
|
end
|
|
192
196
|
end
|
|
193
197
|
|
|
194
198
|
def self.elapsed
|
|
195
|
-
start = Time.now
|
|
196
199
|
yield
|
|
197
|
-
"in #{
|
|
200
|
+
"in #{Time.now.ago}"
|
|
198
201
|
end
|
|
199
202
|
end
|