factbase 0.0.51 → 0.0.52

Sign up to get free protection for your applications and to get access to all the features.
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