factbase 0.0.51 → 0.0.52

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: a5185906fd48ccc7df8bfd07a8dd88ae4e7ce4636ec40b3cc3fc174cdeb347d1
4
- data.tar.gz: a0bc08ef0001d876b9cd26c31fd4a59f094f9ad88dbbb002e345ca4c47cc1a7f
3
+ metadata.gz: a70fcc45481bd1611d6bf2ed5c41ef40fe6f1ce67494abfa769c3b859b215960
4
+ data.tar.gz: 609bd76eaca6f4668bbbf5678514503edb1f9184e9abd62321dc02cdd614f80c
5
5
  SHA512:
6
- metadata.gz: ad553e4da47bb2a6af81a07a64d627dc03ebfe68f96000bc1862df8a25e5bd3cc1563d93ef3a184932af6af286e47912789ca232154cadef512cfc97a3341387
7
- data.tar.gz: b85a0a97e043e3bc4fa03396a3073c63f18cc268bf296174df66045b7ce47ccd082eb2cbe92d1cd1b07c7a43225ebcd830f624abccdd7d71bfae78825c05dcc5
6
+ metadata.gz: 97e8cc3c9f53349aaf85aa3f95ba9f0e2525dd85f242ade079083b4ffab042dbb0b3f1ad776573646f7fae0107e709404b70e340fc9660b99ef8770a313aa8a9
7
+ data.tar.gz: e4d9404781d0314d4e6aa22f93affc3260fc053121c9e70204f8690d30de34feb26533254f15519b9e2562a5562e93398779d890ab78c3acc3afcd8ed8fba782
data/Gemfile CHANGED
@@ -23,11 +23,11 @@
23
23
  source 'https://rubygems.org'
24
24
  gemspec
25
25
 
26
- gem 'minitest', '5.23.1', require: false
26
+ gem 'minitest', '5.24.0', require: false
27
27
  gem 'rake', '13.2.1', require: false
28
- gem 'rspec-rails', '6.1.2', require: false
28
+ gem 'rspec-rails', '6.1.3', require: false
29
29
  gem 'rubocop', '1.64.1', require: false
30
- gem 'rubocop-performance', '1.21.0', require: false
30
+ gem 'rubocop-performance', '1.21.1', require: false
31
31
  gem 'rubocop-rspec', '3.0.1', require: false
32
32
  gem 'simplecov', '0.22.0', require: false
33
33
  gem 'simplecov-cobertura', '2.1.0', require: false
data/Gemfile.lock CHANGED
@@ -62,7 +62,7 @@ GEM
62
62
  crass (~> 1.0.2)
63
63
  nokogiri (>= 1.12.0)
64
64
  loog (0.5.1)
65
- minitest (5.23.1)
65
+ minitest (5.24.0)
66
66
  mutex_m (0.2.0)
67
67
  nokogiri (1.16.6-arm64-darwin)
68
68
  racc (~> 1.4)
@@ -119,7 +119,7 @@ GEM
119
119
  rspec-mocks (3.13.1)
120
120
  diff-lcs (>= 1.2.0, < 2.0)
121
121
  rspec-support (~> 3.13.0)
122
- rspec-rails (6.1.2)
122
+ rspec-rails (6.1.3)
123
123
  actionpack (>= 6.1)
124
124
  activesupport (>= 6.1)
125
125
  railties (>= 6.1)
@@ -141,7 +141,7 @@ GEM
141
141
  unicode-display_width (>= 2.4.0, < 3.0)
142
142
  rubocop-ast (1.31.3)
143
143
  parser (>= 3.3.1.0)
144
- rubocop-performance (1.21.0)
144
+ rubocop-performance (1.21.1)
145
145
  rubocop (>= 1.48.1, < 2.0)
146
146
  rubocop-ast (>= 1.31.1, < 2.0)
147
147
  rubocop-rspec (3.0.1)
@@ -176,11 +176,11 @@ PLATFORMS
176
176
 
177
177
  DEPENDENCIES
178
178
  factbase!
179
- minitest (= 5.23.1)
179
+ minitest (= 5.24.0)
180
180
  rake (= 13.2.1)
181
- rspec-rails (= 6.1.2)
181
+ rspec-rails (= 6.1.3)
182
182
  rubocop (= 1.64.1)
183
- rubocop-performance (= 1.21.0)
183
+ rubocop-performance (= 1.21.1)
184
184
  rubocop-rspec (= 3.0.1)
185
185
  simplecov (= 0.22.0)
186
186
  simplecov-cobertura (= 2.1.0)
data/README.md CHANGED
@@ -86,8 +86,9 @@ It's possible to modify the facts retrieved, on fly:
86
86
 
87
87
  * `(as p v)` adds property `p` with the value `v`
88
88
  * `(join s t)` adds properties named by the `s` mask with the values retrieved
89
- by the `t` term, for example, `(join "foo_*" (gt x 5))` will add `foo_x` and
90
- all other properties found in the facts that match `(gt x 5)`
89
+ by the `t` term, for example, `(join "x<=foo,y<=bar" (gt x 5))` will add
90
+ `x` and `y` properties, setting them to values found in the `foo` and `bar`
91
+ properties in the facts that match `(gt x 5)`
91
92
 
92
93
  Also, some simple arithmetic:
93
94
 
@@ -128,6 +129,10 @@ a positive integer)
128
129
  * `(min p)` returns the minimum
129
130
  * `(sum p)` returns the arithmetic sum of all values of the `p` property
130
131
 
132
+ It's also possible to use a sub-query in a shorter form than with the `agg`:
133
+
134
+ * `(empty q)` is true if the subquery `q` is empty
135
+
131
136
  ## How to contribute
132
137
 
133
138
  Read
@@ -39,7 +39,7 @@ class Factbase::Accum
39
39
  end
40
40
 
41
41
  def to_s
42
- @fact.to_s
42
+ "#{@fact} + #{@props}"
43
43
  end
44
44
 
45
45
  def method_missing(*args)
@@ -55,6 +55,7 @@ class Factbase::Accum
55
55
  kk = args[1].to_s
56
56
  vv = @props[kk].nil? ? [] : @props[kk]
57
57
  vvv = @fact.method_missing(*args)
58
+ vvv = [vvv] unless vvv.nil? || vvv.is_a?(Array)
58
59
  vv += vvv unless vvv.nil?
59
60
  vv.uniq!
60
61
  return vv.empty? ? nil : vv
@@ -129,12 +129,26 @@ class Factbase::Looged
129
129
  @loog = loog
130
130
  end
131
131
 
132
- def each(&)
132
+ def one(params = {})
133
+ q = Factbase::Syntax.new(@expr).to_term.to_s
134
+ r = nil
135
+ tail = Factbase::Looged.elapsed do
136
+ r = @fb.query(@expr).one(params)
137
+ end
138
+ if r.nil?
139
+ @loog.debug("Nothing found by '#{q}' #{tail}")
140
+ else
141
+ @loog.debug("Found #{r} (#{r.class}) by '#{q}' #{tail}")
142
+ end
143
+ r
144
+ end
145
+
146
+ def each(params = {}, &)
133
147
  q = Factbase::Syntax.new(@expr).to_term.to_s
134
148
  if block_given?
135
149
  r = nil
136
150
  tail = Factbase::Looged.elapsed do
137
- r = @fb.query(@expr).each(&)
151
+ r = @fb.query(@expr).each(params, &)
138
152
  end
139
153
  raise ".each of #{@query.class} returned #{r.class}" unless r.is_a?(Integer)
140
154
  if r.zero?
@@ -146,7 +160,7 @@ class Factbase::Looged
146
160
  else
147
161
  array = []
148
162
  tail = Factbase::Looged.elapsed do
149
- @fb.query(@expr).each do |f|
163
+ @fb.query(@expr).each(params) do |f|
150
164
  array << f
151
165
  end
152
166
  end
@@ -24,6 +24,7 @@ require_relative '../factbase'
24
24
  require_relative 'syntax'
25
25
  require_relative 'fact'
26
26
  require_relative 'accum'
27
+ require_relative 'tee'
27
28
 
28
29
  # Query.
29
30
  #
@@ -46,16 +47,19 @@ class Factbase::Query
46
47
  @query = query
47
48
  end
48
49
 
49
- # Iterate them one by one.
50
+ # Iterate facts one by one.
51
+ # @param [Hash] params Optional params accessible in the query via the "$" symbol
50
52
  # @yield [Fact] Facts one-by-one
51
53
  # @return [Integer] Total number of facts yielded
52
- def each
53
- return to_enum(__method__) unless block_given?
54
+ def each(params = {})
55
+ return to_enum(__method__, params) unless block_given?
54
56
  term = Factbase::Syntax.new(@query).to_term
55
57
  yielded = 0
56
58
  @maps.each do |m|
57
59
  extras = {}
58
60
  f = Factbase::Fact.new(@mutex, m)
61
+ params = params.transform_keys(&:to_s) if params.is_a?(Hash)
62
+ f = Factbase::Tee.new(f, params)
59
63
  a = Factbase::Accum.new(f, extras, false)
60
64
  r = term.evaluate(a, @maps)
61
65
  unless r.is_a?(TrueClass) || r.is_a?(FalseClass)
@@ -68,6 +72,19 @@ class Factbase::Query
68
72
  yielded
69
73
  end
70
74
 
75
+ # Read a single value.
76
+ # @param [Hash] params Optional params accessible in the query via the "$" symbol
77
+ # @return The value evaluated
78
+ def one(params = {})
79
+ term = Factbase::Syntax.new(@query).to_term
80
+ params = params.transform_keys(&:to_s) if params.is_a?(Hash)
81
+ r = term.evaluate(Factbase::Tee.new(nil, params), @maps)
82
+ unless %w[String Integer Float Time Array NilClass].include?(r.class.to_s)
83
+ raise "Incorrect type #{r.class} returned by #{@query}"
84
+ end
85
+ r
86
+ end
87
+
71
88
  # Delete all facts that match the query.
72
89
  # @return [Integer] Total number of facts deleted
73
90
  def delete!
@@ -126,8 +126,12 @@ class Factbase::Rules
126
126
  @check = check
127
127
  end
128
128
 
129
- def each
130
- return to_enum(__method__) unless block_given?
129
+ def one(params = {})
130
+ @query.one(params)
131
+ end
132
+
133
+ def each(params = {})
134
+ return to_enum(__method__, params) unless block_given?
131
135
  @query.each do |f|
132
136
  yield Fact.new(f, @check)
133
137
  end
data/lib/factbase/tee.rb CHANGED
@@ -41,8 +41,10 @@ class Factbase::Tee
41
41
  end
42
42
 
43
43
  def method_missing(*args)
44
- return @upper[args[1].to_s[1..]] if args[0].to_s == '[]' && args[1].to_s.start_with?('$')
45
- @fact.method_missing(*args)
44
+ return @fact.method_missing(*args) unless args[0].to_s == '[]' && args[1].to_s.start_with?('$')
45
+ n = args[1].to_s
46
+ n = n[1..] unless @upper.is_a?(Factbase::Tee)
47
+ @upper[n]
46
48
  end
47
49
 
48
50
  # rubocop:disable Style/OptionalBooleanParameter
@@ -81,10 +81,17 @@ module Factbase::Term::Aggregates
81
81
  raise "A term expected, but '#{selector}' provided" unless selector.is_a?(Factbase::Term)
82
82
  term = @operands[1]
83
83
  raise "A term expected, but '#{term}' provided" unless term.is_a?(Factbase::Term)
84
- subset = maps.select { |m| selector.evaluate(Factbase::Tee.new(Factbase::Fact.new(Mutex.new, m), fact), maps) }
84
+ subset = Factbase::Query.new(maps, Mutex.new, selector.to_s).each(fact).to_a
85
85
  term.evaluate(nil, subset)
86
86
  end
87
87
 
88
+ def empty(fact, maps)
89
+ assert_args(1)
90
+ term = @operands[0]
91
+ raise "A term expected, but '#{term}' provided" unless term.is_a?(Factbase::Term)
92
+ Factbase::Query.new(maps, Mutex.new, term.to_s).each(fact).to_a.empty?
93
+ end
94
+
88
95
  def best(maps)
89
96
  k = @operands[0]
90
97
  raise "A symbol expected, but #{k} provided" unless k.is_a?(Symbol)
@@ -39,15 +39,19 @@ module Factbase::Term::Aliases
39
39
 
40
40
  def join(fact, maps)
41
41
  assert_args(2)
42
- mask = @operands[0]
43
- raise "A string expected as first argument of 'join'" unless mask.is_a?(String)
42
+ jumps = @operands[0]
43
+ raise "A string expected as first argument of 'join'" unless jumps.is_a?(String)
44
+ jumps = jumps.split(',')
45
+ .map(&:strip)
46
+ .map { |j| j.split('<=').map(&:strip) }
47
+ .map { |j| j.size == 1 ? [j[0], j[0]] : j }
44
48
  term = @operands[1]
45
49
  raise "A term expected, but '#{term}' provided" unless term.is_a?(Factbase::Term)
46
- subset = maps.select { |m| term.evaluate(Factbase::Tee.new(Factbase::Fact.new(Mutex.new, m), fact), maps) }
47
- subset.each do |m|
48
- m.each do |k, vv|
49
- vv.each do |v|
50
- fact.send("#{mask.gsub('*', k)}=", v)
50
+ subset = Factbase::Query.new(maps, Mutex.new, term.to_s).each(fact).to_a
51
+ subset.each do |s|
52
+ jumps.each do |to, from|
53
+ s[from]&.each do |v|
54
+ fact.send("#{to}=", v)
51
55
  end
52
56
  end
53
57
  end
data/lib/factbase.rb CHANGED
@@ -81,7 +81,7 @@ require 'yaml'
81
81
  # License:: MIT
82
82
  class Factbase
83
83
  # Current version of the gem (changed by .rultor.yml on every release)
84
- VERSION = '0.0.51'
84
+ VERSION = '0.0.52'
85
85
 
86
86
  # An exception that may be thrown in a transaction, to roll it back.
87
87
  class Rollback < StandardError; end
@@ -31,12 +31,12 @@ require_relative '../../../lib/factbase/syntax'
31
31
  class TestAggregates < Minitest::Test
32
32
  def test_aggregation
33
33
  maps = [
34
- { 'x' => 1, 'y' => 0, 'z' => 4 },
35
- { 'x' => 2, 'y' => 42, 'z' => 3 },
36
- { 'x' => 3, 'y' => 42, 'z' => 5 },
37
- { 'x' => 4, 'y' => 42, 'z' => 2 },
38
- { 'x' => 5, 'y' => 42, 'z' => 1 },
39
- { 'x' => 8, 'y' => 0, 'z' => 6 }
34
+ { 'x' => [1], 'y' => [0], 'z' => [4] },
35
+ { 'x' => [2], 'y' => [42], 'z' => [3] },
36
+ { 'x' => [3], 'y' => [42], 'z' => [5] },
37
+ { 'x' => [4], 'y' => [42], 'z' => [2] },
38
+ { 'x' => [5], 'y' => [42], 'z' => [1] },
39
+ { 'x' => [8], 'y' => [0], 'z' => [6] }
40
40
  ]
41
41
  {
42
42
  '(eq x (agg (eq y 42) (min x)))' => '(eq x 2)',
@@ -54,6 +54,20 @@ class TestAggregates < Minitest::Test
54
54
  end
55
55
  end
56
56
 
57
+ def test_empty
58
+ maps = [
59
+ { 'x' => [1], 'y' => [0], 'z' => [4] },
60
+ { 'x' => [8], 'y' => [0] }
61
+ ]
62
+ {
63
+ '(empty (eq y 42))' => true,
64
+ '(empty (eq x 1))' => false
65
+ }.each do |q, r|
66
+ t = Factbase::Syntax.new(q).to_term
67
+ assert_equal(r, t.evaluate(nil, maps), q)
68
+ end
69
+ end
70
+
57
71
  private
58
72
 
59
73
  def fact(map = {})
@@ -51,19 +51,22 @@ class TestAliases < Minitest::Test
51
51
 
52
52
  def test_join
53
53
  maps = [
54
- { 'x' => [1], 'y' => [0], 'z' => [4] },
54
+ { 'x' => 1, 'y' => 0, 'z' => 4 },
55
55
  { 'x' => [2], 'bar' => [44, 55, 66] }
56
56
  ]
57
57
  {
58
- '(join "foo_*" (gt x 1))' => '(exists foo_x)',
59
- '(join "foo_*" (exists bar))' => '(and (eq foo_bar 44) (eq foo_bar 55))',
60
- '(join "foo_*" (eq fff 1))' => '(absent foo_bar)'
58
+ '(join "foo_x<=x" (gt x 1))' => '(exists foo_x)',
59
+ '(join "foo <=bar " (exists bar))' => '(and (eq foo 44) (eq foo 55))',
60
+ '(join "uuu" (eq x 1))' => '(absent uuu)',
61
+ '(join "uuu <= fff" (eq fff 1))' => '(absent uuu)'
61
62
  }.each do |q, r|
62
63
  t = Factbase::Syntax.new(q).to_term
63
64
  maps.each do |m|
64
65
  f = Factbase::Accum.new(fact(m), {}, false)
66
+ require_relative '../../../lib/factbase/looged'
67
+ f = Factbase::Looged::Fact.new(f, Loog::NULL)
65
68
  next unless t.evaluate(f, maps)
66
- assert(Factbase::Syntax.new(r).to_term.evaluate(f, []), "#{q} -> #{f}")
69
+ assert(Factbase::Syntax.new(r).to_term.evaluate(f, []), "#{q} -> #{f} doesn't match #{r}")
67
70
  end
68
71
  end
69
72
  end
@@ -45,6 +45,14 @@ class TestLooged < Minitest::Test
45
45
  assert_equal(2, fb.size)
46
46
  end
47
47
 
48
+ def test_reading_one
49
+ fb = Factbase::Looged.new(Factbase.new, Loog::NULL)
50
+ fb.insert
51
+ fb.insert.bar = 42
52
+ assert_equal(1, fb.query('(agg (exists bar) (count))').one)
53
+ assert_equal([42], fb.query('(agg (exists bar) (first bar))').one)
54
+ end
55
+
48
56
  def test_with_txn
49
57
  log = Loog::Buffer.new
50
58
  fb = Factbase::Looged.new(Factbase.new, log)
@@ -110,6 +110,21 @@ class TestQuery < Minitest::Test
110
110
  assert_equal(1, maps.size)
111
111
  end
112
112
 
113
+ def test_reading_one
114
+ maps = []
115
+ maps << { 'foo' => [42] }
116
+ maps << { 'bar' => [4, 5] }
117
+ {
118
+ '(agg (exists foo) (first foo))' => [42],
119
+ '(agg (exists z) (first z))' => nil,
120
+ '(agg (always) (count))' => 2,
121
+ '(agg (eq bar $v) (count))' => 1,
122
+ '(agg (eq z 40) (count))' => 0
123
+ }.each do |q, r|
124
+ assert_equal(r, Factbase::Query.new(maps, Mutex.new, q).one(v: 4), "#{q} -> #{r}")
125
+ end
126
+ end
127
+
113
128
  def test_deleting_nothing
114
129
  maps = []
115
130
  maps << { 'foo' => [42] }
@@ -140,6 +155,19 @@ class TestQuery < Minitest::Test
140
155
  assert_equal(1, maps[0].size)
141
156
  end
142
157
 
158
+ def test_with_params
159
+ maps = []
160
+ maps << { 'foo' => [42] }
161
+ maps << { 'foo' => [17] }
162
+ found = 0
163
+ Factbase::Query.new(maps, Mutex.new, '(eq foo $bar)').each(bar: [42]) do
164
+ found += 1
165
+ end
166
+ assert_equal(1, found)
167
+ assert_equal(1, Factbase::Query.new(maps, Mutex.new, '(eq foo $bar)').each(bar: 42).to_a.size)
168
+ assert_equal(0, Factbase::Query.new(maps, Mutex.new, '(eq foo $bar)').each(bar: 555).to_a.size)
169
+ end
170
+
143
171
  def test_with_nil_alias
144
172
  maps = []
145
173
  maps << { 'foo' => [42] }
@@ -53,6 +53,13 @@ class TestRules < Minitest::Test
53
53
  assert(f.to_s.length.positive?)
54
54
  end
55
55
 
56
+ def test_query_one
57
+ fb = Factbase::Rules.new(Factbase.new, '(always)')
58
+ f = fb.insert
59
+ f.foo = 42
60
+ assert_equal(1, fb.query('(agg (eq foo $v) (count))').one(v: 42))
61
+ end
62
+
56
63
  def test_check_only_when_txn_is_closed
57
64
  fb = Factbase::Rules.new(Factbase.new, '(when (exists a) (exists b))')
58
65
  ok = false
@@ -94,6 +101,6 @@ class TestRules < Minitest::Test
94
101
  end
95
102
  end
96
103
  assert(ok)
97
- assert_equal(0, fb.query('(eq hello 42)').each.to_a.size)
104
+ assert_equal(0, fb.query('(eq hello $v)').each(v: 42).to_a.size)
98
105
  end
99
106
  end
@@ -40,4 +40,14 @@ class TestTee < Minitest::Test
40
40
  assert_equal(42, t.foo)
41
41
  assert_equal([13], t['$bar'])
42
42
  end
43
+
44
+ def test_recursively
45
+ map = {}
46
+ prim = Factbase::Fact.new(Mutex.new, map)
47
+ prim.foo = 42
48
+ t = Factbase::Tee.new(nil, { 'bar' => 7 })
49
+ assert_equal(7, t['$bar'])
50
+ t = Factbase::Tee.new(prim, t)
51
+ assert_equal(7, t['$bar'])
52
+ end
43
53
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: factbase
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.51
4
+ version: 0.0.52
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yegor Bugayenko
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-06-18 00:00:00.000000000 Z
11
+ date: 2024-06-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: backtrace