1.25 Facts

Facts are triples in the form {subject, relation, object}. They are stored in a database. To create an empty database call Facts.database(). To query the environment database (global database with all program symbols and operators) use Facts.env_db().

Use Facts.add_tags to add a triple to a database and Facts.remove_tags to remove a triple from a database.

Facts can be queried in order from queries in triples. If you only need one triple you can use Facts.with_tags or Facts.collect_with_tags. If you need a join query you can use Facts.with or Facts.collect_with to query multiple triples at the same time. These functions initialize internally an iterator for the query and then call the iterator in a loop calling a callback function or evaluating the passed block for _macro variants.

You can achieve persistence by calling just one function. Facts.open takes a database and a pathname as arguments and replays the dump and the log in the file and opens the file for appending for logging all transactions.

Transactions make their content atomic. A transaction either end in success or error. In the latter case, the already applied parts of the transaction are rolled back in reverse order. The database is locked read/write during a transaction. Transactions can be nested and are multithread ready with pthread support.

An inner commit remains part of its outer transaction and is undone if the outer transaction rolls back. Log records, log hooks, broadcasts, and replication removal records are published only at the outermost commit. Each mutation has its own ID, including mutations inside nested transactions. Rollback restores removed facts with their original IDs; it does not reuse consumed IDs.

Facts.with_transaction rolls back on evaluation failure or a nonlocal exit from its body. Returning false normally is a successful evaluation and commits. Transactions cover database facts, not arbitrary file I/O or other side effects performed by the body.

Transactions require a local database or the authoritative replication server. Starting one on a replica that forwards writes to a remote master fails before any transaction mutations are made. The replication protocol does not provide distributed transactions. Commit publication remains a sequence of records, not a crash-atomic on-disk or remote transaction; publication failure does not undo an already committed in-memory state.

A persistence error makes the current log unusable for further writes. Add/remove operations report failure, new transactions are rejected, and closing the log does not retry buffered KC3 records. A nontransactional add failure removes its tentative in-memory addition; a remove failure leaves the fact in place. A commit failure stops publication at the first failed record and reports failure, but retains the in-memory commit. Hooks and broadcasts for that record and subsequent records are not issued.

An I/O failure can occur after some or all bytes reached a file, including one of the primary/after-dump logs but not the other. Treat the failed operation's on-disk outcome as uncertain. Preserve and check the logs, resolve any incomplete tail, and reopen into a fresh database before resuming persistent writes. Do not retry the failed marshalling state or clear an error flag to resume appending. This is error reporting and fail-closed recovery, not a guarantee of fsync durability or crash-atomic multi-record commits.

Replication reconnects use an ordered event cursor. A retained deletion has its own ID, separate from the deleted fact's insertion ID. Reconnect replay merges deletions and surviving additions in ID order. Replicas advance their cursor only after successfully applying each event, including a deletion of a fact already absent. Interrupted replay resumes from that cursor; a failed stream is closed before later events can overtake it.

Set Facts.set_server_count to a nonzero value before replicated writes to retain deletions, even when disk logging is disabled. Sending a deletion is not a peer acknowledgement: deletion records are now retained until the database is cleaned, rather than discarded after a number of sends. Their memory use grows with retained deletions; bounded retention with durable per-peer acknowledgements is not implemented.

This fixes reconnects within one authoritative database's lifetime, with replicas that do not write independently while disconnected. It does not implement leader failover, conflict resolution, or persistence of reconnect history across restart/compaction. Reinitialize replicas from an authoritative snapshot when that history is lost. Protocol version 2 is incompatible with older peers: upgrade all peers together and initialize fresh replication state from an authoritative snapshot, assigning distinct insertion IDs to legacy facts that shared a transaction ID. Existing persisted facts are not automatically migrated to a new replication epoch.

1.25.1 Examples

Create a database and add and remove triples and do a full query (?, ?, ?) :

ikc3> db = Facts.database()
(Ptr) 0x12345678
ikc3> Facts.add_tags(db, "Blade Runner", :is_a, :movie)
true
ikc3> Facts.add_tags(db, "Snow White", :is_a, :movie)
true
ikc3> Facts.remove_tags(db, "Blade Runner", :is_a, :movie)
true
ikc3> Facts.with_tags(db, ?, ?, ?, fn (fact) { puts(inspect(fact)) })
{"Snow White", :is_a, :movie}

Query operators from the global environment :

ikc3> Facts.with(Facts.env_db(), [[op = ?, :is_a, :op],
ikc3>                             [op, :op_sym, sym = ?]], fn (fact) {
ikc3>   puts("#{inspect(sym)} #{inspect(op)}")
ikc3> })

The config module from an HTTPd instance has an example of persistence, this way the database is loaded when the module is loaded :

defmodule Config do
  def db = Facts.database()
  Facts.open(db, "db/app.facts")
end

1.25.2 Network Replication

Facts databases support network replication. When connections are established between databases, all add and remove operations are automatically broadcast to connected peers. This works independently of on-disk persistence (the log).

Connecting to a remote database

Use Facts.connect to connect to a remote facts server:

ikc3> db = Facts.database()
ikc3> Facts.connect(db, "192.168.1.100", "9000")
true

The connection runs a background thread that receives facts from the remote peer and applies them to the local database.

Accepting connections (server mode)

To accept incoming connections, first create a TCP server socket using the Socket module, then use Facts.accept:

ikc3> require Socket
ikc3> db = Facts.database()
ikc3> server = Socket.listen("0.0.0.0", "9000")
ikc3> Facts.accept(db, server)
true

Each accepted connection spawns a background thread for receiving facts.

Closing connections

To close all connections on a database:

ikc3> Facts.close_all(db)

This gracefully shuts down all connection threads and closes the sockets.


Top : KC3 documentation

Previous : 1.24 Variable

Next : 1.26 Named block