factbase 0.0.38 → 0.0.39
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 +4 -4
- data/README.md +12 -5
- data/lib/factbase.rb +88 -8
- 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/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
|
72
|
+
* `(matches a re)` returns true when `a` matches regular expression `re`
|
73
|
+
|
74
|
+
There are a few terms that return non-boolean values:
|
75
|
+
|
73
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")
|
74
79
|
* `(either a b)` returns `b` if `a` is `nil`
|
75
|
-
* `(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
|
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.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
|
@@ -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
|