factbase 0.16.8 → 0.17.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.
Files changed (84) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +2 -1
  3. data/Gemfile.lock +23 -18
  4. data/README.md +28 -27
  5. data/Rakefile +14 -6
  6. data/lib/factbase/cached/cached_query.rb +2 -0
  7. data/lib/factbase/indexed/indexed_absent.rb +25 -0
  8. data/lib/factbase/indexed/indexed_and.rb +79 -0
  9. data/lib/factbase/indexed/indexed_eq.rb +46 -0
  10. data/lib/factbase/indexed/indexed_exists.rb +25 -0
  11. data/lib/factbase/indexed/indexed_factbase.rb +46 -0
  12. data/lib/factbase/indexed/indexed_gt.rb +41 -0
  13. data/lib/factbase/indexed/indexed_lt.rb +41 -0
  14. data/lib/factbase/indexed/indexed_not.rb +32 -0
  15. data/lib/factbase/indexed/indexed_one.rb +25 -0
  16. data/lib/factbase/indexed/indexed_or.rb +28 -0
  17. data/lib/factbase/indexed/indexed_query.rb +2 -0
  18. data/lib/factbase/indexed/indexed_term.rb +29 -185
  19. data/lib/factbase/indexed/indexed_unique.rb +25 -0
  20. data/lib/factbase/query.rb +3 -1
  21. data/lib/factbase/sync/sync_factbase.rb +11 -11
  22. data/lib/factbase/sync/sync_query.rb +7 -8
  23. data/lib/factbase/term.rb +110 -91
  24. data/lib/factbase/terms/absent.rb +26 -0
  25. data/lib/factbase/terms/aggregates.rb +0 -13
  26. data/lib/factbase/terms/always.rb +27 -0
  27. data/lib/factbase/terms/and.rb +28 -0
  28. data/lib/factbase/terms/arithmetic.rb +55 -0
  29. data/lib/factbase/terms/as.rb +31 -0
  30. data/lib/factbase/terms/{debug.rb → assert.rb} +17 -15
  31. data/lib/factbase/terms/base.rb +17 -0
  32. data/lib/factbase/terms/boolean.rb +28 -0
  33. data/lib/factbase/terms/compare.rb +38 -0
  34. data/lib/factbase/terms/concat.rb +26 -0
  35. data/lib/factbase/terms/count.rb +25 -0
  36. data/lib/factbase/terms/defn.rb +16 -15
  37. data/lib/factbase/terms/div.rb +25 -0
  38. data/lib/factbase/terms/either.rb +31 -0
  39. data/lib/factbase/terms/env.rb +28 -0
  40. data/lib/factbase/terms/eq.rb +28 -0
  41. data/lib/factbase/terms/exists.rb +27 -0
  42. data/lib/factbase/terms/first.rb +30 -0
  43. data/lib/factbase/terms/gt.rb +28 -0
  44. data/lib/factbase/terms/gte.rb +27 -0
  45. data/lib/factbase/terms/head.rb +37 -0
  46. data/lib/factbase/terms/inverted.rb +34 -0
  47. data/lib/factbase/terms/{aliases.rb → join.rb} +15 -15
  48. data/lib/factbase/terms/logical.rb +0 -83
  49. data/lib/factbase/terms/lt.rb +28 -0
  50. data/lib/factbase/terms/lte.rb +28 -0
  51. data/lib/factbase/terms/many.rb +29 -0
  52. data/lib/factbase/terms/{strings.rb → matches.rb} +12 -12
  53. data/lib/factbase/terms/minus.rb +25 -0
  54. data/lib/factbase/terms/never.rb +26 -0
  55. data/lib/factbase/terms/nil.rb +26 -0
  56. data/lib/factbase/terms/not.rb +27 -0
  57. data/lib/factbase/terms/one.rb +30 -0
  58. data/lib/factbase/terms/or.rb +28 -0
  59. data/lib/factbase/terms/plus.rb +27 -0
  60. data/lib/factbase/terms/prev.rb +29 -0
  61. data/lib/factbase/terms/shared.rb +69 -0
  62. data/lib/factbase/terms/size.rb +30 -0
  63. data/lib/factbase/terms/sorted.rb +38 -0
  64. data/lib/factbase/terms/sprintf.rb +29 -0
  65. data/lib/factbase/terms/times.rb +25 -0
  66. data/lib/factbase/terms/to_float.rb +28 -0
  67. data/lib/factbase/terms/to_integer.rb +28 -0
  68. data/lib/factbase/terms/to_string.rb +28 -0
  69. data/lib/factbase/terms/to_time.rb +28 -0
  70. data/lib/factbase/terms/traced.rb +33 -0
  71. data/lib/factbase/terms/type.rb +31 -0
  72. data/lib/factbase/terms/undef.rb +33 -0
  73. data/lib/factbase/terms/unique.rb +34 -0
  74. data/lib/factbase/terms/when.rb +29 -0
  75. data/lib/factbase/terms/zero.rb +28 -0
  76. data/lib/factbase/version.rb +1 -1
  77. data/lib/factbase.rb +10 -1
  78. metadata +60 -10
  79. data/lib/factbase/terms/casting.rb +0 -41
  80. data/lib/factbase/terms/lists.rb +0 -57
  81. data/lib/factbase/terms/math.rb +0 -103
  82. data/lib/factbase/terms/meta.rb +0 -58
  83. data/lib/factbase/terms/ordering.rb +0 -34
  84. data/lib/factbase/terms/system.rb +0 -19
@@ -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
+ # Indexed term 'or'.
7
+ class Factbase::IndexedOr
8
+ def initialize(term, idx)
9
+ @term = term
10
+ @idx = idx
11
+ end
12
+
13
+ def predict(maps, fb, params)
14
+ return nil if @idx.nil?
15
+ r = nil
16
+ @term.operands.each do |o|
17
+ n = o.predict(maps, fb, params)
18
+ if n.nil?
19
+ r = nil
20
+ break
21
+ end
22
+ r = maps & [] if r.nil?
23
+ r |= n.to_a
24
+ return maps if r.size > maps.size / 4 # it's big enough already
25
+ end
26
+ r
27
+ end
28
+ end
@@ -12,6 +12,8 @@ require_relative 'indexed_fact'
12
12
  # Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
13
13
  # License:: MIT
14
14
  class Factbase::IndexedQuery
15
+ include Enumerable
16
+
15
17
  # Constructor.
16
18
  # @param [Factbase::Query] origin Original query
17
19
  # @param [Hash] idx The index
@@ -5,6 +5,16 @@
5
5
 
6
6
  require 'tago'
7
7
  require_relative '../../factbase'
8
+ require_relative '../indexed/indexed_eq'
9
+ require_relative '../indexed/indexed_lt'
10
+ require_relative '../indexed/indexed_gt'
11
+ require_relative '../indexed/indexed_one'
12
+ require_relative '../indexed/indexed_not'
13
+ require_relative '../indexed/indexed_exists'
14
+ require_relative '../indexed/indexed_and'
15
+ require_relative '../indexed/indexed_or'
16
+ require_relative '../indexed/indexed_absent'
17
+ require_relative '../indexed/indexed_unique'
8
18
 
9
19
  # Term with an index.
10
20
  #
@@ -20,196 +30,30 @@ module Factbase::IndexedTerm
20
30
  # @param [Hash] params Key/value params to use
21
31
  # @return [Array<Hash>|nil] Returns a new array, or NIL if the original array must be used
22
32
  def predict(maps, fb, params)
33
+ if @terms.key?(@op)
34
+ t = @terms[@op]
35
+ return t.predict(maps, fb, params) if t.respond_to?(:predict)
36
+ end
23
37
  m = :"#{@op}_predict"
24
38
  return send(m, maps, fb, params) if respond_to?(m)
25
- key = [maps.object_id, @operands.first, @op]
26
- case @op
27
- when :one
28
- if @idx[key].nil?
29
- @idx[key] = []
30
- prop = @operands.first.to_s
31
- maps.to_a.each do |m|
32
- @idx[key].append(m) if !m[prop].nil? && m[prop].size == 1
33
- end
34
- end
35
- (maps & []) | @idx[key]
36
- when :exists
37
- if @idx[key].nil?
38
- @idx[key] = []
39
- prop = @operands.first.to_s
40
- maps.to_a.each do |m|
41
- @idx[key].append(m) unless m[prop].nil?
42
- end
43
- end
44
- (maps & []) | @idx[key]
45
- when :absent
46
- if @idx[key].nil?
47
- @idx[key] = []
48
- prop = @operands.first.to_s
49
- maps.to_a.each do |m|
50
- @idx[key].append(m) if m[prop].nil?
51
- end
52
- end
53
- (maps & []) | @idx[key]
54
- when :eq
55
- if @operands.first.is_a?(Symbol) && _scalar?(@operands[1])
56
- if @idx[key].nil?
57
- @idx[key] = {}
58
- prop = @operands.first.to_s
59
- maps.to_a.each do |m|
60
- m[prop]&.each do |v|
61
- @idx[key][v] = [] if @idx[key][v].nil?
62
- @idx[key][v].append(m)
63
- end
64
- end
65
- end
66
- vv =
67
- if @operands[1].is_a?(Symbol)
68
- params[@operands[1].to_s] || []
69
- else
70
- [@operands[1]]
71
- end
72
- if vv.empty?
73
- (maps & [])
74
- else
75
- j = vv.map { |v| @idx[key][v] || [] }.reduce(&:|)
76
- (maps & []) | j
77
- end
78
- end
79
- when :gt
80
- if @operands.first.is_a?(Symbol) && _scalar?(@operands[1])
81
- prop = @operands.first.to_s
82
- cache_key = [maps.object_id, @operands.first, :sorted]
83
- if @idx[cache_key].nil?
84
- @idx[cache_key] = []
85
- maps.to_a.each do |m|
86
- values = m[prop]
87
- next if values.nil?
88
- values.each do |v|
89
- @idx[cache_key] << [v, m]
90
- end
91
- end
92
- @idx[cache_key].sort_by! { |pair| pair[0] }
93
- end
94
- threshold = @operands[1].is_a?(Symbol) ? params[@operands[1].to_s]&.first : @operands[1]
95
- return nil if threshold.nil?
96
- i = @idx[cache_key].bsearch_index { |pair| pair[0] > threshold } || @idx[cache_key].size
97
- result = @idx[cache_key][i..].map { |pair| pair[1] }.uniq
98
- (maps & []) | result
99
- end
100
- when :lt
101
- if @operands.first.is_a?(Symbol) && _scalar?(@operands[1])
102
- prop = @operands.first.to_s
103
- cache_key = [maps.object_id, @operands.first, :sorted]
104
- if @idx[cache_key].nil?
105
- @idx[cache_key] = []
106
- maps.to_a.each do |m|
107
- values = m[prop]
108
- next if values.nil?
109
- values.each do |v|
110
- @idx[cache_key] << [v, m]
111
- end
112
- end
113
- @idx[cache_key].sort_by! { |pair| pair[0] }
114
- end
115
- threshold = @operands[1].is_a?(Symbol) ? params[@operands[1].to_s]&.first : @operands[1]
116
- return nil if threshold.nil?
117
- i = @idx[cache_key].bsearch_index { |pair| pair[0] >= threshold } || @idx[cache_key].size
118
- result = @idx[cache_key][0...i].map { |pair| pair[1] }.uniq
119
- (maps & []) | result
120
- end
121
- when :and
122
- r = nil
123
- if @operands.all? { |o| o.op == :eq } && @operands.size > 1 \
124
- && @operands.all? { |o| o.operands.first.is_a?(Symbol) && _scalar?(o.operands[1]) }
125
- props = @operands.map { |o| o.operands.first }.sort
126
- key = [maps.object_id, props, :multi_and_eq]
127
- if @idx[key].nil?
128
- @idx[key] = {}
129
- maps.to_a.each do |m|
130
- _all_tuples(m, props).each do |t|
131
- @idx[key][t] = [] if @idx[key][t].nil?
132
- @idx[key][t].append(m)
133
- end
134
- end
135
- end
136
- tuples = Enumerator.product(
137
- *@operands.sort_by { |o| o.operands.first }.map do |o|
138
- if o.operands[1].is_a?(Symbol)
139
- params[o.operands[1].to_s] || []
140
- else
141
- [o.operands[1]]
142
- end
143
- end
144
- )
145
- j = tuples.map { |t| @idx[key][t] || [] }.reduce(&:|)
146
- r = (maps & []) | j
147
- else
148
- @operands.each do |o|
149
- n = o.predict(maps, fb, params)
150
- break if n.nil?
151
- if r.nil?
152
- r = n
153
- elsif n.size < r.size * 8 # to skip some obvious matchings
154
- r &= n.to_a
155
- end
156
- break if r.size < maps.size / 32 # it's already small enough
157
- break if r.size < 128 # it's obviously already small enough
158
- end
159
- end
160
- r
161
- when :or
162
- r = nil
163
- @operands.each do |o|
164
- n = o.predict(maps, fb, params)
165
- if n.nil?
166
- r = nil
167
- break
168
- end
169
- r = maps & [] if r.nil?
170
- r |= n.to_a
171
- return maps if r.size > maps.size / 4 # it's big enough already
172
- end
173
- r
174
- when :not
175
- if @idx[key].nil?
176
- yes = @operands.first.predict(maps, fb, params)
177
- if yes.nil?
178
- @idx[key] = { r: nil }
179
- else
180
- yes = yes.to_a.to_set
181
- @idx[key] = { r: maps.to_a.reject { |m| yes.include?(m) } }
182
- end
183
- end
184
- r = @idx[key][:r]
185
- if r.nil?
186
- nil
187
- else
188
- (maps & []) | r
189
- end
190
- end
39
+ _init_indexes unless @indexes
40
+ @indexes[@op].predict(maps, fb, params) if @indexes.key?(@op)
191
41
  end
192
42
 
193
43
  private
194
44
 
195
- def _all_tuples(fact, props)
196
- prop = props.first.to_s
197
- tuples = []
198
- tuples += (fact[prop] || []).zip
199
- if props.size > 1
200
- tails = _all_tuples(fact, props[1..])
201
- ext = []
202
- tuples.each do |t|
203
- tails.each do |tail|
204
- ext << (t + tail)
205
- end
206
- end
207
- tuples = ext
208
- end
209
- tuples
210
- end
211
-
212
- def _scalar?(item)
213
- item.is_a?(String) || item.is_a?(Time) || item.is_a?(Integer) || item.is_a?(Float) || item.is_a?(Symbol)
45
+ def _init_indexes
46
+ @indexes = {
47
+ eq: Factbase::IndexedEq.new(self, @idx),
48
+ lt: Factbase::IndexedLt.new(self, @idx),
49
+ gt: Factbase::IndexedGt.new(self, @idx),
50
+ one: Factbase::IndexedOne.new(self, @idx),
51
+ exists: Factbase::IndexedExists.new(self, @idx),
52
+ absent: Factbase::IndexedAbsent.new(self, @idx),
53
+ unique: Factbase::IndexedUnique.new(self, @idx),
54
+ and: Factbase::IndexedAnd.new(self, @idx),
55
+ not: Factbase::IndexedNot.new(self, @idx),
56
+ or: Factbase::IndexedOr.new(self, @idx)
57
+ }
214
58
  end
215
59
  end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2024-2025 Yegor Bugayenko
4
+ # SPDX-License-Identifier: MIT
5
+
6
+ # Indexed term 'unique'.
7
+ # @todo #249:30min Improve prediction for 'unique' term. Current prediction is quite naive and
8
+ # returns many false positives because it just filters facts which have exactly the same set
9
+ # of keys regardless the values. We should introduce more smart prediction.
10
+ class Factbase::IndexedUnique
11
+ def initialize(term, idx)
12
+ @term = term
13
+ @idx = idx
14
+ end
15
+
16
+ def predict(maps, _fb, _params)
17
+ return nil if @idx.nil?
18
+ key = [maps.object_id, @term.operands.first, @term.op]
19
+ if @idx[key].nil?
20
+ props = @term.operands.map(&:to_s)
21
+ @idx[key] = maps.to_a.select { |m| props.all? { |p| !m[p].nil? } }
22
+ end
23
+ (maps & []) | @idx[key]
24
+ end
25
+ end
@@ -20,6 +20,8 @@ require_relative 'tee'
20
20
  # Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
21
21
  # License:: MIT
22
22
  class Factbase::Query
23
+ include Enumerable
24
+
23
25
  # Constructor.
24
26
  # @param [Array<Fact>] maps Array of facts to start with
25
27
  # @param [String|Factbase::Term] term The query term
@@ -77,7 +79,7 @@ class Factbase::Query
77
79
 
78
80
  # Delete all facts that match the query.
79
81
  # @param [Factbase] fb The factbase to delete from
80
- # @param [id] String The id of facts that uniquely identify them
82
+ # @param [String] id The id of facts that uniquely identify them
81
83
  # @return [Integer] Total number of facts deleted
82
84
  def delete!(fb = @fb, id: '_id')
83
85
  deleted = 0
@@ -4,6 +4,7 @@
4
4
  # SPDX-License-Identifier: MIT
5
5
 
6
6
  require 'decoor'
7
+ require 'monitor'
7
8
  require_relative '../../factbase'
8
9
 
9
10
  # A synchronous thread-safe factbase.
@@ -16,10 +17,10 @@ class Factbase::SyncFactbase
16
17
 
17
18
  # Constructor.
18
19
  # @param [Factbase] origin Original factbase to decorate
19
- # @param [Mutex] mutex Mutex to use for synchronization
20
- def initialize(origin, mutex = Mutex.new)
20
+ # @param [Monitor] monitor Monitor to use for synchronization
21
+ def initialize(origin, monitor = Monitor.new)
21
22
  @origin = origin
22
- @mutex = mutex
23
+ @monitor = monitor
23
24
  end
24
25
 
25
26
  # Insert a new fact and return it.
@@ -43,24 +44,23 @@ class Factbase::SyncFactbase
43
44
  def query(term, maps = nil)
44
45
  term = to_term(term) if term.is_a?(String)
45
46
  require_relative 'sync_query'
46
- Factbase::SyncQuery.new(@origin.query(term, maps), @mutex, self)
47
+ Factbase::SyncQuery.new(@origin.query(term, maps), @monitor, self)
47
48
  end
48
49
 
49
50
  # Run an ACID transaction.
50
51
  # @return [Factbase::Churn] How many facts have been changed (zero if rolled back)
51
52
  # @yield [Factbase] Block to execute in transaction
52
53
  def txn
53
- @origin.txn do |fbt|
54
- yield Factbase::SyncFactbase.new(fbt, @mutex)
54
+ try_lock do
55
+ @origin.txn do |fbt|
56
+ yield Factbase::SyncFactbase.new(fbt, @monitor)
57
+ end
55
58
  end
56
59
  end
57
60
 
58
61
  private
59
62
 
60
- def try_lock
61
- locked = @mutex.try_lock
62
- r = yield
63
- @mutex.unlock if locked
64
- r
63
+ def try_lock(&)
64
+ @monitor.synchronize(&)
65
65
  end
66
66
  end
@@ -11,12 +11,14 @@ require_relative '../../factbase'
11
11
  # Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
12
12
  # License:: MIT
13
13
  class Factbase::SyncQuery
14
+ include Enumerable
15
+
14
16
  # Constructor.
15
17
  # @param [Factbase::Query] origin Original query
16
- # @param [Mutex] mutex The mutex
17
- def initialize(origin, mutex, fb)
18
+ # @param [Monitor] monitor The monitor
19
+ def initialize(origin, monitor, fb)
18
20
  @origin = origin
19
- @mutex = mutex
21
+ @monitor = monitor
20
22
  @fb = fb
21
23
  end
22
24
 
@@ -57,10 +59,7 @@ class Factbase::SyncQuery
57
59
 
58
60
  private
59
61
 
60
- def try_lock
61
- locked = @mutex.try_lock
62
- r = yield
63
- @mutex.unlock if locked
64
- r
62
+ def try_lock(&)
63
+ @monitor.synchronize(&)
65
64
  end
66
65
  end
data/lib/factbase/term.rb CHANGED
@@ -7,6 +7,51 @@ require 'backtrace'
7
7
  require_relative '../factbase'
8
8
  require_relative 'fact'
9
9
  require_relative 'tee'
10
+ require_relative 'terms/unique'
11
+ require_relative 'terms/prev'
12
+ require_relative 'terms/concat'
13
+ require_relative 'terms/sprintf'
14
+ require_relative 'terms/matches'
15
+ require_relative 'terms/traced'
16
+ require_relative 'terms/assert'
17
+ require_relative 'terms/env'
18
+ require_relative 'terms/defn'
19
+ require_relative 'terms/undef'
20
+ require_relative 'terms/as'
21
+ require_relative 'terms/join'
22
+ require_relative 'terms/exists'
23
+ require_relative 'terms/absent'
24
+ require_relative 'terms/size'
25
+ require_relative 'terms/type'
26
+ require_relative 'terms/nil'
27
+ require_relative 'terms/many'
28
+ require_relative 'terms/one'
29
+ require_relative 'terms/to_string'
30
+ require_relative 'terms/to_integer'
31
+ require_relative 'terms/to_float'
32
+ require_relative 'terms/to_time'
33
+ require_relative 'terms/sorted'
34
+ require_relative 'terms/inverted'
35
+ require_relative 'terms/head'
36
+ require_relative 'terms/plus'
37
+ require_relative 'terms/minus'
38
+ require_relative 'terms/times'
39
+ require_relative 'terms/div'
40
+ require_relative 'terms/zero'
41
+ require_relative 'terms/eq'
42
+ require_relative 'terms/lt'
43
+ require_relative 'terms/lte'
44
+ require_relative 'terms/gt'
45
+ require_relative 'terms/gte'
46
+ require_relative 'terms/always'
47
+ require_relative 'terms/never'
48
+ require_relative 'terms/not'
49
+ require_relative 'terms/or'
50
+ require_relative 'terms/and'
51
+ require_relative 'terms/when'
52
+ require_relative 'terms/either'
53
+ require_relative 'terms/count'
54
+ require_relative 'terms/first'
10
55
 
11
56
  # Term.
12
57
  #
@@ -44,41 +89,14 @@ class Factbase::Term
44
89
  # @return [Array] The operands
45
90
  attr_reader :operands
46
91
 
47
- require_relative 'terms/math'
48
- include Factbase::Math
49
-
50
92
  require_relative 'terms/logical'
51
93
  include Factbase::Logical
52
94
 
53
95
  require_relative 'terms/aggregates'
54
96
  include Factbase::Aggregates
55
97
 
56
- require_relative 'terms/lists'
57
- include Factbase::Lists
58
-
59
- require_relative 'terms/strings'
60
- include Factbase::Strings
61
-
62
- require_relative 'terms/casting'
63
- include Factbase::Casting
64
-
65
- require_relative 'terms/meta'
66
- include Factbase::Meta
67
-
68
- require_relative 'terms/aliases'
69
- include Factbase::Aliases
70
-
71
- require_relative 'terms/ordering'
72
- include Factbase::Ordering
73
-
74
- require_relative 'terms/defn'
75
- include Factbase::Defn
76
-
77
- require_relative 'terms/system'
78
- include Factbase::System
79
-
80
- require_relative 'terms/debug'
81
- include Factbase::Debug
98
+ require_relative 'terms/shared'
99
+ include Factbase::TermShared
82
100
 
83
101
  # Ctor.
84
102
  # @param [Symbol] operator Operator
@@ -86,6 +104,53 @@ class Factbase::Term
86
104
  def initialize(operator, operands)
87
105
  @op = operator
88
106
  @operands = operands
107
+ @terms = {
108
+ unique: Factbase::Unique.new(operands),
109
+ prev: Factbase::Prev.new(operands),
110
+ concat: Factbase::Concat.new(operands),
111
+ sprintf: Factbase::Sprintf.new(operands),
112
+ matches: Factbase::Matches.new(operands),
113
+ traced: Factbase::Traced.new(operands),
114
+ assert: Factbase::Assert.new(operands),
115
+ env: Factbase::Env.new(operands),
116
+ defn: Factbase::Defn.new(operands),
117
+ undef: Factbase::Undef.new(operands),
118
+ as: Factbase::As.new(operands),
119
+ join: Factbase::Join.new(operands),
120
+ exists: Factbase::Exists.new(operands),
121
+ absent: Factbase::Absent.new(operands),
122
+ size: Factbase::Size.new(operands),
123
+ type: Factbase::Type.new(operands),
124
+ nil: Factbase::Nil.new(operands),
125
+ many: Factbase::Many.new(operands),
126
+ one: Factbase::One.new(operands),
127
+ to_string: Factbase::ToString.new(operands),
128
+ to_integer: Factbase::ToInteger.new(operands),
129
+ to_float: Factbase::ToFloat.new(operands),
130
+ to_time: Factbase::ToTime.new(operands),
131
+ sorted: Factbase::Sorted.new(operands),
132
+ inverted: Factbase::Inverted.new(operands),
133
+ head: Factbase::Head.new(operands),
134
+ plus: Factbase::Plus.new(operands),
135
+ minus: Factbase::Minus.new(operands),
136
+ times: Factbase::Times.new(operands),
137
+ div: Factbase::Div.new(operands),
138
+ zero: Factbase::Zero.new(operands),
139
+ eq: Factbase::Eq.new(operands),
140
+ lt: Factbase::Lt.new(operands),
141
+ lte: Factbase::Lte.new(operands),
142
+ gt: Factbase::Gt.new(operands),
143
+ gte: Factbase::Gte.new(operands),
144
+ always: Factbase::Always.new(operands),
145
+ never: Factbase::Never.new(operands),
146
+ not: Factbase::Not.new(operands),
147
+ or: Factbase::Or.new(operands),
148
+ and: Factbase::And.new(operands),
149
+ when: Factbase::When.new(operands),
150
+ either: Factbase::Either.new(operands),
151
+ count: Factbase::Count.new(operands),
152
+ first: Factbase::First.new(operands)
153
+ }
89
154
  end
90
155
 
91
156
  # Extend it with the module.
@@ -108,24 +173,35 @@ class Factbase::Term
108
173
  # should be evaluated. If no prediction can be made,
109
174
  # the same list is returned.
110
175
  # @param [Array<Hash>] maps Records to iterate, maybe
111
- # @param [Hash] _params Params to use (keys must be strings, not symbols, with values as arrays)
176
+ # @param [Hash] params Params to use (keys must be strings, not symbols, with values as arrays)
112
177
  # @return [Array<Hash>] Records to iterate
113
178
  def predict(maps, fb, params)
114
179
  m = :"#{@op}_predict"
115
- if respond_to?(m)
180
+ if @terms.key?(@op)
181
+ t = @terms[@op]
182
+ if t.respond_to?(:predict)
183
+ t.predict(maps, fb, params)
184
+ else
185
+ maps
186
+ end
187
+ elsif respond_to?(m)
116
188
  send(m, maps, fb, params)
117
189
  else
118
190
  maps
119
191
  end
120
192
  end
121
193
 
122
- # Does it match the fact?
194
+ # Evaluate term on a fact
123
195
  # @param [Factbase::Fact] fact The fact
124
196
  # @param [Array<Factbase::Fact>] maps All maps available
125
197
  # @param [Factbase] fb Factbase to use for sub-queries
126
- # @return [Boolean] TRUE if matches
198
+ # @return [Object] The result of evaluation
127
199
  def evaluate(fact, maps, fb)
128
- send(@op, fact, maps, fb)
200
+ if @terms.key?(@op)
201
+ @terms[@op].evaluate(fact, maps, fb)
202
+ else
203
+ send(@op, fact, maps, fb)
204
+ end
129
205
  rescue NoMethodError => e
130
206
  raise "Probably the term '#{@op}' is not defined at #{self}: #{e.message}"
131
207
  rescue StandardError => e
@@ -169,27 +245,6 @@ class Factbase::Term
169
245
  false
170
246
  end
171
247
 
172
- # Turns it into a string.
173
- # @return [String] The string of it
174
- def to_s
175
- @to_s ||=
176
- begin
177
- items = []
178
- items << @op
179
- items +=
180
- @operands.map do |o|
181
- if o.is_a?(String)
182
- "'#{o.gsub("'", "\\\\'").gsub('"', '\\\\"')}'"
183
- elsif o.is_a?(Time)
184
- o.utc.iso8601
185
- else
186
- o.to_s
187
- end
188
- end
189
- "(#{items.join(' ')})"
190
- end
191
- end
192
-
193
248
  def at(fact, maps, fb)
194
249
  assert_args(2)
195
250
  i = _values(0, fact, maps, fb)
@@ -200,40 +255,4 @@ class Factbase::Term
200
255
  return nil if v.nil?
201
256
  v[i]
202
257
  end
203
-
204
- private
205
-
206
- def assert_args(num)
207
- c = @operands.size
208
- raise "Too many (#{c}) operands for '#{@op}' (#{num} expected)" if c > num
209
- raise "Too few (#{c}) operands for '#{@op}' (#{num} expected)" if c < num
210
- end
211
-
212
- def _by_symbol(pos, fact)
213
- o = @operands[pos]
214
- raise "A symbol expected at ##{pos}, but '#{o}' (#{o.class}) provided" unless o.is_a?(Symbol)
215
- k = o.to_s
216
- fact[k]
217
- end
218
-
219
- # @return [Array|nil] Either array of values or NIL
220
- def _values(pos, fact, maps, fb)
221
- v = @operands[pos]
222
- v = v.evaluate(fact, maps, fb) if v.is_a?(Factbase::Term)
223
- v = fact[v.to_s] if v.is_a?(Symbol)
224
- return v if v.nil?
225
- unless v.is_a?(Array)
226
- v =
227
- if v.respond_to?(:each)
228
- v.to_a
229
- else
230
- [v]
231
- end
232
- end
233
- raise 'Why not array?' unless v.is_a?(Array)
234
- unless v.all? { |i| [Float, Integer, String, Time, TrueClass, FalseClass].any? { |t| i.is_a?(t) } }
235
- raise 'Wrong type inside'
236
- end
237
- v
238
- end
239
258
  end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2024-2025 Yegor Bugayenko
4
+ # SPDX-License-Identifier: MIT
5
+
6
+ require_relative 'base'
7
+
8
+ # Represents a term that evaluates to true if the specified operand is absent in the fact.
9
+ class Factbase::Absent < Factbase::TermBase
10
+ # Constructor.
11
+ # @param [Array] operands Operands
12
+ def initialize(operands)
13
+ super()
14
+ @operands = operands
15
+ end
16
+
17
+ # Evaluate term on a fact.
18
+ # @param [Factbase::Fact] fact The fact
19
+ # @param [Array<Factbase::Fact>] _maps All maps available
20
+ # @param [Factbase] _fb Factbase to use for sub-queries
21
+ # @return [Boolean] True if absent
22
+ def evaluate(fact, _maps, _fb)
23
+ assert_args(1)
24
+ _by_symbol(0, fact).nil?
25
+ end
26
+ end