factbase 0.0.38 → 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: f3c7e0abf7bec6e53f050b4eeaeddd2b9954ee77b2a5f9a0332394c995d91ba9
4
- data.tar.gz: 5da4b8574d7b1a93fd67f3d833bf51952eb5b3d9c303b9e20a084dc5953d71bc
3
+ metadata.gz: 8ff1db55d5a25e5036d3a98423caedc960f8323681e2227c82e0a397727067c0
4
+ data.tar.gz: b3000b708bd2eb69005998a722011a296384d5cea66641b975471e611b93e4d0
5
5
  SHA512:
6
- metadata.gz: f472603e32966c098979c2a461d94cd60f1f6213c4cea4abae225694d59ae88df252bbd42dcfb3292efb97352618cfa35fe56bef86bf87310dc74b4dbce65a20
7
- data.tar.gz: fb91b271a5d57c59268dd386f8960e1c3c3c1577ea80e5c9bc3b623eaa6c559b15ce326f23ec9035e795d08ce647af353f4ae12aef5a34a124286f41fdf79130
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
- # 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.38'
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
@@ -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.38
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