factbase 0.8.0 → 0.9.1

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 (82) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -1
  3. data/Gemfile +1 -1
  4. data/Gemfile.lock +13 -13
  5. data/README.md +24 -24
  6. data/REUSE.toml +7 -2
  7. data/Rakefile +8 -1
  8. data/benchmark/bench_factbase.rb +1 -1
  9. data/fixtures/stories/agg.yml +17 -0
  10. data/fixtures/stories/always.yml +16 -0
  11. data/fixtures/stories/as.yml +16 -0
  12. data/fixtures/stories/count.yml +18 -0
  13. data/fixtures/stories/eq.yml +30 -0
  14. data/fixtures/stories/gt.yml +18 -0
  15. data/fixtures/stories/join.yml +19 -0
  16. data/fixtures/stories/max.yml +14 -0
  17. data/fixtures/stories/min.yml +14 -0
  18. data/fixtures/stories/nth.yml +14 -0
  19. data/fixtures/stories/or.yml +18 -0
  20. data/fixtures/stories/sprintf.yml +12 -0
  21. data/fixtures/stories/sum.yml +14 -0
  22. data/lib/factbase/cached/cached_fact.rb +28 -0
  23. data/lib/factbase/cached/cached_factbase.rb +64 -0
  24. data/lib/factbase/cached/cached_query.rb +61 -0
  25. data/lib/factbase/cached/cached_term.rb +25 -0
  26. data/lib/factbase/fact.rb +13 -13
  27. data/lib/factbase/indexed/indexed_fact.rb +28 -0
  28. data/lib/factbase/indexed/indexed_factbase.rb +64 -0
  29. data/lib/factbase/indexed/indexed_query.rb +56 -0
  30. data/lib/factbase/indexed/indexed_term.rb +60 -0
  31. data/lib/factbase/inv.rb +18 -6
  32. data/lib/factbase/light.rb +7 -6
  33. data/lib/factbase/logged.rb +87 -62
  34. data/lib/factbase/query.rb +29 -34
  35. data/lib/factbase/rules.rb +16 -15
  36. data/lib/factbase/sync/sync_factbase.rb +57 -0
  37. data/lib/factbase/sync/sync_query.rb +61 -0
  38. data/lib/factbase/syntax.rb +12 -25
  39. data/lib/factbase/tallied.rb +11 -10
  40. data/lib/factbase/taped.rb +8 -0
  41. data/lib/factbase/tee.rb +2 -0
  42. data/lib/factbase/term.rb +45 -17
  43. data/lib/factbase/terms/aggregates.rb +17 -15
  44. data/lib/factbase/terms/aliases.rb +4 -4
  45. data/lib/factbase/terms/casting.rb +8 -8
  46. data/lib/factbase/terms/debug.rb +2 -2
  47. data/lib/factbase/terms/defn.rb +3 -3
  48. data/lib/factbase/terms/logical.rb +53 -14
  49. data/lib/factbase/terms/math.rb +26 -26
  50. data/lib/factbase/terms/meta.rb +14 -14
  51. data/lib/factbase/terms/ordering.rb +4 -4
  52. data/lib/factbase/terms/strings.rb +8 -8
  53. data/lib/factbase/terms/system.rb +3 -3
  54. data/lib/factbase.rb +67 -55
  55. data/test/factbase/cached/test_cached_factbase.rb +22 -0
  56. data/test/factbase/cached/test_cached_query.rb +79 -0
  57. data/test/factbase/indexed/test_indexed_query.rb +175 -0
  58. data/test/factbase/sync/test_sync_query.rb +30 -0
  59. data/test/factbase/terms/test_aggregates.rb +5 -5
  60. data/test/factbase/terms/test_aliases.rb +7 -7
  61. data/test/factbase/terms/test_casting.rb +8 -8
  62. data/test/factbase/terms/test_debug.rb +6 -6
  63. data/test/factbase/terms/test_defn.rb +14 -14
  64. data/test/factbase/terms/test_logical.rb +17 -19
  65. data/test/factbase/terms/test_math.rb +63 -61
  66. data/test/factbase/terms/test_meta.rb +36 -36
  67. data/test/factbase/terms/test_ordering.rb +9 -9
  68. data/test/factbase/terms/test_strings.rb +10 -10
  69. data/test/factbase/terms/test_system.rb +6 -6
  70. data/test/factbase/test_accum.rb +5 -5
  71. data/test/factbase/test_fact.rb +12 -12
  72. data/test/factbase/test_logged.rb +7 -0
  73. data/test/factbase/test_query.rb +110 -37
  74. data/test/factbase/test_rules.rb +1 -1
  75. data/test/factbase/test_syntax.rb +12 -12
  76. data/test/factbase/test_tee.rb +8 -8
  77. data/test/factbase/test_term.rb +39 -30
  78. data/test/test__helper.rb +2 -2
  79. data/test/test_factbase.rb +6 -0
  80. metadata +29 -4
  81. data/lib/factbase/query_once.rb +0 -54
  82. data/lib/factbase/term_once.rb +0 -67
@@ -4,9 +4,9 @@
4
4
  # SPDX-License-Identifier: MIT
5
5
 
6
6
  require_relative '../factbase'
7
- require_relative 'syntax'
8
- require_relative 'fact'
9
7
  require_relative 'accum'
8
+ require_relative 'fact'
9
+ require_relative 'syntax'
10
10
  require_relative 'tee'
11
11
 
12
12
  # Query.
@@ -21,40 +21,37 @@ require_relative 'tee'
21
21
  # License:: MIT
22
22
  class Factbase::Query
23
23
  # Constructor.
24
- # @param [Factbase] fb Factbase
25
24
  # @param [Array<Fact>] maps Array of facts to start with
26
- # @param [Mutex] mutex Mutex to sync all modifications to the +maps+
27
- # @param [String] query The query as a string
28
- def initialize(fb, maps, mutex, query)
29
- @fb = fb
25
+ # @param [String|Factbase::Term] term The query term
26
+ def initialize(maps, term, fb)
30
27
  @maps = maps
31
- @mutex = mutex
32
- @query = query
28
+ @term = term.is_a?(String) ? Factbase::Syntax.new(term).to_term : term
29
+ @fb = fb
33
30
  end
34
31
 
35
32
  # Print it as a string.
36
33
  # @return [String] The query as a string
37
34
  def to_s
38
- @query.to_s
35
+ @term.to_s
39
36
  end
40
37
 
41
38
  # Iterate facts one by one.
39
+ # @param [Factbase] fb The factbase
42
40
  # @param [Hash] params Optional params accessible in the query via the "$" symbol
43
41
  # @yield [Fact] Facts one-by-one
44
42
  # @return [Integer] Total number of facts yielded
45
- def each(params = {})
46
- return to_enum(__method__, params) unless block_given?
47
- term = Factbase::Syntax.new(@fb, @query).to_term
43
+ def each(fb = @fb, params = {})
44
+ return to_enum(__method__, fb, params) unless block_given?
48
45
  yielded = 0
49
- @maps.each do |m|
46
+ params = params.transform_keys(&:to_s) if params.is_a?(Hash)
47
+ (@term.predict(@maps, params) || @maps).each do |m|
50
48
  extras = {}
51
- f = Factbase::Fact.new(@fb, @mutex, m)
52
- params = params.transform_keys(&:to_s) if params.is_a?(Hash)
49
+ f = Factbase::Fact.new(m)
53
50
  f = Factbase::Tee.new(f, params)
54
51
  a = Factbase::Accum.new(f, extras, false)
55
- r = term.evaluate(a, @maps)
52
+ r = @term.evaluate(a, @maps, fb)
56
53
  unless r.is_a?(TrueClass) || r.is_a?(FalseClass)
57
- raise "Unexpected evaluation result of type #{r.class}, must be Boolean at #{@query.inspect}"
54
+ raise "Unexpected evaluation result of type #{r.class}, must be Boolean at #{@term.inspect}"
58
55
  end
59
56
  next unless r
60
57
  yield Factbase::Accum.new(f, extras, true)
@@ -64,32 +61,30 @@ class Factbase::Query
64
61
  end
65
62
 
66
63
  # Read a single value.
64
+ # @param [Factbase] fb The factbase
67
65
  # @param [Hash] params Optional params accessible in the query via the "$" symbol
68
- # @return The value evaluated
69
- def one(params = {})
70
- term = Factbase::Syntax.new(@fb, @query).to_term
66
+ # @return [String|Integer|Float|Time|Array|NilClass] The value evaluated
67
+ def one(fb = @fb, params = {})
71
68
  params = params.transform_keys(&:to_s) if params.is_a?(Hash)
72
- r = term.evaluate(Factbase::Tee.new(nil, params), @maps)
69
+ r = @term.evaluate(Factbase::Tee.new(Factbase::Fact.new({}), params), @maps, fb)
73
70
  unless %w[String Integer Float Time Array NilClass].include?(r.class.to_s)
74
- raise "Incorrect type #{r.class} returned by #{@query.inspect}"
71
+ raise "Incorrect type #{r.class} returned by #{@term.inspect}"
75
72
  end
76
73
  r
77
74
  end
78
75
 
79
76
  # Delete all facts that match the query.
77
+ # @param [Factbase] fb The factbase to delete from
80
78
  # @return [Integer] Total number of facts deleted
81
- def delete!
82
- term = Factbase::Syntax.new(@fb, @query).to_term
79
+ def delete!(fb = @fb)
83
80
  deleted = 0
84
- @mutex.synchronize do
85
- @maps.delete_if do |m|
86
- f = Factbase::Fact.new(@fb, @mutex, m)
87
- if term.evaluate(f, @maps)
88
- deleted += 1
89
- true
90
- else
91
- false
92
- end
81
+ @maps.delete_if do |m|
82
+ f = Factbase::Fact.new(m)
83
+ if @term.evaluate(f, @maps, fb)
84
+ deleted += 1
85
+ true
86
+ else
87
+ false
93
88
  end
94
89
  end
95
90
  deleted
@@ -37,11 +37,11 @@ class Factbase::Rules
37
37
  end
38
38
 
39
39
  def insert
40
- Fact.new(@fb.insert, @check)
40
+ Fact.new(@fb.insert, @check, @fb)
41
41
  end
42
42
 
43
- def query(query)
44
- Query.new(@fb.query(query), @check)
43
+ def query(query, maps = nil)
44
+ Query.new(@fb.query(query, maps), @check, @fb)
45
45
  end
46
46
 
47
47
  def txn
@@ -53,7 +53,7 @@ class Factbase::Rules
53
53
  @check = before
54
54
  fbt.query('(always)').each do |f|
55
55
  next unless later.include?(f)
56
- @check.it(f)
56
+ @check.it(f, @fb)
57
57
  end
58
58
  end
59
59
  end
@@ -63,9 +63,10 @@ class Factbase::Rules
63
63
  # This is an internal class, it is not supposed to be instantiated directly.
64
64
  #
65
65
  class Fact
66
- def initialize(fact, check)
66
+ def initialize(fact, check, fb)
67
67
  @fact = fact
68
68
  @check = check
69
+ @fb = fb
69
70
  end
70
71
 
71
72
  def to_s
@@ -79,7 +80,7 @@ class Factbase::Rules
79
80
  others do |*args|
80
81
  r = @fact.method_missing(*args)
81
82
  k = args[0].to_s
82
- @check.it(@fact) if k.end_with?('=')
83
+ @check.it(@fact, @fb) if k.end_with?('=')
83
84
  r
84
85
  end
85
86
  end
@@ -87,19 +88,19 @@ class Factbase::Rules
87
88
  # Query decorator.
88
89
  #
89
90
  # This is an internal class, it is not supposed to be instantiated directly.
90
- #
91
91
  class Query
92
92
  decoor(:query)
93
93
 
94
- def initialize(query, check)
94
+ def initialize(query, check, fb)
95
95
  @query = query
96
96
  @check = check
97
+ @fb = fb
97
98
  end
98
99
 
99
- def each(params = {})
100
- return to_enum(__method__, params) unless block_given?
101
- @query.each do |f|
102
- yield Fact.new(f, @check)
100
+ def each(fb = @fb, params = {})
101
+ return to_enum(__method__, fb, params) unless block_given?
102
+ @query.each(fb, params) do |f|
103
+ yield Fact.new(f, @check, fb)
103
104
  end
104
105
  end
105
106
  end
@@ -112,8 +113,8 @@ class Factbase::Rules
112
113
  @expr = expr
113
114
  end
114
115
 
115
- def it(fact)
116
- return if Factbase::Syntax.new(Factbase.new, @expr).to_term.evaluate(fact, [])
116
+ def it(fact, fb)
117
+ return if Factbase::Syntax.new(@expr).to_term.evaluate(fact, [], fb)
117
118
  e = "#{@expr[0..32]}..." if @expr.length > 32
118
119
  raise "The fact doesn't match the #{e.inspect} rule: #{fact}"
119
120
  end
@@ -128,7 +129,7 @@ class Factbase::Rules
128
129
  @facts = Set.new
129
130
  end
130
131
 
131
- def it(fact)
132
+ def it(fact, _fb)
132
133
  a = fact[@uid]
133
134
  return if a.nil?
134
135
  @facts << a[0] unless @uid.nil?
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2024-2025 Yegor Bugayenko
4
+ # SPDX-License-Identifier: MIT
5
+
6
+ require 'decoor'
7
+ require_relative '../../factbase'
8
+
9
+ # A synchronous thread-safe factbase.
10
+ #
11
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
12
+ # Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
13
+ # License:: MIT
14
+ class Factbase::SyncFactbase
15
+ decoor(:origin)
16
+
17
+ # Constructor.
18
+ # @param [Factbase] origin Original factbase to decorate
19
+ # @param [Mutex] mutex Mutex to use for synchronization
20
+ def initialize(origin, mutex = Mutex.new)
21
+ @origin = origin
22
+ @mutex = mutex
23
+ end
24
+
25
+ # Insert a new fact and return it.
26
+ # @return [Factbase::Fact] The fact just inserted
27
+ def insert
28
+ @mutex.synchronize do
29
+ @origin.insert
30
+ end
31
+ end
32
+
33
+ # Convert a query to a term.
34
+ # @param [String] query The query to convert
35
+ # @return [Factbase::Term] The term
36
+ def to_term(query)
37
+ @origin.to_term(query)
38
+ end
39
+
40
+ # Create a query capable of iterating.
41
+ # @param [String] term The query to use for selections
42
+ # @param [Array<Hash>] maps Possible maps to use
43
+ def query(term, maps = nil)
44
+ term = to_term(term) if term.is_a?(String)
45
+ require_relative 'sync_query'
46
+ Factbase::SyncQuery.new(@origin.query(term, maps), @mutex, self)
47
+ end
48
+
49
+ # Run an ACID transaction.
50
+ # @return [Factbase::Churn] How many facts have been changed (zero if rolled back)
51
+ # @yield [Factbase] Block to execute in transaction
52
+ def txn
53
+ @origin.txn do |fbt|
54
+ yield Factbase::SyncFactbase.new(fbt, @mutex)
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,61 @@
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
+ # Synchronized thread-safe query.
9
+ #
10
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
11
+ # Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
12
+ # License:: MIT
13
+ class Factbase::SyncQuery
14
+ # Constructor.
15
+ # @param [Factbase::Query] origin Original query
16
+ # @param [Mutex] mutex The mutex
17
+ def initialize(origin, mutex, fb)
18
+ @origin = origin
19
+ @mutex = mutex
20
+ @fb = fb
21
+ end
22
+
23
+ # Iterate facts one by one.
24
+ # @param [Hash] params Optional params accessible in the query via the "$" symbol
25
+ # @yield [Fact] Facts one-by-one
26
+ # @return [Integer] Total number of facts yielded
27
+ def each(fb = @fb, params = {}, &)
28
+ return to_enum(__method__, fb, params) unless block_given?
29
+ try_lock do
30
+ @origin.each(fb, params, &)
31
+ end
32
+ end
33
+
34
+ # Read a single value.
35
+ # @param [Factbase] fb The factbase
36
+ # @param [Hash] params Optional params accessible in the query via the "$" symbol
37
+ # @return [String|Integer|Float|Time|Array|NilClass] The value evaluated
38
+ def one(fb = @fb, params = {})
39
+ try_lock do
40
+ @origin.one(fb, params)
41
+ end
42
+ end
43
+
44
+ # Delete all facts that match the query.
45
+ # @param [Factbase] fb The factbase
46
+ # @return [Integer] Total number of facts deleted
47
+ def delete!(fb = @fb)
48
+ try_lock do
49
+ @origin.delete!(fb)
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ def try_lock
56
+ locked = @mutex.try_lock
57
+ r = yield
58
+ @mutex.unlock if locked
59
+ r
60
+ end
61
+ end
@@ -8,22 +8,11 @@ require 'time'
8
8
  require_relative '../factbase'
9
9
  require_relative 'fact'
10
10
  require_relative 'term'
11
- require_relative 'term_once'
12
11
 
13
- # Syntax.
12
+ # Syntax parser.
14
13
  #
15
14
  # This is an internal class, it is not supposed to be instantiated directly.
16
15
  #
17
- # However, you can use it directly, if you need a parser of our syntax. You can
18
- # create your own "Term" class and let this parser make instances of it for
19
- # every term it meets in the query:
20
- #
21
- # require 'factbase/syntax'
22
- # t = Factbase::Syntax.new(Factbase.new, '(hello world)', MyTerm).to_term
23
- #
24
- # The +MyTerm+ class should have a constructor with two arguments:
25
- # the operator and the list of operands (Array).
26
- #
27
16
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
28
17
  # Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
29
18
  # License:: MIT
@@ -33,19 +22,13 @@ class Factbase::Syntax
33
22
 
34
23
  # Ctor.
35
24
  #
36
- # The class provided as the +term+ argument must have a three-argument
37
- # constructor, similar to the class +Factbase::Term+. Also, it must be
25
+ # The class provided as the +term+ argument must have a constructor that accepts
26
+ # an operator, operands array, and a keyword argument fb. Also, it must be
38
27
  # a child of +Factbase::Term+.
39
28
  #
40
- # @param [Factbase] fb Factbase
41
29
  # @param [String] query The query, for example "(eq id 42)"
42
- # @param [Class] term The class to instantiate to make every term
43
- def initialize(fb, query, term: Factbase::Term)
44
- @fb = fb
30
+ def initialize(query)
45
31
  @query = query
46
- raise "Term must be a Class, while #{term.class.name} provided" unless term.is_a?(Class)
47
- raise "The 'term' must be a child of Factbase::Term, while #{term.name} provided" unless term <= Factbase::Term
48
- @term = term
49
32
  end
50
33
 
51
34
  # Convert it to a term.
@@ -74,7 +57,7 @@ class Factbase::Syntax
74
57
  raise "Too many terms (#{@ast[1]} != #{@tokens.size})" if @ast[1] != @tokens.size
75
58
  t = @ast[0]
76
59
  raise 'No terms found in the AST' if t.nil?
77
- raise "#{t.class.name} is not an instance of #{@term}, thus not a proper term" unless t.is_a?(@term)
60
+ raise "#{t.class.name} is not an instance of Term" unless t.is_a?(Factbase::Term)
78
61
  t
79
62
  end
80
63
 
@@ -82,9 +65,13 @@ class Factbase::Syntax
82
65
  # token at the position is not a literal (like 42 or "Hello") but a term,
83
66
  # the function recursively calls itself.
84
67
  #
85
- # The function returns an two-elements array, where the first element
68
+ # The function returns a two-element array, where the first element
86
69
  # is the term/literal and the second one is the position where the
87
70
  # scanning should continue.
71
+ #
72
+ # @param [Array] tokens Array of tokens
73
+ # @param [Integer] at Position to start parsing from
74
+ # @return [Array<Factbase::Term,Integer>] The term detected and ending position
88
75
  def to_ast(tokens, at)
89
76
  raise "Closing too soon at ##{at}" if tokens[at] == :close
90
77
  return [tokens[at], at + 1] unless tokens[at] == :open
@@ -103,12 +90,12 @@ class Factbase::Syntax
103
90
  operands << operand
104
91
  break if tokens[at] == :close
105
92
  end
106
- t = @term.new(@fb, op, operands)
107
- t = Factbase::TermOnce.new(t, @fb.cache) if t.instance_of?(Factbase::Term)
93
+ t = Factbase::Term.new(op, operands)
108
94
  [t, at + 1]
109
95
  end
110
96
 
111
97
  # Turns a query into an array of tokens.
98
+ # @return [Array] Array of tokens
112
99
  def to_tokens
113
100
  list = []
114
101
  acc = ''
@@ -31,8 +31,8 @@ class Factbase::Tallied
31
31
  f
32
32
  end
33
33
 
34
- def query(query)
35
- Query.new(@fb.query(query), @churn)
34
+ def query(query, maps = nil)
35
+ Query.new(@fb.query(query, maps), @churn, @fb)
36
36
  end
37
37
 
38
38
  def txn
@@ -69,24 +69,25 @@ class Factbase::Tallied
69
69
  #
70
70
  # This is an internal class, it is not supposed to be instantiated directly.
71
71
  class Query
72
- def initialize(query, churn)
72
+ def initialize(query, churn, fb)
73
73
  @query = query
74
74
  @churn = churn
75
+ @fb = fb
75
76
  end
76
77
 
77
- def one(params = {})
78
- @query.one(params)
78
+ def one(fb = @fb, params = {})
79
+ @query.one(fb, params)
79
80
  end
80
81
 
81
- def each(params = {}, &)
82
- return to_enum(__method__, params) unless block_given?
83
- @query.each(params) do |f|
82
+ def each(fb = @fb, params = {}, &)
83
+ return to_enum(__method__, fb, params) unless block_given?
84
+ @query.each(fb, params) do |f|
84
85
  yield Fact.new(f, @churn)
85
86
  end
86
87
  end
87
88
 
88
- def delete!
89
- c = @query.delete!
89
+ def delete!(fb = @fb)
90
+ c = @query.delete!(fb)
90
91
  @churn.append(0, c, 0)
91
92
  c
92
93
  end
@@ -59,6 +59,14 @@ class Factbase::Taped
59
59
  end
60
60
  end
61
61
 
62
+ def to_a
63
+ @origin.to_a
64
+ end
65
+
66
+ def group_by(&)
67
+ @origin.group_by(&)
68
+ end
69
+
62
70
  # Decorator of Hash.
63
71
  class TapedHash
64
72
  def initialize(origin, added)
data/lib/factbase/tee.rb CHANGED
@@ -16,7 +16,9 @@ class Factbase::Tee
16
16
  # @param [Factbase::Fact] fact Primary fact to use for reading
17
17
  # @param [Factbase::Fact] upper Fact to access with a "$" prefix
18
18
  def initialize(fact, upper)
19
+ raise 'Fact is nil' if fact.nil?
19
20
  @fact = fact
21
+ raise 'Upper is nil' if upper.nil?
20
22
  @upper = upper
21
23
  end
22
24
 
data/lib/factbase/term.rb CHANGED
@@ -17,8 +17,8 @@ require_relative 'tee'
17
17
  #
18
18
  # require 'factbase/fact'
19
19
  # require 'factbase/term'
20
- # f = Factbase::Fact.new(Factbase.new, Mutex.new, { 'foo' => [42, 256, 'Hello, world!'] })
21
- # t = Factbase::Term.new(Factbase.new, :lt, [:foo, 50])
20
+ # f = Factbase::Fact.new({ 'foo' => [42, 256, 'Hello, world!'] })
21
+ # t = Factbase::Term.new(:lt, [:foo, 50])
22
22
  # assert(t.evaluate(f))
23
23
  #
24
24
  # The design of this class may look ugly, since it has a large number of
@@ -30,11 +30,19 @@ require_relative 'tee'
30
30
  # Moreover, it looks like the number of possible term types is rather limited
31
31
  # and currently we implement most of them.
32
32
  #
33
+ # It is NOT thread-safe!
34
+ #
33
35
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
34
36
  # Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
35
37
  # License:: MIT
36
38
  class Factbase::Term
37
- attr_reader :op, :operands
39
+ # The operator of this term
40
+ # @return [Symbol] The operator
41
+ attr_reader :op
42
+
43
+ # The operands of this term
44
+ # @return [Array] The operands
45
+ attr_reader :operands
38
46
 
39
47
  require_relative 'terms/math'
40
48
  include Factbase::Term::Math
@@ -70,21 +78,41 @@ class Factbase::Term
70
78
  include Factbase::Term::Debug
71
79
 
72
80
  # Ctor.
73
- # @param [Factbase] fb Factbase
74
81
  # @param [Symbol] operator Operator
75
82
  # @param [Array] operands Operands
76
- def initialize(fb, operator, operands)
77
- @fb = fb
83
+ def initialize(operator, operands)
78
84
  @op = operator
79
85
  @operands = operands
80
86
  end
81
87
 
88
+ def redress!(type, **args)
89
+ extend type
90
+ args.each { |k, v| send(:instance_variable_set, :"@#{k}", v) }
91
+ @operands.map do |op|
92
+ if op.is_a?(Factbase::Term)
93
+ op.redress!(type, **args)
94
+ else
95
+ op
96
+ end
97
+ end
98
+ end
99
+
100
+ # Try to predict which facts from the provided list
101
+ # should be evaluated. If no prediction can be made,
102
+ # the same list is returned.
103
+ # @param [Array<Hash>] maps Records to iterate, maybe
104
+ # @return [Array<Hash>] Records to iterate
105
+ def predict(maps, _params)
106
+ maps
107
+ end
108
+
82
109
  # Does it match the fact?
83
110
  # @param [Factbase::Fact] fact The fact
84
111
  # @param [Array<Factbase::Fact>] maps All maps available
85
- # @return [bool] TRUE if matches
86
- def evaluate(fact, maps)
87
- send(@op, fact, maps)
112
+ # @param [Factbase] fb Factbase to use for sub-queries
113
+ # @return [Boolean] TRUE if matches
114
+ def evaluate(fact, maps, fb)
115
+ send(@op, fact, maps, fb)
88
116
  rescue NoMethodError => e
89
117
  raise "Probably the term '#{@op}' is not defined at #{self}:\n#{Backtrace.new(e)}"
90
118
  rescue StandardError => e
@@ -146,26 +174,26 @@ class Factbase::Term
146
174
  "(#{items.join(' ')})"
147
175
  end
148
176
 
149
- private
150
-
151
- def at(fact, maps)
177
+ def at(fact, maps, fb)
152
178
  assert_args(2)
153
- i = the_values(0, fact, maps)
179
+ i = _values(0, fact, maps, fb)
154
180
  raise "Too many values (#{i.size}) at first position, one expected" unless i.size == 1
155
181
  i = i[0]
156
182
  return nil if i.nil?
157
- v = the_values(1, fact, maps)
183
+ v = _values(1, fact, maps, fb)
158
184
  return nil if v.nil?
159
185
  v[i]
160
186
  end
161
187
 
188
+ private
189
+
162
190
  def assert_args(num)
163
191
  c = @operands.size
164
192
  raise "Too many (#{c}) operands for '#{@op}' (#{num} expected)" if c > num
165
193
  raise "Too few (#{c}) operands for '#{@op}' (#{num} expected)" if c < num
166
194
  end
167
195
 
168
- def by_symbol(pos, fact)
196
+ def _by_symbol(pos, fact)
169
197
  o = @operands[pos]
170
198
  raise "A symbol expected at ##{pos}, but '#{o}' (#{o.class}) provided" unless o.is_a?(Symbol)
171
199
  k = o.to_s
@@ -173,9 +201,9 @@ class Factbase::Term
173
201
  end
174
202
 
175
203
  # @return [Array|nil] Either array of values or NIL
176
- def the_values(pos, fact, maps)
204
+ def _values(pos, fact, maps, fb)
177
205
  v = @operands[pos]
178
- v = v.evaluate(fact, maps) if v.is_a?(Factbase::Term)
206
+ v = v.evaluate(fact, maps, fb) if v.is_a?(Factbase::Term)
179
207
  v = fact[v.to_s] if v.is_a?(Symbol)
180
208
  return v if v.nil?
181
209
  unless v.is_a?(Array)
@@ -11,30 +11,32 @@ require_relative '../../factbase'
11
11
  # Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
12
12
  # License:: MIT
13
13
  module Factbase::Term::Aggregates
14
- def min(_fact, maps)
14
+ def min(_fact, maps, _fb)
15
15
  assert_args(1)
16
- best(maps) { |v, b| v < b }
16
+ _best(maps) { |v, b| v < b }
17
17
  end
18
18
 
19
- def max(_fact, maps)
19
+ def max(_fact, maps, _fb)
20
20
  assert_args(1)
21
- best(maps) { |v, b| v > b }
21
+ _best(maps) { |v, b| v > b }
22
22
  end
23
23
 
24
- def count(_fact, maps)
24
+ def count(_fact, maps, _fb)
25
25
  maps.size
26
26
  end
27
27
 
28
- def nth(_fact, maps)
28
+ def nth(_fact, maps, _fb)
29
29
  assert_args(2)
30
30
  pos = @operands[0]
31
31
  raise "An integer expected, but #{pos} provided" unless pos.is_a?(Integer)
32
32
  k = @operands[1]
33
33
  raise "A symbol expected, but #{k} provided" unless k.is_a?(Symbol)
34
- maps[pos][k.to_s]
34
+ m = maps[pos]
35
+ return nil if m.nil?
36
+ m[k.to_s]
35
37
  end
36
38
 
37
- def first(_fact, maps)
39
+ def first(_fact, maps, _fb)
38
40
  assert_args(1)
39
41
  k = @operands[0]
40
42
  raise "A symbol expected, but #{k} provided" unless k.is_a?(Symbol)
@@ -43,7 +45,7 @@ module Factbase::Term::Aggregates
43
45
  first[k.to_s]
44
46
  end
45
47
 
46
- def sum(_fact, maps)
48
+ def sum(_fact, maps, _fb)
47
49
  k = @operands[0]
48
50
  raise "A symbol expected, but '#{k}' provided" unless k.is_a?(Symbol)
49
51
  sum = 0
@@ -58,24 +60,24 @@ module Factbase::Term::Aggregates
58
60
  sum
59
61
  end
60
62
 
61
- def agg(fact, maps)
63
+ def agg(fact, maps, fb)
62
64
  assert_args(2)
63
65
  selector = @operands[0]
64
66
  raise "A term expected, but '#{selector}' provided" unless selector.is_a?(Factbase::Term)
65
67
  term = @operands[1]
66
68
  raise "A term expected, but '#{term}' provided" unless term.is_a?(Factbase::Term)
67
- subset = @fb.query(selector.to_s, maps).each(fact).to_a
68
- term.evaluate(nil, subset)
69
+ subset = fb.query(selector, maps).each(fb, fact).to_a
70
+ term.evaluate(nil, subset, fb)
69
71
  end
70
72
 
71
- def empty(fact, maps)
73
+ def empty(fact, maps, fb)
72
74
  assert_args(1)
73
75
  term = @operands[0]
74
76
  raise "A term expected, but '#{term}' provided" unless term.is_a?(Factbase::Term)
75
- @fb.query(term.to_s, maps).each(fact).to_a.empty?
77
+ fb.query(term, maps).each(fb, fact).to_a.empty?
76
78
  end
77
79
 
78
- def best(maps)
80
+ def _best(maps)
79
81
  k = @operands[0]
80
82
  raise "A symbol expected, but #{k} provided" unless k.is_a?(Symbol)
81
83
  best = nil