A Postgres target for the engine's record store protocol
(vaelii.impl.protocols/RecordStore) — the durable ground truth a KB is recovered
from: canonical sentexes and justifications, keyed by integer handle, in a database
an operator already runs.
It buys COPY (copy-sentexes! below, the fastest ingest path any records backend
has), an operator's existing backup, PITR, replication, monitoring and access
control, a store larger than one machine's disk, and a real REPEATABLE READ
snapshot while something else is writing hard.
It does not buy a shared KB. Belief lives in the writing process's RAM — the
JTMS and the taxonomy closures — so a second process connected to this same database
does not see the first's beliefs, and its retraction sweep deletes records the first
still believes. That is the engine's single-writer contract, and a server does not
weaken it by one clause (docs/storage.md, The single-writer contract).
Three tables, and the handle is the key throughout:
vaelii_record (id, kind, frame, premise, strength) — one row per record, kind
splitting sentexes (0) from justifications (1), frame the whole record
nippy-frozen so a fetch thaws back type-identical (a LiteralSentex stays a
LiteralSentex). A fetch passes the thawed record through the engine's codec
(codec/decode-sentex, codec/decode-justification), which drops the fields the
running engine's record types do not have. id is bigint: handles are ints
in the engine today, and a column type is the one place that decision becomes a
migration on a table with 100M rows in it.strength column as the authoritative
value and is assoced back onto the thawed record on read — so mark-premise is a
one-row column update rather than a frame rewrite, and the column and the frame
cannot drift because the column always wins.vaelii_record_provenance (id, prov) and vaelii_record_meta (k, v), the latter
holding high_water — the largest handle ever allocated, so a delete of the max
handle followed by a reopen still never reissues it.vaelii_record_stamp (store_id, writes), one row: a uuid minted when the row is
first made, and a count a statement-level trigger on vaelii_record moves in the same
transaction as every insert, update, delete and truncate — COPY included. It is the
stamp an index or reasoning image is validated against (p/ImageStamping), so a KB can
keep both images beside records on a server (:pg-snapshot): one row read at open
says whether the records moved since the images were written, and the uuid says whether
they are the same records at all. A rolled-back statement does not move it.A put is one statement, not a transaction: a data-modifying CTE writes the record
and moves high_water together, so a write costs one round trip rather than a
BEGIN/two statements/COMMIT pair of four. A delete pairs the record and its
provenance the same way.
A point read is a network round trip where the disk store's is a page touch, and everything here is a response to that:
premise-ids walk itself — the walk
already selects the column, and recover asks for every one of those strengths
immediately afterwards, so the pair costs one scan instead of one scan plus a round
trip per premise;reduce-rows): autocommit
off and a set fetch size, which is what makes the driver hand back a portal instead
of buffering the whole result.Point the store at a db-spec ({:dbtype "postgresql" :host … :dbname …}, or
{:jdbcUrl "jdbc:postgresql://…"}) and it builds and owns a HikariCP pool — one
writer and N readers, by the contract above. Hand it a javax.sql.DataSource
instead and it borrows from that and closes nothing. :schema puts the three tables
in a schema of their own, so one database can hold several KBs and an operator can
drop one with DROP SCHEMA.
fsync is a no-op with a reason: durability here is the server's fsync, settled by
its synchronous_commit and its WAL, and there is no client-side buffer for this
store to force. It registers its close with the engine's durability daemon all
the same, so a JVM that exits without a close! still releases the pool.
Apache-2.0, and an adapter: it implements the SSPL engine's
vaelii.impl.protocols/RecordStore and is never depended on by it. Core wires it in
by a lazy requiring-resolve, so the engine never loads a JDBC driver unless a KB
selects :pg-memory or :pg-disk-log.
A Postgres target for the engine's **record store** protocol
(`vaelii.impl.protocols/RecordStore`) — the durable ground truth a KB is recovered
from: canonical sentexes and justifications, keyed by integer handle, in a database
an operator already runs.
## What a server buys, and what it does not
It buys `COPY` (`copy-sentexes!` below, the fastest ingest path any records backend
has), an operator's existing backup, PITR, replication, monitoring and access
control, a store larger than one machine's disk, and a real `REPEATABLE READ`
snapshot while something else is writing hard.
It does **not** buy a shared KB. Belief lives in the writing process's RAM — the
JTMS and the taxonomy closures — so a second process connected to this same database
does not see the first's beliefs, and its retraction sweep deletes records the first
still believes. That is the engine's single-writer contract, and a server does not
weaken it by one clause (`docs/storage.md`, *The single-writer contract*).
## The shape
Three tables, and the handle is the key throughout:
* `vaelii_record (id, kind, frame, premise, strength)` — one row per record, `kind`
splitting sentexes (0) from justifications (1), `frame` the **whole record**
nippy-frozen so a fetch thaws back type-identical (a `LiteralSentex` stays a
`LiteralSentex`). A fetch passes the thawed record through the engine's codec
(`codec/decode-sentex`, `codec/decode-justification`), which drops the fields the
running engine's record types do not have. `id` is **`bigint`**: handles are ints
in the engine today, and a column type is the one place that decision becomes a
migration on a table with 100M rows in it.
* a sentex's **assumption strength** rides the `strength` column as the authoritative
value and is `assoc`ed back onto the thawed record on read — so `mark-premise` is a
one-row column update rather than a frame rewrite, and the column and the frame
cannot drift because the column always wins.
* `vaelii_record_provenance (id, prov)` and `vaelii_record_meta (k, v)`, the latter
holding `high_water` — the largest handle ever allocated, so a delete of the max
handle followed by a reopen still never reissues it.
* `vaelii_record_stamp (store_id, writes)`, one row: a uuid minted when the row is
first made, and a count a statement-level trigger on `vaelii_record` moves in the same
transaction as every insert, update, delete and truncate — `COPY` included. It is the
stamp an index or reasoning image is validated against (`p/ImageStamping`), so a KB can
keep both images beside records on a server (`:pg-snapshot`): one row read at open
says whether the records moved since the images were written, and the uuid says whether
they are the same records at all. A rolled-back statement does not move it.
A put is **one statement**, not a transaction: a data-modifying CTE writes the record
and moves `high_water` together, so a write costs one round trip rather than a
`BEGIN`/two statements/`COMMIT` pair of four. A delete pairs the record and its
provenance the same way.
## The round trip, and what is done about it
A point read is a network round trip where the disk store's is a page touch, and
everything here is a response to that:
* a **fetch LRU per kind** in front of every read, so a hot handle never reaches the
server;
* a **premise-strength cache** filled by the `premise-ids` walk itself — the walk
already selects the column, and `recover` asks for every one of those strengths
immediately afterwards, so the pair costs one scan instead of one scan plus a round
trip per premise;
* **enumeration streams** through a server-side cursor (`reduce-rows`): autocommit
off and a set fetch size, which is what makes the driver hand back a portal instead
of buffering the whole result.
## Connections
Point the store at a db-spec (`{:dbtype "postgresql" :host … :dbname …}`, or
`{:jdbcUrl "jdbc:postgresql://…"}`) and it builds and owns a HikariCP pool — one
writer and N readers, by the contract above. Hand it a `javax.sql.DataSource`
instead and it borrows from that and closes nothing. `:schema` puts the three tables
in a schema of their own, so one database can hold several KBs and an operator can
drop one with `DROP SCHEMA`.
`fsync` is a no-op with a reason: durability here is the server's `fsync`, settled by
its `synchronous_commit` and its WAL, and there is no client-side buffer for this
store to force. It registers its **close** with the engine's durability daemon all
the same, so a JVM that exits without a `close!` still releases the pool.
## Boundary
Apache-2.0, and an **adapter**: it implements the SSPL engine's
`vaelii.impl.protocols/RecordStore` and is never depended on by it. Core wires it in
by a lazy `requiring-resolve`, so the engine never loads a JDBC driver unless a KB
selects `:pg-memory` or `:pg-disk-log`.(copy-justifications! store justifications)(copy-justifications! store
justifications
{:keys [batch] :or {batch default-copy-batch}})Bulk-load justifications — each carrying its :id — the same way
copy-sentexes! loads sentexes. A justification has no strength column.
Bulk-load `justifications` — each carrying its `:id` — the same way `copy-sentexes!` loads sentexes. A justification has no strength column.
(copy-sentexes! store sentexes)(copy-sentexes! store
sentexes
{:keys [batch premises?]
:or {batch default-copy-batch premises? true}})Bulk-load sentexes — each carrying its :id — through COPY … FROM STDIN BINARY, returning how many rows landed. This is the ingest path a server has and
the others do not, and it is a load rather than an upsert: a handle the store already
holds raises rather than being overwritten.
A record's :strength rosters it as a premise here, where put-sentex leaves the
mark to mark-premise. The two entry points are asked different questions: the engine
stores a fact and marks it at the choke point one line later, while a bulk load has no
such line — the strength a dump's record carries is what a later recover reads as
its assumption, and re-asking for it a handle at a time is a round trip per record.
{:premises? false} loads the strengths without the marks, for records that are
derivations rather than assumptions. :batch rows per buffer (default 10000).
This is the protocol's own public entry point, for an application holding a corpus already. The
engine reaches the same path without knowing this namespace exists, through
protocols/BulkLoading — which is what import! writes its records through.
Bulk-load `sentexes` — each carrying its `:id` — through `COPY … FROM STDIN
BINARY`, returning how many rows landed. This is the ingest path a server has and
the others do not, and it is a load rather than an upsert: a handle the store already
holds raises rather than being overwritten.
**A record's `:strength` rosters it as a premise here**, where `put-sentex` leaves the
mark to `mark-premise`. The two entry points are asked different questions: the engine
stores a fact and marks it at the choke point one line later, while a bulk load has no
such line — the strength a dump's record carries *is* what a later `recover` reads as
its assumption, and re-asking for it a handle at a time is a round trip per record.
`{:premises? false}` loads the strengths without the marks, for records that are
derivations rather than assumptions. `:batch` rows per buffer (default 10000).
This is the protocol's own public entry point, for an application holding a corpus already. The
**engine** reaches the same path without knowing this namespace exists, through
`protocols/BulkLoading` — which is what `import!` writes its records through.(ensure-schema! ds sql)Create the record, provenance, meta and stamp tables (and the premise index and the trigger that moves the stamp), and the schema itself when one is named. Idempotent; runs before any op, so a store made before the stamp existed gains it at its next open, counting from zero — it has no image to be stamped against yet.
Create the record, provenance, meta and stamp tables (and the premise index and the trigger that moves the stamp), and the schema itself when one is named. Idempotent; runs before any op, so a store made before the stamp existed gains it at its next open, counting from zero — it has no image to be stamped against yet.
(pg-record-store spec)(pg-record-store spec opts)A durable RecordStore over Postgres.
spec is a next.jdbc db-spec ({:dbtype "postgresql" :host … :dbname … :user …}
or {:jdbcUrl "jdbc:postgresql://…"}), in which case this builds and owns a
HikariCP pool — or an existing javax.sql.DataSource, which it borrows and does not
close. Its own keys, which never reach the driver, are store-keys:
:schema — put the three tables in a schema of their own (created if absent), so
one database holds several KBs and DROP SCHEMA removes one whole;:cache-capacity — the per-kind fetch LRU (default 65536);:premise-cache-capacity — how many premise strengths to hold in RAM (default
4,000,000); past it the walk stops filling and premise-strength pays a round trip;:fetch-size — rows per cursor fetch on the enumerations (default 5000);:pool-size — maximum pooled connections (default 8);:prefetch — act on a caller's prefetch hint (default true). Whether a hint ever
arrives is the caller's setting (resolution/*prefetch-candidates*, off by
default); this says whether this store does anything when one does, and false here
is the hard off for benchmarking one against the other;:prefetch-min — how many handles in a hint must be uncached before a batched
read is issued instead of leaving them to the point reads (default 4).Creates the schema if absent and loads the handle counter, so a reopen mints above
every handle the store has ever held. The store is java.io.Closeable; close
releases the pool it built and drops its durability registration.
A durable `RecordStore` over Postgres.
`spec` is a next.jdbc db-spec (`{:dbtype "postgresql" :host … :dbname … :user …}`
or `{:jdbcUrl "jdbc:postgresql://…"}`), in which case this builds and owns a
HikariCP pool — or an existing `javax.sql.DataSource`, which it borrows and does not
close. Its own keys, which never reach the driver, are `store-keys`:
* `:schema` — put the three tables in a schema of their own (created if absent), so
one database holds several KBs and `DROP SCHEMA` removes one whole;
* `:cache-capacity` — the per-kind fetch LRU (default 65536);
* `:premise-cache-capacity` — how many premise strengths to hold in RAM (default
4,000,000); past it the walk stops filling and `premise-strength` pays a round trip;
* `:fetch-size` — rows per cursor fetch on the enumerations (default 5000);
* `:pool-size` — maximum pooled connections (default 8);
* `:prefetch` — act on a caller's prefetch hint (default true). Whether a hint ever
arrives is the *caller's* setting (`resolution/*prefetch-candidates*`, off by
default); this says whether this store does anything when one does, and `false` here
is the hard off for benchmarking one against the other;
* `:prefetch-min` — how many handles in a hint must be **uncached** before a batched
read is issued instead of leaving them to the point reads (default 4).
Creates the schema if absent and loads the handle counter, so a reopen mints above
every handle the store has ever held. The store is `java.io.Closeable`; `close`
releases the pool it built and drops its durability registration.(reduce-rows store sql params rf init)Reduce rf over the rows sql selects, streaming them through a server-side
cursor rather than buffering the result: the walk borrows a connection, turns
autocommit off and sets a fetch size, which together are what make the driver hand
back a portal. Without both, SELECT id FROM vaelii_record on a large store reads
every row into the driver before the first one is visible.
rf is an ordinary reducing fn (rf acc ^ResultSet rs), called with the cursor
positioned on each row; wrapping the accumulator in reduced stops the walk and
closes the cursor without draining it. The transaction is rolled back on the way
out — every walk here is a read — and the connection goes back to the pool.
Reduce `rf` over the rows `sql` selects, streaming them through a **server-side cursor** rather than buffering the result: the walk borrows a connection, turns autocommit off and sets a fetch size, which together are what make the driver hand back a portal. Without both, `SELECT id FROM vaelii_record` on a large store reads every row into the driver before the first one is visible. `rf` is an ordinary reducing fn `(rf acc ^ResultSet rs)`, called with the cursor positioned on each row; wrapping the accumulator in `reduced` stops the walk and closes the cursor without draining it. The transaction is rolled back on the way out — every walk here is a read — and the connection goes back to the pool.
The keys pg-record-store reads itself. Everything else in the spec is the
database's — :dbtype, :host, :dbname, :jdbcUrl, credentials — and is handed
to the driver untouched, so this is the set that must be stripped before it is.
The keys `pg-record-store` reads itself. Everything else in the spec is the database's — `:dbtype`, `:host`, `:dbname`, `:jdbcUrl`, credentials — and is handed to the driver untouched, so this is the set that must be stripped before it is.
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |