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.
Files changed (83) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +5 -4
  3. data/Gemfile.lock +18 -14
  4. data/README.md +127 -20
  5. data/Rakefile +2 -7
  6. data/factbase.gemspec +11 -11
  7. data/lib/factbase/accum.rb +1 -1
  8. data/lib/factbase/cached/cached_fact.rb +1 -2
  9. data/lib/factbase/cached/cached_factbase.rb +3 -3
  10. data/lib/factbase/cached/cached_query.rb +4 -6
  11. data/lib/factbase/cached/cached_term.rb +1 -2
  12. data/lib/factbase/churn.rb +4 -8
  13. data/lib/factbase/fact.rb +12 -9
  14. data/lib/factbase/flatten.rb +2 -2
  15. data/lib/factbase/impatient.rb +14 -13
  16. data/lib/factbase/indexed/indexed_and.rb +14 -20
  17. data/lib/factbase/indexed/indexed_eq.rb +5 -1
  18. data/lib/factbase/indexed/indexed_fact.rb +1 -4
  19. data/lib/factbase/indexed/indexed_factbase.rb +4 -4
  20. data/lib/factbase/indexed/indexed_gt.rb +3 -1
  21. data/lib/factbase/indexed/indexed_gte.rb +51 -0
  22. data/lib/factbase/indexed/indexed_lt.rb +3 -1
  23. data/lib/factbase/indexed/indexed_lte.rb +51 -0
  24. data/lib/factbase/indexed/indexed_not.rb +1 -1
  25. data/lib/factbase/indexed/indexed_or.rb +2 -2
  26. data/lib/factbase/indexed/indexed_query.rb +6 -7
  27. data/lib/factbase/indexed/indexed_term.rb +10 -6
  28. data/lib/factbase/indexed/indexed_unique.rb +4 -2
  29. data/lib/factbase/inv.rb +3 -3
  30. data/lib/factbase/lazy_taped.rb +10 -13
  31. data/lib/factbase/lazy_taped_hash.rb +2 -1
  32. data/lib/factbase/light.rb +1 -1
  33. data/lib/factbase/logged.rb +37 -34
  34. data/lib/factbase/pre.rb +3 -3
  35. data/lib/factbase/query.rb +4 -5
  36. data/lib/factbase/rules.rb +8 -8
  37. data/lib/factbase/sync/sync_factbase.rb +2 -2
  38. data/lib/factbase/syntax.rb +19 -20
  39. data/lib/factbase/tallied.rb +7 -8
  40. data/lib/factbase/taped.rb +5 -11
  41. data/lib/factbase/tee.rb +2 -2
  42. data/lib/factbase/term.rb +58 -59
  43. data/lib/factbase/terms/agg.rb +3 -4
  44. data/lib/factbase/terms/arithmetic.rb +7 -7
  45. data/lib/factbase/terms/as.rb +2 -2
  46. data/lib/factbase/terms/assert.rb +5 -13
  47. data/lib/factbase/terms/base.rb +7 -10
  48. data/lib/factbase/terms/best.rb +1 -1
  49. data/lib/factbase/terms/boolean.rb +1 -1
  50. data/lib/factbase/terms/compare.rb +17 -1
  51. data/lib/factbase/terms/contains.rb +28 -0
  52. data/lib/factbase/terms/defn.rb +8 -6
  53. data/lib/factbase/terms/empty.rb +1 -1
  54. data/lib/factbase/terms/ends_with.rb +27 -0
  55. data/lib/factbase/terms/first.rb +2 -2
  56. data/lib/factbase/terms/head.rb +3 -3
  57. data/lib/factbase/terms/inverted.rb +2 -2
  58. data/lib/factbase/terms/join.rb +8 -7
  59. data/lib/factbase/terms/matches.rb +14 -4
  60. data/lib/factbase/terms/max.rb +1 -1
  61. data/lib/factbase/terms/min.rb +1 -1
  62. data/lib/factbase/terms/nth.rb +3 -3
  63. data/lib/factbase/terms/plus.rb +1 -1
  64. data/lib/factbase/terms/prev.rb +3 -6
  65. data/lib/factbase/terms/sorted.rb +2 -2
  66. data/lib/factbase/terms/sprintf.rb +11 -2
  67. data/lib/factbase/terms/starts_with.rb +27 -0
  68. data/lib/factbase/terms/sum.rb +2 -2
  69. data/lib/factbase/terms/to_float.rb +2 -2
  70. data/lib/factbase/terms/to_integer.rb +2 -2
  71. data/lib/factbase/terms/to_string.rb +1 -1
  72. data/lib/factbase/terms/to_time.rb +10 -2
  73. data/lib/factbase/terms/traced.rb +2 -2
  74. data/lib/factbase/terms/undef.rb +2 -2
  75. data/lib/factbase/terms/unique.rb +3 -7
  76. data/lib/factbase/terms/when.rb +2 -3
  77. data/lib/factbase/to_json.rb +2 -2
  78. data/lib/factbase/to_xml.rb +6 -10
  79. data/lib/factbase/to_yaml.rb +1 -1
  80. data/lib/factbase/version.rb +1 -2
  81. data/lib/factbase.rb +27 -13
  82. data/lib/fuzz.rb +3 -3
  83. metadata +6 -1
@@ -11,12 +11,12 @@ class Factbase::IndexedAnd
11
11
  end
12
12
 
13
13
  def predict(maps, fb, params)
14
- return nil if @idx.nil?
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
- break if n.nil?
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 # to skip some obvious matchings
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 # it's already small enough
59
- break if r.size < 128 # it's obviously already small enough
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
- prop = props.first.to_s
73
- tuples = []
74
- tuples += (fact[prop] || []).zip
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.send(*args)
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 Factbase::IndexedFactbase.new(fbt, inner_idx, @fresh)
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 }.uniq(&:object_id)
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 }.uniq(&:object_id)
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
@@ -18,7 +18,7 @@ class Factbase::IndexedNot
18
18
  _feed(maps.to_a, entry) do
19
19
  sub.predict(maps, fb, params)
20
20
  end
21
- return nil if entry[:facts].nil?
21
+ return if entry[:facts].nil?
22
22
  maps.respond_to?(:repack) ? maps.repack(entry[:facts]) : entry[:facts]
23
23
  end
24
24
 
@@ -11,7 +11,7 @@ class Factbase::IndexedOr
11
11
  end
12
12
 
13
13
  def predict(maps, fb, params)
14
- return nil if @idx.nil?
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 # it's big enough already
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
- a = @origin.each(fb, params).to_a
41
- a.each do |f|
42
- yield Factbase::IndexedFact.new(f, @idx, @fresh)
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
- a.size
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
- result = @origin.delete!(fb)
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/indexed_lt'
11
+ require_relative '../indexed/indexed_exists'
10
12
  require_relative '../indexed/indexed_gt'
11
- require_relative '../indexed/indexed_one'
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/indexed_exists'
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 send(m, maps, fb, params) if respond_to?(m)
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),
@@ -10,7 +10,9 @@ class Factbase::IndexedUnique
10
10
  @idx = idx
11
11
  end
12
12
 
13
- def predict(maps, _fb, _params)
14
- maps
13
+ # rubocop:disable Elegant/NoNilReturn
14
+ def predict(_maps, _fb, _params)
15
+ nil
15
16
  end
17
+ # rubocop:enable Elegant/NoNilReturn
16
18
  end
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 Factbase::Inv.new(fbt, &@block)
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 Fact.new(f, @block)
92
+ yield(Fact.new(f, @block))
93
93
  end
94
94
  end
95
95
  end
@@ -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 nil unless @copied
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 _tape(m)
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 _tape(f)
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 m
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
- copied = other.map { |o| @copies[o] || o }
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
- n = yield to_a, other.to_a
139
- raise 'Cannot join with another Taped' if other.respond_to?(:inserted)
140
- raise 'Can only join with array' unless other.is_a?(Array)
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
- copy = @copies[map] || map
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
- current_map.send(method, *, &)
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)
@@ -32,6 +32,6 @@ class Factbase::Light
32
32
  end
33
33
 
34
34
  def txn
35
- raise 'You cannot start a transaction inside another transaction'
35
+ raise(StandardError, 'You cannot start a transaction inside another transaction')
36
36
  end
37
37
  end
@@ -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
- start = Time.now
38
- f = @origin.insert
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
- start = Time.now
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 Factbase::Logged.new(fbt, tube: @tube)
55
+ yield(Factbase::Logged.new(fbt, tube: @tube))
56
56
  rescue Factbase::Rollback => e
57
57
  rollback = true
58
- raise e
58
+ raise(e)
59
59
  end
60
60
  if rollback
61
- @tube.say(start, "Txn ##{id} rolled back in #{start.ago}")
61
+ @tube.say(mono, "Txn ##{id} rolled back in #{Time.now.ago}")
62
62
  else
63
- @tube.say(start, "Txn ##{id} touched #{r} in #{start.ago}")
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(start, msg)
75
+ def say(start_mono, msg)
76
76
  m = :debug
77
- if Time.now - start > @time_tolerate
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.send(m, msg)
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
- start = Time.now
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
- s = v.is_a?(Time) ? v.utc.iso8601 : v.to_s
116
- s = v.to_s.inspect if v.is_a?(String)
117
- s = "#{s[0..(MAX_LENGTH / 2)]}...#{s[(-MAX_LENGTH / 2)..]}" if s.length > MAX_LENGTH
118
- @tube.say(start, "Set '#{k[0..-2]}' to #{s} (#{v.class})") if k.end_with?('=')
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
- start = Time.now
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
- raise ".query(#{@term.to_s.inspect}).each() of #{qry.class} returned #{r.class}" unless r.is_a?(Integer)
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(start, "Zero/#{@fb.size} facts found by #{q} #{tail}")
155
+ @tube.say(mono, "Zero/#{@fb.size} facts found by #{q} #{tail}")
152
156
  else
153
- @tube.say(start, "Found #{r}/#{@fb.size} fact(s) by #{q} #{tail}")
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
- start = Time.now
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(start, "Nothing found by '#{q}' #{tail}")
171
+ @tube.say(mono, "Nothing found by '#{q}' #{tail}")
168
172
  else
169
- @tube.say(start, "Found #{r} (#{r.class}) by '#{q}' #{tail}")
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
- start = Time.now
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(start, "There were no facts, nothing deleted by #{@term} #{tail}")
188
+ @tube.say(mono, "There were no facts, nothing deleted by #{@term} #{tail}")
185
189
  elsif r.zero?
186
- @tube.say(start, "No facts out of #{before} deleted by #{@term} #{tail}")
190
+ @tube.say(mono, "No facts out of #{before} deleted by #{@term} #{tail}")
187
191
  else
188
- @tube.say(start, "Deleted #{r} fact(s) out of #{before} by #{@term} #{tail}")
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 #{start.ago}"
200
+ "in #{Time.now.ago}"
198
201
  end
199
202
  end