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 +4 -4
- data/.rubocop.yml +1 -1
- data/README.md +13 -6
- data/lib/factbase/syntax.rb +8 -2
- data/lib/factbase/term.rb +1 -1
- data/lib/factbase.rb +88 -8
- data/test/factbase/test_query.rb +1 -1
- data/test/factbase/test_syntax.rb +8 -7
- data/test/factbase/test_term.rb +2 -2
- data/test/factbase/test_tuples.rb +21 -0
- data/test/test_factbase.rb +9 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8ff1db55d5a25e5036d3a98423caedc960f8323681e2227c82e0a397727067c0
|
4
|
+
data.tar.gz: b3000b708bd2eb69005998a722011a296384d5cea66641b975471e611b93e4d0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a377a359a877b1f50c83f13c451e095036b4087cf88e39c04a0e5d64e86337bfef458883646009daff4b2b90446abbdbd563a4921b3ed275b88b7408b69ef0de
|
7
|
+
data.tar.gz: 26a5ca088df353e0c526080213aa4d87b23805dd522617d52d2d738b1881c40ae4c659fa886db4fbe19a30a131ff6b5a7e19aadf5dbafcbd0f6b81954386bd65
|
data/.rubocop.yml
CHANGED
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
|
-
|
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
|
data/lib/factbase/syntax.rb
CHANGED
@@ -44,7 +44,9 @@ class Factbase::Syntax
|
|
44
44
|
def to_term
|
45
45
|
build.simplify
|
46
46
|
rescue StandardError => e
|
47
|
-
|
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
|
-
|
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
data/lib/factbase.rb
CHANGED
@@ -23,9 +23,12 @@
|
|
23
23
|
require 'json'
|
24
24
|
require 'yaml'
|
25
25
|
|
26
|
-
#
|
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.
|
66
|
+
# File.binwrite(file, fb1.export)
|
41
67
|
# fb2 = Factbase.new # it's empty
|
42
|
-
# fb2.import(File.
|
68
|
+
# fb2.import(File.binread(file))
|
43
69
|
#
|
44
|
-
# It's
|
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.
|
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
|
-
|
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
|
data/test/factbase/test_query.rb
CHANGED
@@ -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 (
|
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
|
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
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
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
|
|
data/test/factbase/test_term.rb
CHANGED
@@ -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
|
182
|
-
t = Factbase::Term.new(:
|
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
|
data/test/test_factbase.rb
CHANGED
@@ -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.
|
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-
|
11
|
+
date: 2024-05-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: json
|