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))
Here’s how to use transactions to ensure data consistency:
fb = Factbase.new
# Successful transaction
fb.txn do |fbt|
f = fbt.insert
f.name = 'John'
f.age = 30
# If any error occurs here, all changes will be rolled back
end
# Transaction with rollback
fb.txn do |fbt|
f = fbt.insert
f.name = 'Jane'
f.age = 25
raise Factbase::Rollback # This will undo all changes in this transaction
end
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 (yegor256@gmail.com)
- Copyright
-
Copyright © 2024-2025 Yegor Bugayenko
- License
-
MIT
Defined Under Namespace
Modules: Aggregates, Aliases, CachedTerm, Casting, Debug, Defn, IndexedTerm, Logical, Math, Meta, Ordering, Strings, System Classes: Accum, CachedFact, CachedFactbase, CachedQuery, Churn, Fact, Flatten, IndexedFact, IndexedFactbase, IndexedQuery, Inv, Light, Logged, Pre, Query, Rollback, Rules, SyncFactbase, SyncQuery, Syntax, Tallied, Taped, Tee, Term, ToJSON, ToXML, ToYAML
Constant Summary collapse
- VERSION =
Current version of the gem (changed by .rultor.yml on every release)
'0.9.5'
Instance Method Summary collapse
-
#export ⇒ String
Export it into a chain of bytes.
-
#import(bytes) ⇒ Object
Import from a chain of bytes.
-
#initialize(maps = []) ⇒ Factbase
constructor
Constructor.
-
#insert ⇒ Factbase::Fact
Insert a new fact and return it.
-
#query(term, maps = nil) ⇒ Object
Create a query capable of iterating.
-
#size ⇒ Integer
Size, the total number of facts in the factbase.
-
#to_term(query) ⇒ Factbase::Term
Convert a query to a term.
-
#txn ⇒ Factbase::Churn
Run an ACID transaction, which will either modify the factbase or rollback in case of an error.
Constructor Details
permalink #initialize(maps = []) ⇒ Factbase
Constructor.
92 93 94 |
# File 'lib/factbase.rb', line 92 def initialize(maps = []) @maps = maps end |
Instance Method Details
permalink #export ⇒ String
220 221 222 |
# File 'lib/factbase.rb', line 220 def export Marshal.dump(@maps) end |
permalink #import(bytes) ⇒ Object
235 236 237 238 |
# File 'lib/factbase.rb', line 235 def import(bytes) raise 'Empty input, cannot load a factbase' if bytes.empty? @maps += Marshal.load(bytes) end |
permalink #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.
110 111 112 113 114 115 |
# File 'lib/factbase.rb', line 110 def insert map = {} @maps << map require_relative 'factbase/fact' Factbase::Fact.new(map) end |
permalink #query(term, maps = nil) ⇒ 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 bar 200)
(absent zzz)))
The full list of terms available in the query you can find in the README.md
file of the repository.
136 137 138 139 140 141 |
# File 'lib/factbase.rb', line 136 def query(term, maps = nil) maps ||= @maps term = to_term(term) if term.is_a?(String) require_relative 'factbase/query' Factbase::Query.new(maps, term, self) end |
permalink #size ⇒ Integer
Size, the total number of facts in the factbase.
98 99 100 |
# File 'lib/factbase.rb', line 98 def size @maps.size end |
permalink #to_term(query) ⇒ Factbase::Term
Convert a query to a term.
146 147 148 149 |
# File 'lib/factbase.rb', line 146 def to_term(query) require_relative 'factbase/syntax' Factbase::Syntax.new(query).to_term end |
permalink #txn ⇒ Factbase::Churn
Run an ACID transaction, which will either modify the factbase or rollback in case of an error.
If necessary to terminate a transaction and rollback all changes, you should raise the Factbase::Rollback
exception:
fb = Factbase.new
fb.txn do |fbt|
fbt.insert. = 42
raise Factbase::Rollback
end
At the end of this script, the factbase will be empty. No facts will be inserted and all changes that happened in the block will be rolled back.
167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 |
# File 'lib/factbase.rb', line 167 def txn pairs = {} before = @maps.map do |m| n = m.transform_values(&:dup) # rubocop:disable Lint/HashCompareByIdentity pairs[n.object_id] = m.object_id # rubocop:enable Lint/HashCompareByIdentity n end require_relative 'factbase/taped' taped = Factbase::Taped.new(before) begin require_relative 'factbase/light' yield Factbase::Light.new(Factbase.new(taped)) rescue Factbase::Rollback return 0 end require_relative 'factbase/churn' churn = Factbase::Churn.new taped.inserted.each do |oid| b = taped.find_by_object_id(oid) next if b.nil? @maps << b churn.append(1, 0, 0) end garbage = [] taped.added.each do |oid| b = taped.find_by_object_id(oid) next if b.nil? garbage << pairs[oid] @maps << b churn.append(0, 0, 1) end taped.deleted.each do |oid| garbage << pairs[oid] churn.append(0, 1, 0) end @maps.delete_if { |m| garbage.include?(m.object_id) } churn end |