Class: Factbase

Inherits:
Object
  • Object
show all
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

Constructor Details

#initialize(maps = []) ⇒ Factbase

Constructor.

Parameters:

  • maps (Array<Hash>) (defaults to: [])

    Array of facts to start with

[View source]

92
93
94
# File 'lib/factbase.rb', line 92

def initialize(maps = [])
  @maps = maps
end

Instance Method Details

#exportString

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!

Returns:

  • (String)

    Binary string containing serialized data

[View source]

220
221
222
# File 'lib/factbase.rb', line 220

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 be added to them.

Parameters:

  • bytes (String)

    Binary string to import

[View source]

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

#insertFactbase::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.

Returns:

[View source]

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

#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.

Parameters:

  • term (String|Factbase::Term)

    The query to use for selections

  • maps (Array<Hash>|nil) (defaults to: nil)

    The subset of maps (if provided)

[View source]

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

#sizeInteger

Size, the total number of facts in the factbase.

Returns:

  • (Integer)

    How many facts are in there

[View source]

98
99
100
# File 'lib/factbase.rb', line 98

def size
  @maps.size
end

#to_term(query) ⇒ Factbase::Term

Convert a query to a term.

Parameters:

  • query (String)

    The query to convert

Returns:

[View source]

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

#txnFactbase::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.bar = 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.

Returns:

  • (Factbase::Churn)

    How many facts have been changed (zero if rolled back)

[View source]

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