factbase 0.0.37 → 0.0.39

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: a464f6fa40a3aef098e4ab4a58b6f6905ac101421f22fb806b63c4fb624aa610
4
- data.tar.gz: ad19a0147890d8b4ff8fff96f7e9f47b09ecb264b3cb52fa606be892278fa380
3
+ metadata.gz: 8ff1db55d5a25e5036d3a98423caedc960f8323681e2227c82e0a397727067c0
4
+ data.tar.gz: b3000b708bd2eb69005998a722011a296384d5cea66641b975471e611b93e4d0
5
5
  SHA512:
6
- metadata.gz: 6abb74b759ef7eea421e8a7f4f1ebdbf7ae46836dfefcd324286508fbb82e207a787265c0cf9f8931b43b00d50e5df983266fa9442f9c277111dfa9ea3a778de
7
- data.tar.gz: b0ed97f83f59230bafce810488f3e720d098a748149f7fbd58b7f1c25425b2fba6b742ce16b18692df34607eb3c42f9f11196ca212bc0273d7bdffe15e759efe
6
+ metadata.gz: a377a359a877b1f50c83f13c451e095036b4087cf88e39c04a0e5d64e86337bfef458883646009daff4b2b90446abbdbd563a4921b3ed275b88b7408b69ef0de
7
+ data.tar.gz: 26a5ca088df353e0c526080213aa4d87b23805dd522617d52d2d738b1881c40ae4c659fa886db4fbe19a30a131ff6b5a7e19aadf5dbafcbd0f6b81954386bd65
data/.rubocop.yml CHANGED
@@ -43,7 +43,7 @@ Metrics/BlockLength:
43
43
  Metrics/CyclomaticComplexity:
44
44
  Max: 25
45
45
  Metrics/PerceivedComplexity:
46
- Max: 25
46
+ Max: 30
47
47
  Metrics/ClassLength:
48
48
  Enabled: false
49
49
  Layout/EmptyLineAfterGuardClause:
data/README.md CHANGED
@@ -54,7 +54,8 @@ f2.import(File.read(file))
54
54
  assert(f2.query('(eq foo 42)').each.to_a.size == 1)
55
55
  ```
56
56
 
57
- There are some terms available in a query:
57
+ There are some boolean terms available in a query
58
+ (they return either TRUE or FALSE):
58
59
 
59
60
  * `(always)` and `(never)` are "true" and "false"
60
61
  * `(not t)` inverses the `t` if it's boolean (exception otherwise)
@@ -66,14 +67,16 @@ There are some terms available in a query:
66
67
  * `(eq a b)` returns true if `a` equals to `b`
67
68
  * `(lt a b)` returns true if `a` is less than `b`
68
69
  * `(gt a b)` returns true if `a` is greater than `b`
69
- * `(size k)` returns cardinality of `k` property (zero if property is absent)
70
- * `(type a)` returns type of `a` ("String", "Integer", "Float", or "Time")
71
70
  * `(many a)` return true if there are many values in the `a` property
72
71
  * `(one a)` returns true if there is only one value in the `a` property
73
- * `(at i a)` returns the `i`-th value of the `a` property
74
- * `(nonil a b)` returns `b` if `a` is `nil`
75
72
  * `(matches a re)` returns true when `a` matches regular expression `re`
76
- * `(defn foo "self.to_s")` defines a new term using Ruby syntax and returns true
73
+
74
+ There are a few terms that return non-boolean values:
75
+
76
+ * `(at i a)` returns the `i`-th value of the `a` property
77
+ * `(size k)` returns cardinality of `k` property (zero if property is absent)
78
+ * `(type a)` returns type of `a` ("String", "Integer", "Float", or "Time")
79
+ * `(either a b)` returns `b` if `a` is `nil`
77
80
 
78
81
  Also, some simple arithmetic:
79
82
 
@@ -82,6 +85,10 @@ Also, some simple arithmetic:
82
85
  * `(times a b)` is a multiplication of `a` and `b`
83
86
  * `(div a b)` is a division of `a` by `b`
84
87
 
88
+ One term is for meta-programming:
89
+
90
+ * `(defn foo "self.to_s")` defines a new term using Ruby syntax and returns true
91
+
85
92
  There are terms that are history of search aware:
86
93
 
87
94
  * `(prev a)` returns the value of `a` in the previously seen fact
@@ -44,7 +44,9 @@ class Factbase::Syntax
44
44
  def to_term
45
45
  build.simplify
46
46
  rescue StandardError => e
47
- raise "#{e.message} in \"#{@query}\""
47
+ err = "#{e.message} in #{@query}"
48
+ err = "#{err}, tokens: #{@tokens}" unless @tokens.nil?
49
+ raise err
48
50
  end
49
51
 
50
52
  private
@@ -95,7 +97,11 @@ class Factbase::Syntax
95
97
  list = []
96
98
  acc = ''
97
99
  string = false
98
- @query.to_s.gsub(/#.*$/, '').chars.each do |c|
100
+ comment = false
101
+ @query.to_s.chars.each do |c|
102
+ comment = true if !string && c == '#'
103
+ comment = false if comment && c == "\n"
104
+ next if comment
99
105
  if ['\'', '"'].include?(c)
100
106
  if string && acc[acc.length - 1] == '\\'
101
107
  acc = acc[0..-2]
data/lib/factbase/term.rb CHANGED
@@ -158,7 +158,7 @@ class Factbase::Term
158
158
  by_symbol(0, fact).nil?
159
159
  end
160
160
 
161
- def nonil(fact, maps)
161
+ def either(fact, maps)
162
162
  assert_args(2)
163
163
  v = the_values(0, fact, maps)
164
164
  return v unless v.nil?
data/lib/factbase.rb CHANGED
@@ -23,9 +23,12 @@
23
23
  require 'json'
24
24
  require 'yaml'
25
25
 
26
- # Factbase.
26
+ # A factbase, which essentially is a NoSQL one-table in-memory database
27
+ # with a Lisp-ish query interface.
27
28
  #
28
- # This is an entry point to a factbase:
29
+ # This class is an entry point to a factbase. For example, this is how you
30
+ # add a new "fact" to a factbase, then put two properties into it, and then
31
+ # find this fact with a simple search.
29
32
  #
30
33
  # fb = Factbase.new
31
34
  # f = fb.insert # new fact created
@@ -34,14 +37,41 @@ require 'yaml'
34
37
  # found = f.query('(gt 20 age)').each.to_a[0]
35
38
  # assert(found.age == 42)
36
39
  #
40
+ # Every fact is a key-value hash map. Every value is a non-empty set of values.
41
+ # Consider this example of creating a factbase with a single fact inside:
42
+ #
43
+ # fb = Factbase.new
44
+ # f = fb.insert
45
+ # f.name = 'Jeff'
46
+ # f.name = 'Walter'
47
+ # f.age = 42
48
+ # f.age = 'unknown'
49
+ # f.place = 'LA'
50
+ # puts f.to_json
51
+ #
52
+ # This will print the following JSON:
53
+ #
54
+ # {
55
+ # 'name': ['Jeff', 'Walter'],
56
+ # 'age': [42, 'unknown'],
57
+ # 'place: 'LA'
58
+ # }
59
+ #
60
+ # Value sets, as you can see, allow data of different types. However, there
61
+ # are only four types are allowed: Integer, Float, String, and Time.
62
+ #
37
63
  # A factbase may be exported to a file and then imported back:
38
64
  #
39
65
  # fb1 = Factbase.new
40
- # File.writebin(file, fb1.export)
66
+ # File.binwrite(file, fb1.export)
41
67
  # fb2 = Factbase.new # it's empty
42
- # fb2.import(File.readbin(file))
68
+ # fb2.import(File.binread(file))
43
69
  #
44
- # It's important to use +writebin+ and +readbin+, because the content is
70
+ # It's impossible to delete properties of a fact. It is however possible to
71
+ # delete the entire fact, with the help of the +query()+ and then +delete!()+
72
+ # methods.
73
+ #
74
+ # It's important to use +binwrite+ and +binread+, because the content is
45
75
  # a chain of bytes, not a text.
46
76
  #
47
77
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
@@ -49,7 +79,10 @@ require 'yaml'
49
79
  # License:: MIT
50
80
  class Factbase
51
81
  # Current version of the gem (changed by .rultor.yml on every release)
52
- VERSION = '0.0.37'
82
+ VERSION = '0.0.39'
83
+
84
+ # An exception that may be thrown in a transaction, to roll it back.
85
+ class Rollback < StandardError; end
53
86
 
54
87
  # Constructor.
55
88
  def initialize(facts = [])
@@ -69,7 +102,13 @@ class Factbase
69
102
  @maps.size
70
103
  end
71
104
 
72
- # Insert a new fact.
105
+ # Insert a new fact and return it.
106
+ #
107
+ # A fact, when inserted, is empty. It doesn't contain any properties.
108
+ #
109
+ # The operation is thread-safe, meaning that you different threads may
110
+ # insert facts parallel without breaking the consistency of the factbase.
111
+ #
73
112
  # @return [Factbase::Fact] The fact just inserted
74
113
  def insert
75
114
  require_relative 'factbase/fact'
@@ -94,6 +133,9 @@ class Factbase
94
133
  # (gt bar 200)
95
134
  # (absent zzz)))
96
135
  #
136
+ # The full list of terms available in the query you can find in the
137
+ # +README.md+ file of the repository.
138
+ #
97
139
  # @param [String] query The query to use for selections
98
140
  def query(query)
99
141
  require_relative 'factbase/query'
@@ -102,9 +144,27 @@ class Factbase
102
144
 
103
145
  # Run an ACID transaction, which will either modify the factbase
104
146
  # or rollback in case of an error.
147
+ #
148
+ # If necessary to terminate a transaction and roolback all changes,
149
+ # you should raise the +Factbase::Rollback+ exception:
150
+ #
151
+ # fb = Factbase.new
152
+ # fb.txn do |fbt|
153
+ # fbt.insert.bar = 42
154
+ # raise Factbase::Rollback
155
+ # end
156
+ #
157
+ # A the end of this script, the factbase will be empty. No facts will
158
+ # inserted and all changes that happened in the block will be rolled back.
159
+ #
160
+ # @param [Factbase] this The factbase to use (don't provide this param)
105
161
  def txn(this = self)
106
162
  copy = this.dup
107
- yield copy
163
+ begin
164
+ yield copy
165
+ rescue Factbase::Rollback
166
+ return
167
+ end
108
168
  @mutex.synchronize do
109
169
  after = Marshal.load(copy.export)
110
170
  after.each_with_index do |m, i|
@@ -117,11 +177,31 @@ class Factbase
117
177
  end
118
178
 
119
179
  # Export it into a chain of bytes.
180
+ #
181
+ # Here is how you can export it to a file, for example:
182
+ #
183
+ # fb = Factbase.new
184
+ # fb.insert.foo = 42
185
+ # File.binwrite("foo.fb", fb.export)
186
+ #
187
+ # The data is binary, it's not a text!
188
+ #
189
+ # @return [Bytes] The chain of bytes
120
190
  def export
121
191
  Marshal.dump(@maps)
122
192
  end
123
193
 
124
194
  # Import from a chain of bytes.
195
+ #
196
+ # Here is how you can read it from a file, for example:
197
+ #
198
+ # fb = Factbase.new
199
+ # fb.import(File.binread("foo.fb"))
200
+ #
201
+ # The facts that existed in the factbase before importing will remain there.
202
+ # The facts from the incoming byte stream will added to them.
203
+ #
204
+ # @param [Bytes] bytes Byte array to import
125
205
  def import(bytes)
126
206
  @maps += Marshal.load(bytes)
127
207
  end
@@ -59,7 +59,7 @@ class TestQuery < Minitest::Test
59
59
  '(when (eq num 0) (exists time))' => 2,
60
60
  '(many num)' => 1,
61
61
  '(one num)' => 2,
62
- '(gt num (minus 1 (nonil (at 0 (prev num)) 0)))' => 3,
62
+ '(gt num (minus 1 (either (at 0 (prev num)) 0)))' => 3,
63
63
  '(and (not (many num)) (eq num (plus 21 +21)))' => 1,
64
64
  '(and (not (many num)) (eq num (minus -100 -142)))' => 1,
65
65
  '(and (one num) (eq num (times 7 6)))' => 1,
@@ -42,10 +42,12 @@ class TestSyntax < Minitest::Test
42
42
  def test_simple_parsing
43
43
  [
44
44
  '(foo)',
45
- '(foo (bar) (zz 77) )',
45
+ '(foo (bar) (zz 77) ) # hey',
46
+ "# hello\n\n\n(foo (bar))",
46
47
  "(eq foo \n\n 'Hello, world!'\n)\n",
47
48
  "(eq x 'Hello, \\' \n) \\' ( world!')",
48
49
  "# this is a comment\n(eq foo # test\n 42)\n\n# another comment\n",
50
+ "(foo 'Hello,\n\nworld!\r\t\n')\n",
49
51
  "(or ( a 4) (b 5) (always) (and (always) (c 5) \t\t(r 7 w8s w8is 'Foo')))"
50
52
  ].each do |q|
51
53
  Factbase::Syntax.new(q).to_term
@@ -60,7 +62,7 @@ class TestSyntax < Minitest::Test
60
62
  "(r 'Dude\\'s Friend')",
61
63
  "(r 'I\\'m \\\"good\\\"')",
62
64
  '(foo x y z)',
63
- "(foo x y z t f 42 'Hi!' 33)",
65
+ "(foo x y z t f 42 'Hi!# you' 33)",
64
66
  '(foo (x) y z)',
65
67
  '(eq t 2024-05-25T19:43:48Z)',
66
68
  '(eq t 2024-05-25T19:43:48Z)',
@@ -106,11 +108,10 @@ class TestSyntax < Minitest::Test
106
108
  ')',
107
109
  '"'
108
110
  ].each do |q|
109
- assert(
110
- assert_raises(q) do
111
- Factbase::Syntax.new(q).to_term
112
- end.message.include?(q)
113
- )
111
+ msg = assert_raises(q) do
112
+ Factbase::Syntax.new(q).to_term
113
+ end.message
114
+ assert(msg.include?(q), msg)
114
115
  end
115
116
  end
116
117
 
@@ -178,8 +178,8 @@ class TestTerm < Minitest::Test
178
178
  assert_equal(5, t.evaluate(fact('foo' => [4, 5]), []))
179
179
  end
180
180
 
181
- def test_nonil
182
- t = Factbase::Term.new(:nonil, [Factbase::Term.new(:at, [5, :foo]), 42])
181
+ def test_either
182
+ t = Factbase::Term.new(:either, [Factbase::Term.new(:at, [5, :foo]), 42])
183
183
  assert_equal([42], t.evaluate(fact('foo' => 4), []))
184
184
  end
185
185
 
@@ -82,4 +82,25 @@ class TestTuples < Minitest::Test
82
82
  assert_equal(1, fb.query('(exists bar)').each.to_a.size)
83
83
  assert_equal(1, fb.query('(exists xyz)').each.to_a.size)
84
84
  end
85
+
86
+ def test_with_chaining
87
+ fb = Factbase.new
88
+ f1 = fb.insert
89
+ f1.name = 'Jeff'
90
+ f1.friend = 'Walter'
91
+ f2 = fb.insert
92
+ f2.name = 'Walter'
93
+ f2.group = 1
94
+ f3 = fb.insert
95
+ f3.name = 'Donny'
96
+ f3.group = 1
97
+ tuples = Factbase::Tuples.new(
98
+ fb, ['(eq name "Jeff")', '(eq name "{f0.friend}")', '(eq group {f1.group})']
99
+ )
100
+ tuples.each do |fs|
101
+ assert_equal('Walter', fs[1].name)
102
+ assert(%w[Walter Donny].include?(fs[2].name))
103
+ end
104
+ assert_equal(2, tuples.each.to_a.size)
105
+ end
85
106
  end
@@ -140,4 +140,13 @@ class TestFactbase < Minitest::Test
140
140
  end
141
141
  assert_equal(1, fb.query('(exists xyz)').each.to_a.size)
142
142
  end
143
+
144
+ def test_txn_with_rollback
145
+ fb = Factbase.new
146
+ fb.txn do |fbt|
147
+ fbt.insert.bar = 33
148
+ raise Factbase::Rollback
149
+ end
150
+ assert_equal(0, fb.query('(always)').each.to_a.size)
151
+ end
143
152
  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.37
4
+ version: 0.0.39
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-05-27 00:00:00.000000000 Z
11
+ date: 2024-05-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json