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))

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 Yegor Bugayenko

License

MIT

Defined Under Namespace

Classes: Accum, Fact, Flatten, Inv, Looged, Pre, Query, Rollback, Rules, Syntax, Tee, Term, ToJSON, ToXML, ToYAML

Constant Summary collapse

VERSION =

Current version of the gem (changed by .rultor.yml on every release)

'0.4.0'

Instance Method Summary collapse

Constructor Details

#initialize(facts = []) ⇒ Factbase

Constructor.

Parameters:

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

    Array of facts to start with



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

def initialize(facts = [])
  @maps = facts
  @mutex = Mutex.new
end

Instance Method Details

#dupFactbase

Make a deep duplicate of this factbase.

Returns:



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

def dup
  Factbase.new(@maps.map { |m| m.transform_values(&:dup) })
end

#exportBytes

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:

  • (Bytes)

    The chain of bytes



201
202
203
# File 'lib/factbase.rb', line 201

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.

Parameters:

  • bytes (Bytes)

    Byte array to import



216
217
218
219
# File 'lib/factbase.rb', line 216

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:



116
117
118
119
120
121
122
123
# File 'lib/factbase.rb', line 116

def insert
  map = {}
  @mutex.synchronize do
    @maps << map
  end
  require_relative 'factbase/fact'
  Factbase::Fact.new(@mutex, map)
end

#query(query) ⇒ 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:

  • query (String)

    The query to use for selections



143
144
145
146
# File 'lib/factbase.rb', line 143

def query(query)
  require_relative 'factbase/query'
  Factbase::Query.new(@maps, @mutex, query)
end

#sizeInteger

Size, the total number of facts in the factbase.

Returns:

  • (Integer)

    How many facts are in there



104
105
106
# File 'lib/factbase.rb', line 104

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.bar = 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.

Parameters:

  • this (Factbase) (defaults to: self)

    The factbase to use (don’t provide this param)

Returns:

  • (Boolean)

    TRUE if some changes have been made, FALSE otherwise



165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/factbase.rb', line 165

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