Single-Table NoSQL-ish In-Memory Database
This Ruby gem manages an in-memory database of facts. A fact is simply a map of properties and values. The values are either atomic literals or non-empty sets of literals. It is possible to delete a fact, but impossible to delete a property from a fact.
ATTENTION: The current implemention is naive and,
because of that, very slow. I will be very happy
if you suggest a better implementation without the change of the interface.
The Factbase::query()
method is what mostly needs performance optimization:
currently it simply iterates through all facts in the factbase in order
to find those that match the provided terms. Obviously,
even a simple indexing may significantly increase performance.
Here is how you use it (it's thread-safe, by the way):
fb = Factbase.new
f = fb.insert
f.kind = 'book'
f.title = 'Object Thinking'
fb.query('(eq kind "book")').each do |f|
f.seen = true
end
fb.insert
fb.query('(not (exists seen))').each do |f|
f.title = 'Elegant Objects'
end
You can save the factbase to the disc and then load it back:
file = '/tmp/simple.fb'
f1 = Factbase.new
f = f1.insert
f.foo = 42
File.save(file, f1.export)
f2 = Factbase.new
f2.import(File.read(file))
assert(f2.query('(eq foo 42)').each.to_a.size == 1)
There are some boolean terms available in a query
(they return either true
or false
):
(always)
and(never)
aretrue
andfalse
(nil v)
istrue
ifv
isnil
(not b)
is the inverse ofb
(or b1 b2 ...)
istrue
if at least one argument istrue
(and b1 b2 ...)
— if all arguments aretrue
(when b1 b2)
— ifb1
istrue
andb2
istrue
orb1
isfalse
(exists p)
— ifp
property exists(absent p)
— ifp
property is absent(zero v)
— if anyv
equals to zero(eq v1 v2)
— if anyv1
equals to anyv2
(lt v1 v2)
— if anyv1
is less than anyv2
(gt v1 v2)
— if anyv1
is greater than anyv2
(many v)
— ifv
has many values(one v)
— ifv
has one value
There are string manipulators:
(concat v1 v2 v3 ...)
— concatenates allv
(sprintf v v1 v2 ...)
— creates a string byv
format with params(matches v s)
— if anyv
matches thes
regular expression
There are a few terms that return non-boolean values:
(at i v)
is thei
-th value ofv
(size v)
is the cardinality ofv
(zero ifv
isnil
)(type v)
is the type ofv
("String"
,"Integer"
,"Float"
,"Time"
, or"Array"
)(either v1 v1)
isv2
ifv1
isnil
It's possible to modify the facts retrieved, on fly:
(as p v)
adds propertyp
with the valuev
(join s t)
adds properties named by thes
mask with the values retrieved by thet
term, for example,(join "x<=foo,y<=bar" (gt x 5))
will addx
andy
properties, setting them to values found in thefoo
andbar
properties in the facts that match(gt x 5)
Also, some simple arithmetic:
(plus v1 v2)
is a sum of∑v1
and∑v2
(minus v1 v2)
is a deducation of∑v2
from∑v1
(times v1 v2)
is a multiplication of∏v1
and∏v2
(div v1 v2)
is a division of∏v1
by∏v2
It's possible to add and deduct string values to time values, like
(plus t '2 days')
or (minus t '14 hours')
.
Types may be converted:
(to_int v)
is an integer ofv
(to_str v)
is a string ofv
(to_float v)
is a float ofv
One term is for meta-programming:
(defn f "self.to_s")
defines a new term using Ruby syntax and returnstrue
(undef f)
undefines a term (nothing happens if it's not defined yet), returnstrue
There are terms that are history of search aware:
(prev p)
returns the value ofp
property in the previously seen fact(unique p)
returns true if the value ofp
property hasn't been seen yet
The agg
term enables sub-queries by evaluating the first argument (term)
over all available facts, passing the entire subset to the second argument,
and then returning the result as an atomic value:
(lt age (agg (eq gender 'F') (max age)))
selects all facts where theage
is smaller than the maximumage
of all women(eq id (agg (always) (max id)))
selects the fact with the largestid
(eq salary (agg (eq dept $dept) (avg salary)))
selects the facts with the salary average in their departments
There are also terms that match the entire factbase
and must be used primarily inside the (agg ..)
term:
(nth v p)
returns thep
property of the v-th fact (must be a positive integer)(first p)
returns thep
property of the first fact(count)
returns the tally of facts(max p)
returns the maximum value of thep
property in all facts(min p)
returns the minimum(sum p)
returns the arithmetic sum of all values of thep
property
It's also possible to use a sub-query in a shorter form than with the agg
:
(empty q)
is true if the subqueryq
is empty
There are some system-level terms:
(env v1 v2)
returns the value of environment variablev1
or the stringv2
if it's not set
How to contribute
Read these guidelines. Make sure you build is green before you contribute your pull request. You will need to have Ruby 3.2+ and Bundler installed. Then:
bundle update
bundle exec rake
If it's clean and you don't see any error messages, submit your pull request.