Class: Factbase
- Inherits:
-
Object
- Object
- Factbase
- Defined in:
- lib/factbase.rb
Overview
A factbase, which essentially is a NoSQL one-table in-memory database with a Lisp-ish query interface.
This class is an entry point to a factbase. For example, this is how you add a new “fact” to a factbase, then put two properties into it, and then find this fact with a simple search.
fb = Factbase.new
f = fb.insert # new fact created
f.name = 'Jeff Lebowski'
f.age = 42
found = f.query('(gt 20 age)').each.to_a[0]
assert(found.age == 42)
Every fact is a key-value hash map. Every value is a non-empty set of values. Consider this example of creating a factbase with a single fact inside:
fb = Factbase.new
f = fb.insert
f.name = 'Jeff'
f.name = 'Walter'
f.age = 42
f.age = 'unknown'
f.place = 'LA'
puts f.to_json
This will print the following JSON:
{
'name': ['Jeff', 'Walter'],
'age': [42, 'unknown'],
'place: 'LA'
}
Value sets, as you can see, allow data of different types. However, there are only four types are allowed: Integer, Float, String, and Time.
A factbase may be exported to a file and then imported back:
fb1 = Factbase.new
File.binwrite(file, fb1.export)
fb2 = Factbase.new # it's empty
fb2.import(File.binread(file))
It’s impossible to delete properties of a fact. It is however possible to delete the entire fact, with the help of the query() and then delete!() methods.
It’s important to use binwrite and binread, because the content is a chain of bytes, not a text.
It is NOT thread-safe!
- Author
-
Yegor Bugayenko ([email protected])
- Copyright
-
Copyright © 2024-2025 Yegor Bugayenko
- License
-
MIT
Defined Under Namespace
Classes: Accum, Fact, Flatten, Inv, Looged, Pre, Query, QueryOnce, Rollback, Rules, Syntax, Tee, Term, TermOnce, ToJSON, ToXML, ToYAML
Constant Summary collapse
- VERSION =
Current version of the gem (changed by .rultor.yml on every release)
'0.5.2'
Instance Attribute Summary collapse
-
#cache ⇒ Object
readonly
Returns the value of attribute cache.
Instance Method Summary collapse
-
#dup ⇒ Factbase
Make a deep duplicate of this factbase.
-
#export ⇒ Bytes
Export it into a chain of bytes.
-
#import(bytes) ⇒ Object
Import from a chain of bytes.
-
#initialize(facts = []) ⇒ Factbase
constructor
Constructor.
-
#insert ⇒ Factbase::Fact
Insert a new fact and return it.
-
#query(query, maps = @maps) ⇒ Object
Create a query capable of iterating.
-
#size ⇒ Integer
Size, the total number of facts in the factbase.
-
#txn(this = self) ⇒ Boolean
Run an ACID transaction, which will either modify the factbase or rollback in case of an error.
Constructor Details
#initialize(facts = []) ⇒ Factbase
Constructor.
93 94 95 96 97 |
# File 'lib/factbase.rb', line 93 def initialize(facts = []) @maps = facts @mutex = Mutex.new @cache = {} end |
Instance Attribute Details
#cache ⇒ Object (readonly)
Returns the value of attribute cache.
89 90 91 |
# File 'lib/factbase.rb', line 89 def cache @cache end |
Instance Method Details
#dup ⇒ Factbase
Make a deep duplicate of this factbase.
101 102 103 |
# File 'lib/factbase.rb', line 101 def dup Factbase.new(@maps.map { |m| m.transform_values(&:dup) }) end |
#export ⇒ Bytes
Export it into a chain of bytes.
Here is how you can export it to a file, for example:
fb = Factbase.new
fb.insert.foo = 42
File.binwrite("foo.fb", fb.export)
The data is binary, it’s not a text!
211 212 213 |
# File 'lib/factbase.rb', line 211 def export Marshal.dump(@maps) end |
#import(bytes) ⇒ Object
Import from a chain of bytes.
Here is how you can read it from a file, for example:
fb = Factbase.new
fb.import(File.binread("foo.fb"))
The facts that existed in the factbase before importing will remain there. The facts from the incoming byte stream will added to them.
226 227 228 229 |
# File 'lib/factbase.rb', line 226 def import(bytes) raise 'Empty input, cannot load a factbase' if bytes.empty? @maps += Marshal.load(bytes) end |
#insert ⇒ Factbase::Fact
Insert a new fact and return it.
A fact, when inserted, is empty. It doesn’t contain any properties.
The operation is thread-safe, meaning that you different threads may insert facts parallel without breaking the consistency of the factbase.
119 120 121 122 123 124 125 126 127 |
# File 'lib/factbase.rb', line 119 def insert map = {} @mutex.synchronize do @maps << map end @cache.clear require_relative 'factbase/fact' Factbase::Fact.new(self, @mutex, map) end |
#query(query, maps = @maps) ⇒ Object
Create a query capable of iterating.
There is a Lisp-like syntax, for example:
(eq title 'Object Thinking')
(gt time 2024-03-23T03:21:43Z)
(gt cost 42)
(exists seenBy)
(and
(eq foo 42.998)
(or
(gt 200)
(absent zzz)))
The full list of terms available in the query you can find in the README.md file of the repository.
148 149 150 151 152 153 154 155 156 |
# File 'lib/factbase.rb', line 148 def query(query, maps = @maps) require_relative 'factbase/query' require_relative 'factbase/query_once' Factbase::QueryOnce.new( self, Factbase::Query.new(self, maps, @mutex, query), maps ) end |
#size ⇒ Integer
Size, the total number of facts in the factbase.
107 108 109 |
# File 'lib/factbase.rb', line 107 def size @maps.size end |
#txn(this = self) ⇒ Boolean
Run an ACID transaction, which will either modify the factbase or rollback in case of an error.
If necessary to terminate a transaction and roolback all changes, you should raise the Factbase::Rollback exception:
fb = Factbase.new
fb.txn do |fbt|
fbt.insert. = 42
raise Factbase::Rollback
end
A the end of this script, the factbase will be empty. No facts will inserted and all changes that happened in the block will be rolled back.
175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 |
# File 'lib/factbase.rb', line 175 def txn(this = self) copy = this.dup begin yield copy rescue Factbase::Rollback return false end modified = false @mutex.synchronize do after = Marshal.load(copy.export) after.each_with_index do |m, i| if i >= @maps.size @maps << {} modified = true end m.each do |k, vv| next if @maps[i][k] == vv @maps[i][k] = vv modified = true end end end modified end |