factbase 0.17.1 → 0.18.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 91ef3ed499ea15abb77bc39cfed30eb9af704921e86c3c03c7bdd9b13cb44270
4
- data.tar.gz: 3c2e80d00620e0de14ec4be35ff08b28393793c69742dda9bda55c4a5ae01c8f
3
+ metadata.gz: 90b1ab3d82ed371b4de11b7eceb98137b2a7687bc2625e61d4d54a6538849b34
4
+ data.tar.gz: f9fd82d8254d7cfa646a742db6989790791ce157ce53d9e5a310f8f8f8c0db11
5
5
  SHA512:
6
- metadata.gz: 9c6e18a32a5ae773e78672e449cce031a2c7641b1291b9f637ccb28afb6afddc87091955ce06e75c0e35326f4db37b04fac5b827b1cdc203a8b0262c105ca2c3
7
- data.tar.gz: 923207d8494313e1af4fae0ecf3068372a5dde28924840edbe1b6d625127afb7e4eb2cf44c9f86a28d9d7fc650336abce9c9b99cd492b3ff29bcbb4cf214c83e
6
+ metadata.gz: ae0d064e075f304c9fae6f4903c961293d07e186ee6bdf4e180d5f407f5b166f2b3cc345cec612d8184675394a2ee504197d4c13d9ce0f480cffd850d7d10169
7
+ data.tar.gz: b53da29f44f9ad5520bed3602c9c7dadf882115856ddb344dfba95a32017cd2e89c4582a7a160baa2ed611ff37acd0ba39e8fb9a8aa8ed7bec62f0acf9f0ee70
data/Gemfile CHANGED
@@ -12,7 +12,7 @@ gem 'minitest-reporters', '~>1.7', require: false
12
12
  gem 'os', '~>1.1', require: false
13
13
  gem 'qbash', '~>0.4', require: false
14
14
  gem 'rake', '~>13.2', require: false
15
- gem 'rdoc', '6.16.1', require: false # GPL
15
+ gem 'rdoc', '6.17.0', require: false # GPL
16
16
  gem 'rubocop', '~>1.74', require: false
17
17
  gem 'rubocop-minitest', '~>0.38', require: false
18
18
  gem 'rubocop-performance', '~>1.25', require: false
data/Gemfile.lock CHANGED
@@ -68,7 +68,7 @@ GEM
68
68
  racc (1.8.1)
69
69
  rainbow (3.1.1)
70
70
  rake (13.3.1)
71
- rdoc (6.16.1)
71
+ rdoc (6.17.0)
72
72
  erb
73
73
  psych (>= 4.0.0)
74
74
  tsort
@@ -140,7 +140,7 @@ DEPENDENCIES
140
140
  os (~> 1.1)
141
141
  qbash (~> 0.4)
142
142
  rake (~> 13.2)
143
- rdoc (= 6.16.1)
143
+ rdoc (= 6.17.0)
144
144
  rubocop (~> 1.74)
145
145
  rubocop-minitest (~> 0.38)
146
146
  rubocop-performance (~> 1.25)
@@ -0,0 +1,141 @@
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 'taped'
8
+ require_relative 'lazy_taped_hash'
9
+
10
+ # A lazy decorator of an Array with HashMaps that defers copying until modification.
11
+ class Factbase::LazyTaped
12
+ def initialize(origin)
13
+ @origin = origin
14
+ @copied = false
15
+ @maps = nil
16
+ @pairs = nil
17
+ @inserted = []
18
+ @deleted = []
19
+ @added = []
20
+ end
21
+
22
+ # Returns a hash mapping copied maps to their originals.
23
+ # This is used during transaction commit to identify which original facts
24
+ # were modified, allowing the factbase to update the correct entries.
25
+ def pairs
26
+ return {} unless @pairs
27
+ result = {}.compare_by_identity
28
+ @pairs.each { |copied, original| result[copied] = original }
29
+ result
30
+ end
31
+
32
+ # Returns the unique object IDs of maps that were inserted (newly created).
33
+ # This is used during transaction commit to identify new facts that need
34
+ # to be added to the factbase.
35
+ def inserted
36
+ @inserted.uniq
37
+ end
38
+
39
+ # Returns the unique object IDs of maps that were deleted.
40
+ # This is used during transaction commit to identify facts that need
41
+ # to be removed from the factbase.
42
+ def deleted
43
+ @deleted.uniq
44
+ end
45
+
46
+ # Returns the unique object IDs of maps that were modified (properties added).
47
+ # This is used during transaction commit to track the churn (number of changes).
48
+ def added
49
+ @added.uniq
50
+ end
51
+
52
+ def find_by_object_id(oid)
53
+ (@maps || @origin).find { |m| m.object_id == oid }
54
+ end
55
+
56
+ def size
57
+ (@maps || @origin).size
58
+ end
59
+
60
+ def empty?
61
+ (@maps || @origin).empty?
62
+ end
63
+
64
+ def <<(map)
65
+ ensure_copied
66
+ @maps << map
67
+ @inserted.append(map.object_id)
68
+ end
69
+
70
+ def each
71
+ return to_enum(__method__) unless block_given?
72
+ if @copied
73
+ @maps.each do |m|
74
+ yield Factbase::Taped::TapedHash.new(m, @added)
75
+ end
76
+ else
77
+ @origin.each do |m|
78
+ yield LazyTapedHash.new(m, self, @added)
79
+ end
80
+ end
81
+ end
82
+
83
+ def delete_if
84
+ ensure_copied
85
+ @maps.delete_if do |m|
86
+ r = yield m
87
+ @deleted.append(@pairs[m].object_id) if r
88
+ r
89
+ end
90
+ end
91
+
92
+ def to_a
93
+ (@maps || @origin).to_a
94
+ end
95
+
96
+ def &(other)
97
+ if other == [] || (@maps || @origin).empty?
98
+ return Factbase::Taped.new([], inserted: @inserted, deleted: @deleted, added: @added)
99
+ end
100
+ join(other, &:&)
101
+ end
102
+
103
+ def |(other)
104
+ return Factbase::Taped.new(to_a, inserted: @inserted, deleted: @deleted, added: @added) if other == []
105
+ if (@maps || @origin).empty?
106
+ return Factbase::Taped.new(other, inserted: @inserted, deleted: @deleted, added: @added)
107
+ end
108
+ join(other, &:|)
109
+ end
110
+
111
+ def ensure_copied
112
+ return if @copied
113
+ @pairs = {}.compare_by_identity
114
+ @maps =
115
+ @origin.map do |m|
116
+ n = m.transform_values(&:dup)
117
+ @pairs[n] = m
118
+ n
119
+ end
120
+ @copied = true
121
+ end
122
+
123
+ def get_copied_map(original_map)
124
+ ensure_copied
125
+ @maps.find { |m| @pairs[m].equal?(original_map) }
126
+ end
127
+
128
+ private
129
+
130
+ def join(other)
131
+ n = yield (@maps || @origin).to_a, other.to_a
132
+ raise 'Cannot join with another Taped' if other.respond_to?(:inserted)
133
+ raise 'Can only join with array' unless other.is_a?(Array)
134
+ Factbase::Taped.new(
135
+ n,
136
+ inserted: @inserted,
137
+ deleted: @deleted,
138
+ added: @added
139
+ )
140
+ end
141
+ end
@@ -0,0 +1,62 @@
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
+ class Factbase::LazyTaped
9
+ # Decorator of Array that triggers copy-on-write.
10
+ # @todo #424:30min Add dedicated unit tests for LazyTapedArray class.
11
+ # Currently this class is tested indirectly through LazyTaped tests.
12
+ # We should add explicit tests for all public methods including each, [],
13
+ # to_a, any?, <<, and uniq! to ensure proper copy-on-write behavior.
14
+ class LazyTapedArray
15
+ # Creates a new lazy array wrapper.
16
+ # @param origin [Array] The original array to wrap
17
+ # @param key [String] The key in the parent hash where this array is stored
18
+ # @param taped_hash [LazyTapedHash] The parent hash wrapper that owns this array
19
+ # @param added [Array] Accumulator for tracking object IDs of modified facts
20
+ def initialize(origin, key, taped_hash, added)
21
+ @origin = origin
22
+ @key = key
23
+ @taped_hash = taped_hash
24
+ @added = added
25
+ end
26
+
27
+ def each(&)
28
+ return to_enum(__method__) unless block_given?
29
+ current_array.each(&)
30
+ end
31
+
32
+ def [](idx)
33
+ current_array[idx]
34
+ end
35
+
36
+ def to_a
37
+ current_array.to_a
38
+ end
39
+
40
+ def any?(pattern = nil, &)
41
+ pattern ? current_array.any?(pattern) : current_array.any?(&)
42
+ end
43
+
44
+ def <<(item)
45
+ @taped_hash.ensure_copied_map
46
+ @added.append(@taped_hash.tracking_id)
47
+ @taped_hash.get_copied_array(@key) << item
48
+ end
49
+
50
+ def uniq!
51
+ @taped_hash.ensure_copied_map
52
+ @added.append(@taped_hash.tracking_id)
53
+ @taped_hash.get_copied_array(@key).uniq!
54
+ end
55
+
56
+ private
57
+
58
+ def current_array
59
+ @taped_hash.copied? ? @taped_hash.get_copied_array(@key) : @origin
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,79 @@
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 'lazy_taped_array'
8
+
9
+ class Factbase::LazyTaped
10
+ # Decorator of Hash that triggers copy-on-write.
11
+ # @todo #424:30min Add dedicated unit tests for LazyTapedHash class.
12
+ # Currently this class is tested indirectly through LazyTaped tests.
13
+ # We should add explicit tests for all public methods including keys, map,
14
+ # bracket access, bracket assignment, and the copy-on-write behavior.
15
+ class LazyTapedHash
16
+ # Creates a new LazyTapedHash decorator.
17
+ # @param origin [Hash] The original hash being wrapped (not yet copied)
18
+ # @param lazy_taped [Factbase::LazyTaped] The parent LazyTaped instance that manages copy-on-write
19
+ # @param added [Array] Array to track object IDs of maps that have been modified
20
+ def initialize(origin, lazy_taped, added)
21
+ @origin = origin
22
+ @lazy_taped = lazy_taped
23
+ @added = added
24
+ @copied_map = nil
25
+ end
26
+
27
+ def keys
28
+ current_map.keys
29
+ end
30
+
31
+ def map(&)
32
+ current_map.map(&)
33
+ end
34
+
35
+ def [](key)
36
+ v = current_map[key]
37
+ v = LazyTapedArray.new(v, key, self, @added) if v.is_a?(Array)
38
+ v
39
+ end
40
+
41
+ def []=(key, value)
42
+ ensure_copied_map
43
+ @copied_map[key] = value
44
+ @added.append(@copied_map.object_id)
45
+ end
46
+
47
+ def ensure_copied_map
48
+ return if @copied_map
49
+ @copied_map = @lazy_taped.get_copied_map(@origin)
50
+ end
51
+
52
+ def get_copied_array(key)
53
+ ensure_copied_map
54
+ @copied_map[key]
55
+ end
56
+
57
+ def tracking_id
58
+ @copied_map ? @copied_map.object_id : @origin.object_id
59
+ end
60
+
61
+ def copied?
62
+ !@copied_map.nil?
63
+ end
64
+
65
+ private
66
+
67
+ def current_map
68
+ @copied_map || @origin
69
+ end
70
+
71
+ def method_missing(method, *, &)
72
+ current_map.send(method, *, &)
73
+ end
74
+
75
+ def respond_to_missing?(method, include_private = false)
76
+ current_map.respond_to?(method, include_private)
77
+ end
78
+ end
79
+ end
data/lib/factbase/term.rb CHANGED
@@ -86,7 +86,7 @@ require_relative 'terms/max'
86
86
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
87
87
  # Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
88
88
  # License:: MIT
89
- class Factbase::Term
89
+ class Factbase::Term < Factbase::TermBase
90
90
  # The operator of this term
91
91
  # @return [Symbol] The operator
92
92
  attr_reader :op
@@ -95,13 +95,11 @@ class Factbase::Term
95
95
  # @return [Array] The operands
96
96
  attr_reader :operands
97
97
 
98
- require_relative 'terms/shared'
99
- include Factbase::TermShared
100
-
101
98
  # Ctor.
102
99
  # @param [Symbol] operator Operator
103
100
  # @param [Array] operands Operands
104
101
  def initialize(operator, operands)
102
+ super()
105
103
  @op = operator
106
104
  @operands = operands
107
105
  @terms = {
@@ -26,7 +26,7 @@ class Factbase::Agg < Factbase::TermBase
26
26
  raise "A term is expected, but '#{selector}' provided"
27
27
  end
28
28
  term = @operands[1]
29
- unless term.is_a?(Factbase::Term) || selector.is_a?(Factbase::TermBase)
29
+ unless term.is_a?(Factbase::Term) || term.is_a?(Factbase::TermBase)
30
30
  raise "A term is expected, but '#{term}' provided"
31
31
  end
32
32
  subset = fb.query(selector, maps).each(fb, fact).to_a
@@ -10,8 +10,61 @@
10
10
 
11
11
  # Base class for all terms.
12
12
  class Factbase::TermBase
13
- require_relative 'shared'
14
- include Factbase::TermShared
13
+ # Turns it into a string.
14
+ # @return [String] The string of it
15
+ def to_s
16
+ @to_s ||=
17
+ begin
18
+ items = []
19
+ items << @op
20
+ items +=
21
+ @operands.map do |o|
22
+ if o.is_a?(String)
23
+ "'#{o.gsub("'", "\\\\'").gsub('"', '\\\\"')}'"
24
+ elsif o.is_a?(Time)
25
+ o.utc.iso8601
26
+ else
27
+ o.to_s
28
+ end
29
+ end
30
+ "(#{items.join(' ')})"
31
+ end
32
+ end
15
33
 
16
- protected :assert_args, :_by_symbol, :_values, :to_s
34
+ private
35
+
36
+ def assert_args(num)
37
+ c = @operands.size
38
+ raise "Too many (#{c}) operands for '#{@op}' (#{num} expected)" if c > num
39
+ raise "Too few (#{c}) operands for '#{@op}' (#{num} expected)" if c < num
40
+ end
41
+
42
+ def _by_symbol(pos, fact)
43
+ o = @operands[pos]
44
+ raise "A symbol expected at ##{pos}, but '#{o}' (#{o.class}) provided" unless o.is_a?(Symbol)
45
+ k = o.to_s
46
+ fact[k]
47
+ end
48
+
49
+ # @return [Array|nil] Either array of values or NIL
50
+ def _values(pos, fact, maps, fb)
51
+ v = @operands[pos]
52
+ v = v.evaluate(fact, maps, fb) if v.is_a?(Factbase::Term)
53
+ v = v.evaluate(fact, maps, fb) if v.is_a?(Factbase::TermBase)
54
+ v = fact[v.to_s] if v.is_a?(Symbol)
55
+ return v if v.nil?
56
+ unless v.is_a?(Array)
57
+ v =
58
+ if v.respond_to?(:each)
59
+ v.to_a
60
+ else
61
+ [v]
62
+ end
63
+ end
64
+ raise 'Why not array?' unless v.is_a?(Array)
65
+ unless v.all? { |i| [Float, Integer, String, Time, TrueClass, FalseClass].any? { |t| i.is_a?(t) } }
66
+ raise 'Wrong type inside'
67
+ end
68
+ v
69
+ end
17
70
  end
@@ -9,5 +9,5 @@
9
9
  # License:: MIT
10
10
  class Factbase
11
11
  # Current version of the gem (changed by .rultor.yml on every release)
12
- VERSION = '0.17.1' unless const_defined?(:VERSION)
12
+ VERSION = '0.18.0' unless const_defined?(:VERSION)
13
13
  end
data/lib/factbase.rb CHANGED
@@ -164,17 +164,8 @@ class Factbase
164
164
  #
165
165
  # @return [Factbase::Churn] How many facts have been changed (zero if rolled back)
166
166
  def txn
167
- pairs = {}
168
- before =
169
- @maps.map do |m|
170
- n = m.transform_values(&:dup)
171
- # rubocop:disable Lint/HashCompareByIdentity
172
- pairs[n.object_id] = m.object_id
173
- # rubocop:enable Lint/HashCompareByIdentity
174
- n
175
- end
176
- require_relative 'factbase/taped'
177
- taped = Factbase::Taped.new(before)
167
+ require_relative 'factbase/lazy_taped'
168
+ taped = Factbase::LazyTaped.new(@maps)
178
169
  require_relative 'factbase/churn'
179
170
  churn = Factbase::Churn.new
180
171
  catch :commit do
@@ -188,30 +179,32 @@ class Factbase
188
179
  rescue Factbase::Rollback
189
180
  return churn
190
181
  end
191
- seen = []
192
- garbage = []
182
+ seen = {}.compare_by_identity
183
+ garbage = {}.compare_by_identity
184
+ pairs = taped.pairs
193
185
  taped.deleted.each do |oid|
194
- garbage << pairs[oid]
195
- seen << oid
186
+ original = @maps.find { |m| m.object_id == oid }
187
+ next if original.nil?
188
+ garbage[original] = true
196
189
  churn.append(0, 1, 0)
197
190
  end
198
191
  taped.inserted.each do |oid|
199
- next if seen.include?(oid)
200
192
  b = taped.find_by_object_id(oid)
201
193
  next if b.nil?
202
- seen << oid
194
+ next if seen.key?(b)
195
+ seen[b] = true
203
196
  @maps << b
204
197
  churn.append(1, 0, 0)
205
198
  end
206
199
  taped.added.each do |oid|
207
- next if seen.include?(oid)
208
200
  b = taped.find_by_object_id(oid)
209
201
  next if b.nil?
210
- garbage << pairs[oid]
202
+ next if seen.key?(b)
203
+ garbage[pairs[b]] = true
211
204
  @maps << b
212
205
  churn.append(0, 0, 1)
213
206
  end
214
- @maps.delete_if { |m| garbage.include?(m.object_id) } unless garbage.empty?
207
+ @maps.delete_if { |m| garbage.key?(m) } unless garbage.empty?
215
208
  churn
216
209
  end
217
210
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: factbase
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.17.1
4
+ version: 0.18.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yegor Bugayenko
@@ -195,6 +195,9 @@ files:
195
195
  - lib/factbase/indexed/indexed_term.rb
196
196
  - lib/factbase/indexed/indexed_unique.rb
197
197
  - lib/factbase/inv.rb
198
+ - lib/factbase/lazy_taped.rb
199
+ - lib/factbase/lazy_taped_array.rb
200
+ - lib/factbase/lazy_taped_hash.rb
198
201
  - lib/factbase/light.rb
199
202
  - lib/factbase/logged.rb
200
203
  - lib/factbase/pre.rb
@@ -248,7 +251,6 @@ files:
248
251
  - lib/factbase/terms/or.rb
249
252
  - lib/factbase/terms/plus.rb
250
253
  - lib/factbase/terms/prev.rb
251
- - lib/factbase/terms/shared.rb
252
254
  - lib/factbase/terms/simplified.rb
253
255
  - lib/factbase/terms/size.rb
254
256
  - lib/factbase/terms/sorted.rb
@@ -1,69 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # SPDX-FileCopyrightText: Copyright (c) 2024-2025 Yegor Bugayenko
4
- # SPDX-License-Identifier: MIT
5
-
6
- # This module provides shared methods for Factbase terms, including argument validation,
7
- # symbol-based lookups, and handling of operand values.
8
- # @todo #302:30min Remove this module and move its methods to Factbase::TermBase.
9
- # Currently, we use it because we are required to inject all thesse methods into Factbase::Term.
10
- # When all the terms will inherit from Factbase::TermBase, we can remove this module.
11
- module Factbase::TermShared
12
- # Turns it into a string.
13
- # @return [String] The string of it
14
- def to_s
15
- @to_s ||=
16
- begin
17
- items = []
18
- items << @op
19
- items +=
20
- @operands.map do |o|
21
- if o.is_a?(String)
22
- "'#{o.gsub("'", "\\\\'").gsub('"', '\\\\"')}'"
23
- elsif o.is_a?(Time)
24
- o.utc.iso8601
25
- else
26
- o.to_s
27
- end
28
- end
29
- "(#{items.join(' ')})"
30
- end
31
- end
32
-
33
- private
34
-
35
- def assert_args(num)
36
- c = @operands.size
37
- raise "Too many (#{c}) operands for '#{@op}' (#{num} expected)" if c > num
38
- raise "Too few (#{c}) operands for '#{@op}' (#{num} expected)" if c < num
39
- end
40
-
41
- def _by_symbol(pos, fact)
42
- o = @operands[pos]
43
- raise "A symbol expected at ##{pos}, but '#{o}' (#{o.class}) provided" unless o.is_a?(Symbol)
44
- k = o.to_s
45
- fact[k]
46
- end
47
-
48
- # @return [Array|nil] Either array of values or NIL
49
- def _values(pos, fact, maps, fb)
50
- v = @operands[pos]
51
- v = v.evaluate(fact, maps, fb) if v.is_a?(Factbase::Term)
52
- v = v.evaluate(fact, maps, fb) if v.is_a?(Factbase::TermBase)
53
- v = fact[v.to_s] if v.is_a?(Symbol)
54
- return v if v.nil?
55
- unless v.is_a?(Array)
56
- v =
57
- if v.respond_to?(:each)
58
- v.to_a
59
- else
60
- [v]
61
- end
62
- end
63
- raise 'Why not array?' unless v.is_a?(Array)
64
- unless v.all? { |i| [Float, Integer, String, Time, TrueClass, FalseClass].any? { |t| i.is_a?(t) } }
65
- raise 'Wrong type inside'
66
- end
67
- v
68
- end
69
- end