Class: Factbase

Inherits:
Object
  • Object
show all
Defined in:
lib/factbase.rb,
lib/factbase/version.rb

Overview

Just version.

Author

Yegor Bugayenko (yegor256@gmail.com)

Copyright

Copyright © 2024-2026 Yegor Bugayenko

License

MIT

Defined Under Namespace

Modules: CachedTerm, IndexedTerm Classes: Absent, Accum, Agg, Always, And, Arithmetic, As, Assert, Best, Boolean, CachedFact, CachedFactbase, CachedQuery, Churn, Compare, Concat, Contains, Count, Defn, Div, Either, Empty, EndsWith, Env, Eq, Exists, Fact, FactAsYaml, First, Flatten, Fuzz, Gt, Gte, Head, Impatient, IndexedAbsent, IndexedAnd, IndexedEq, IndexedExists, IndexedFact, IndexedFactbase, IndexedGt, IndexedGte, IndexedLt, IndexedLte, IndexedNot, IndexedOne, IndexedOr, IndexedQuery, IndexedUnique, Inv, Inverted, Join, LazyTaped, Light, Logged, Lt, Lte, Many, Matches, Max, Min, Minus, Never, Nil, Not, Nth, One, Or, Plus, Pre, Prev, Query, Rollback, Rules, Simplified, Size, Sorted, Sprintf, StartsWith, Sum, SyncFactbase, SyncQuery, Syntax, Tallied, Taped, Tee, Term, TermBase, Times, ToFloat, ToInteger, ToJSON, ToString, ToTime, ToXML, ToYAML, Traced, Type, Undef, Unique, When, Zero

Constant Summary collapse

VERSION =
'0.19.12'

Instance Method Summary collapse

Constructor Details

#initialize(maps = []) ⇒ Factbase

Constructor.

Parameters:

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

    Array of facts to start with



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

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

Instance Method Details

#each {|fact| ... } ⇒ Integer, Enumerator

Iterate over all facts yielding plain hashes.

Yield Parameters:

  • fact (Hash)

    Each fact as a plain Hash

Returns:

  • (Integer, Enumerator)

    Total number of facts or Enumerator



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

def each(&)
  @maps.each(&)
end

#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



226
227
228
# File 'lib/factbase.rb', line 226

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.

This method supports both the original format (Array of maps) and the IndexedFactbase format (Hash with :maps and :idx keys).

SECURITY: the input must come from a source you trust, because it is deserialized with Marshal.load. Loading a Marshal stream crafted by an attacker can execute arbitrary code in the calling process, so never call import on bytes received over the network, read from a user-supplied path, or pulled from any other untrusted channel without an out-of-band integrity check. See the official Ruby security notes for Marshal.load at docs.ruby-lang.org/en/3.3/security_rdoc.html#label-Marshal.load for details.

Parameters:

  • bytes (String)

    Binary string to import

Raises:

  • (StandardError)


254
255
256
257
258
259
260
261
262
263
# File 'lib/factbase.rb', line 254

def import(bytes)
  raise(StandardError, 'Empty input, cannot load a factbase') if bytes.empty?
  data = Marshal.load(bytes)
  @maps +=
    if data.is_a?(Hash) && data.key?(:maps)
      Marshal.load(data[:maps])
    else
      data
    end
end

#insertFactbase::Fact

Insert a new fact and return it.

A fact, when inserted, is empty. It doesn’t contain any properties.

Returns:



113
114
115
116
117
118
# File 'lib/factbase.rb', line 113

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)



139
140
141
142
143
144
# File 'lib/factbase.rb', line 139

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



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

def size
  @maps.size
end

#to_term(query) ⇒ Factbase::Term

Convert a query to a term.

Parameters:

  • query (String)

    The query to convert

Returns:



149
150
151
152
# File 'lib/factbase.rb', line 149

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)



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
208
209
210
211
212
213
# File 'lib/factbase.rb', line 170

def txn
  require_relative('factbase/lazy_taped')
  taped = Factbase::LazyTaped.new(@maps)
  require_relative('factbase/churn')
  churn = Factbase::Churn.new
  catch(:commit) do
    require_relative('factbase/light')
    commit = false
    catch(:rollback) do
      yield(Factbase::Light.new(Factbase.new(taped)))
      commit = true
    end
    return churn unless commit
  rescue Factbase::Rollback
    return churn
  end
  seen = {}.compare_by_identity
  garbage = {}.compare_by_identity
  taped.deleted.each do |oid|
    original = @maps.find { |m| m.object_id == oid }
    next if original.nil?
    garbage[original] = true
    churn.append(0, 1, 0)
  end
  taped.inserted.each do |oid|
    b = taped.find_by_object_id(oid)
    next if b.nil?
    next if seen.key?(b)
    seen[b] = true
    @maps << b
    churn.append(1, 0, 0)
  end
  taped.added.each do |oid|
    b = taped.find_by_object_id(oid)
    next if b.nil?
    next if seen.key?(b)
    original = taped.source_of(b)
    garbage[original] = true if original
    @maps << b
    churn.append(0, 0, 1)
  end
  @maps.delete_if { |m| garbage.key?(m) } unless garbage.empty?
  churn
end