pub struct Database { /* private fields */ }Expand description
A directory of tables: the JSONL logs, their index snapshots, the metadata catalog
(hayson.json) describing them, and one writer lock (lock::DirLock, ADR-hjxp/0006 — one
writer per database directory).
Implementations§
Source§impl Database
impl Database
pub fn open(dir: &Path) -> Result<Database, DatabaseError>
Sourcepub fn open_with(
dir: &Path,
options: OpenOptions,
) -> Result<Database, DatabaseError>
pub fn open_with( dir: &Path, options: OpenOptions, ) -> Result<Database, DatabaseError>
Opens (and creates when missing) a Hayson database directory:
- locks the directory for writing, unless
options.readonlyoroptions.lockisLockMode::Skip(ADR-hjxp/0006) — at the path the arrangement on disk puts the lock, settled under the lock itself (seeDatabase::hold_for_open); - reads
hayson.json, or starts an empty catalog when the directory is new; - for each declared table, opens its log and restores its registry from
<table>.idxviacheckpoint::restore_or_rebuildrather than always scanning the whole log; - finishes an unfinished
.hayson.txnjournal, if one is on disk, before returning — so no write reaches a table until whatever a prior crash left half-committed is done.
A refusal at any step after the lock was taken releases it again before returning, so a directory this call rejected is left no more locked than it was found.
Sourcepub fn resolve_layout(
dir: &Path,
options: &LayoutOptions,
) -> Result<Layout, DatabaseError>
pub fn resolve_layout( dir: &Path, options: &LayoutOptions, ) -> Result<Layout, DatabaseError>
Works out where this database’s files are before anything is opened or locked: the metadata
document name the options ask for (refusing metadataFile and manifest naming two
different files), then the arrangement — whatever the document on disk records, or the
requested one when there is no document yet.
Runs before create_dir_all, so a refusal here leaves the directory exactly as it found it.
Public for that same reason: a caller that only needs to know where the writer lock or the
metadata document of a directory sits gets the paths without opening the database, taking
the lock, or creating anything.
@param dir the database directory
@param options the layout/metadataFile/manifest triple of OpenOptions, unresolved
@returns where this database’s files are, or a refusal to name the metadata document twice
pub fn dir(&self) -> &Path
Sourcepub fn layout(&self) -> &Layout
pub fn layout(&self) -> &Layout
Where this handle’s files are on disk — the arrangement and the metadata document name it
resolved at open, the read side of the TS Database.layout property.
Sourcepub fn follow(&self) -> Option<FollowPolicy>
pub fn follow(&self) -> Option<FollowPolicy>
The follow policy every table of this database was opened with — None when reads never
catch up unprompted, which is what a writable open resolves to.
pub fn table_names(&self) -> Vec<&str>
pub fn table(&self, name: &str) -> Option<&Table>
Sourcepub fn table_mut(&mut self, name: &str) -> Option<&mut Table>
pub fn table_mut(&mut self, name: &str) -> Option<&mut Table>
Handing out a writable table is the write path this type cannot see the end of, so the
lease is renewed here as well — a host that never calls DirLock::tick still keeps its
lock as long as it keeps writing. A lock already lost cannot be reported through an
Option; the next call that returns a Result — commit and every schema change — raises
DatabaseError::Lock instead.
Sourcepub fn put_checked(
&mut self,
table: &str,
document: &Value,
force: bool,
) -> Result<HaysonRecord, DatabaseError>
pub fn put_checked( &mut self, table: &str, document: &Value, force: bool, ) -> Result<HaysonRecord, DatabaseError>
Table::put, refusing first if document breaks an enforce: true relation declared on
table. table_mut(name).put(...) skips this — every caller that wants enforcement honoured
has to come in through here, because Table itself cannot see another table’s registry to
check against (only Database holds every table at once).
Sourcepub fn patch_checked(
&mut self,
table: &str,
key: &str,
changes: &Value,
force: bool,
) -> Result<HaysonRecord, DatabaseError>
pub fn patch_checked( &mut self, table: &str, key: &str, changes: &Value, force: bool, ) -> Result<HaysonRecord, DatabaseError>
Table::patch, refusing first if changes breaks an enforce: true relation declared on
table. See Database::put_checked for why this exists alongside table_mut(...).patch.
Sourcepub fn stage_put_checked(
&mut self,
table: &str,
document: &Value,
force: bool,
) -> Result<HaysonRecord, DatabaseError>
pub fn stage_put_checked( &mut self, table: &str, document: &Value, force: bool, ) -> Result<HaysonRecord, DatabaseError>
Table::stage_put, refusing first if document breaks an enforce: true relation declared
on table. Same rationale as Database::put_checked; the napi/wasm bindings call this
instead of table_mut(name).stage_put(...) because their staged writes still run synchronously
with full Database access at the call site, same as a direct put.
Sourcepub fn stage_patch_checked(
&mut self,
table: &str,
key: &str,
changes: &Value,
force: bool,
) -> Result<HaysonRecord, DatabaseError>
pub fn stage_patch_checked( &mut self, table: &str, key: &str, changes: &Value, force: bool, ) -> Result<HaysonRecord, DatabaseError>
Table::stage_patch, refusing first if changes breaks an enforce: true relation declared
on table. See Database::stage_put_checked.
Sourcepub fn delete_checked(
&mut self,
table: &str,
key: &str,
force: bool,
) -> Result<bool, DatabaseError>
pub fn delete_checked( &mut self, table: &str, key: &str, force: bool, ) -> Result<bool, DatabaseError>
Deletes key from table, honouring every on_delete relation naming table as the to
side — restrict refuses if a referencing record still exists, cascade deletes every
referencing record (recursively, watching for a cycle in the relation graph), set-null
clears the referencing field instead of deleting the row, and no-action/undeclared leaves
referencing records untouched exactly as Table::delete always has (checked, not fixed —
Database::check_integrity still finds what this deliberately leaves behind).
A table nothing points at (no relation names it as to) skips the plan and goes straight
through Table::delete — the common case pays nothing extra, and force there behaves as
it always has. Every effect a cascade needs lands in one UnitOfWork
(Database::commit), so a restrict found three tables deep aborts the whole delete with
nothing written, not a half-cascaded one. force has no equivalent inside that unit — a
fresh unit has no pending per-key conflict to force past the way a plain Table::delete
does — so it is honoured only on the no-relation fast path.
Sourcepub fn tick(&self) -> Result<(), DatabaseError>
pub fn tick(&self) -> Result<(), DatabaseError>
Renews the writer lease from outside — DirLock::tick reached through the lock this
handle owns privately. A host without background threads (wasm32-wasip1) has no heartbeat
of its own, so this is what a timer on its side drives; call it at least twice per
LockOptions::heartbeat_ms. Does nothing on a readonly handle, which holds no lock.
Sourcepub fn restore_outcome(&self, name: &str) -> Option<RestoreOutcome>
pub fn restore_outcome(&self, name: &str) -> Option<RestoreOutcome>
How the table named name was brought up at open — Restored from <table>.idx or
Rebuilt by a full scan of its log. None when the database holds no such table.
Sourcepub fn catalog_unchecked(&self) -> &Catalog
pub fn catalog_unchecked(&self) -> &Catalog
The metadata catalog as this handle last read it, without the drift gate — every table it
describes may be one this handle does not serve. catalog is what a caller outside the
engine should ask for.
Sourcepub fn catalog(&self) -> Result<&Catalog, DatabaseError>
pub fn catalog(&self) -> Result<&Catalog, DatabaseError>
The metadata catalog as this handle last read it — the parsed form of hayson.json, which
catalog::catalog_document turns back into the document a caller can serialize.
Refuses when any table open here has drifted from what the catalog now declares, because handing the document over would describe tables this handle does not serve. Refusing is all that happens: the handle is never reloaded behind the caller’s back, so the schema and the index set every open table was opened with stay exactly as they were.
Sourcepub fn describe_table(&self, name: &str) -> Result<Value, DatabaseError>
pub fn describe_table(&self, name: &str) -> Result<Value, DatabaseError>
The schema of one table as a standalone document, the shape create_table accepts — what
the TS describeTable answers with.
Read off the live handle (table::describe_table_document), not off the catalog entry, so
it describes the schema and the index set the table is actually being served with — every
table the catalog declares is open here, both after open and after
Database::refresh, so there is no catalog-only table to fall back to.
The drift gate here is the one table named, unlike Database::catalog, whose document
covers every table and so has to answer for every table: a table of its own that moved on
says nothing about this one.
Sourcepub fn relations(&self, table: Option<&str>) -> Vec<Relation>
pub fn relations(&self, table: Option<&str>) -> Vec<Relation>
The relations the catalog declares.
@param table When given, keeps only the relations touching this table, on either end.
Sourcepub fn check_integrity(&self) -> Result<IntegrityReport>
pub fn check_integrity(&self) -> Result<IntegrityReport>
Walks every declared relation and reports the records whose reference has no counterpart — a diagnosis, not a gate; nothing is repaired, removed or written.
Sourcepub fn refresh(&mut self) -> Result<bool, DatabaseError>
pub fn refresh(&mut self) -> Result<bool, DatabaseError>
Catches up with everything other processes did to this directory since this handle last
looked — the port of the TS Database.refresh(), in the same two steps and the same order:
hayson.jsonis read again, and every table it now declares that this handle does not hold is opened, through the very code pathopenuses per table;- every table already open here is caught up through
Table::refresh.
Answers whether anything at all changed. A catalog that lost a table on disk does not close the one already open here: a live table is not dropped out from under a caller holding it.
Sourcepub fn unit_of_work(&mut self) -> UnitOfWork
pub fn unit_of_work(&mut self) -> UnitOfWork
Starts a unit of work: stage put/patch/delete across several tables, then pass it to
Database::commit, which lands it whole or not at all.
Sourcepub fn commit(
&mut self,
unit: &mut UnitOfWork,
) -> Result<UnitOfWorkResult, DatabaseError>
pub fn commit( &mut self, unit: &mut UnitOfWork, ) -> Result<UnitOfWorkResult, DatabaseError>
Runs every operation staged in unit, across the tables it names, through the six-step
commit txn::UnitOfWork::commit documents (resolve, barrier, prepare, journal, write,
cleanup) — refused when the database is readonly.
The unit is borrowed, not consumed: a commit refused before anything was journalled leaves it staged and retriable, exactly as the TS engine leaves it.
Sourcepub fn prepare_commit(
&mut self,
unit: &mut UnitOfWork,
) -> Result<PreparedUnit, DatabaseError>
pub fn prepare_commit( &mut self, unit: &mut UnitOfWork, ) -> Result<PreparedUnit, DatabaseError>
The first phase of Database::commit — the readonly refusal, the lock renewal and
UnitOfWork::prepare. Nothing is appended and no file is written, so a refusal from here
leaves unit staged and retriable exactly as a refused commit does.
Split out for a caller that keeps the database behind a lock and wants to release it across
the journal write, which touches no table: prepare here, UnitOfWork::journal with the
database let go, then Database::apply_commit once it is held again. The caller must stay
the only writer across all three, or the burned _seq numbers would land out of order.
Sourcepub fn apply_commit(
&mut self,
prepared: PreparedUnit,
) -> Result<UnitOfWorkResult, DatabaseError>
pub fn apply_commit( &mut self, prepared: PreparedUnit, ) -> Result<UnitOfWorkResult, DatabaseError>
The last phase of Database::commit: every prepared entry appended and the journal
removed. See Database::prepare_commit for the contract a caller running the phases apart
has to keep.
Sourcepub fn create_table(
&mut self,
name: &str,
schema_spec: Value,
) -> Result<(), DatabaseError>
pub fn create_table( &mut self, name: &str, schema_spec: Value, ) -> Result<(), DatabaseError>
Creates a table, or brings an existing one up to a newer (additive) schema — catalog::build
does not check revision compatibility yet (see its own doc comment), so a genuinely breaking
change is not refused here either; that check is still on the TODO list this method inherits.
Disk first: the catalog is written before self.catalog/self.tables change, so a failure
past that point is the only way this call can leave the two out of step with each other.
The handle comes from open_catalog_table — the same helper open()/refresh() use — so a
table migrated here carries exactly what it would carry after a restart, triggers included.
name is the key the table lands under in the catalog, and the schema’s own table is
carried into every log line and index snapshot written for it. The two disagreeing is
refused rather than resolved: the catalog key wins wherever they are read back, so accepting
the spec would silently drop the name its author wrote.
Sourcepub fn drop_field(
&mut self,
table: &str,
field: &str,
force: bool,
) -> Result<(), DatabaseError>
pub fn drop_field( &mut self, table: &str, field: &str, force: bool, ) -> Result<(), DatabaseError>
Takes a field off the schema of a table without touching its log — records written before the drop keep carrying the field on disk, reads just stop handing it back.
Refuses (leaving disk and self untouched) when the field is the primary key, is what the
table partitions by, or an index/check/relation of the catalog still depends on it
(catalog::build::drop_field, unconditional on force) — and, unless force is true,
when a live record still holds a value for it (checked here, against the open table, per the
TODO catalog::build::drop_field leaves for this layer).
The declaration check runs first on purpose: with both refusals due the caller is told about
the one force cannot get them past, and the error class matches what the field’s
dependents are — see packages/hayson/test/drop-pola-kolejnosc-odmowy.test.ts.
Sourcepub fn declare_index(
&mut self,
table: &str,
index_spec: Value,
) -> Result<(), DatabaseError>
pub fn declare_index( &mut self, table: &str, index_spec: Value, ) -> Result<(), DatabaseError>
Adds a named index to a table of the catalog — a unique index is refused whole if a live
record already shares a value on it (catalog::declare_index, checked against the open
table). The reopened table’s registry is built from the catalog’s full expanded index list
(catalog::expand_indexes), so a composite or where-scoped index is enforced/served right
away, same as the single-field unconditional case catalog::expand_indexes derives as sugar.
Reopened through open_catalog_table — the same helper open()/refresh() use — so the
handle keeps its snapshot pairing, and the stale .idx is rewritten before this returns.
Sourcepub fn declare_relation(
&mut self,
relation_spec: Value,
force: bool,
) -> Result<(), DatabaseError>
pub fn declare_relation( &mut self, relation_spec: Value, force: bool, ) -> Result<(), DatabaseError>
Adds a named relation to the catalog (catalog::declare_relation). With enforce: true,
refuses whole if a live record already breaks it — unless force is true, which declares
it anyway and lets enforcement start from here on, existing orphans left as they are.
Unlike declare_index, nothing here reopens a table: a relation is not read by a table’s own
registry the way an index is, so no live handle changes shape. from/to are read straight
out of relation_spec first, ahead of the real parse inside catalog::declare_relation, only
to know which two tables to flush and hand it — a malformed spec still gets the full,
authoritative error from the parser, this is not validation.
Sourcepub fn declare_trigger(
&mut self,
trigger_spec: Value,
) -> Result<(), DatabaseError>
pub fn declare_trigger( &mut self, trigger_spec: Value, ) -> Result<(), DatabaseError>
Declares an AFTER trigger (catalog::build::declare_trigger, which refuses a spec naming a
table the catalog does not declare) and refreshes every open table’s live trigger set from
the result — both the source table trigger_spec.table names and the effect table
trigger_spec.effect.table names, since either could be a live table this call did not
itself just open. Disk first, same discipline as create_table/declare_index.
Sourcepub fn flush(&mut self) -> Result<(), DatabaseError>
pub fn flush(&mut self) -> Result<(), DatabaseError>
Appends every write staged against any table of this database and not yet committed — the
TS Database.flush(). A no-op for a table with nothing in flight, which is every table of a
database whose writes all went through put/patch/delete.
Sourcepub fn close(self) -> Result<()>
pub fn close(self) -> Result<()>
Appends whatever every table still holds staged, snapshots the ones with writes since their
last snapshot (see Table::needs_snapshot), then releases the writer lock, if this handle
holds one — mirrors the TS Database.close(). The catalog needs no flush of its own: it is
already durable (fsync) by the time the call that wrote it returns.
The lock is released whether or not a table refused its flush or snapshot: the handle is consumed either way, and a lock file left behind names a live pid that no staleness rule reclaims, so keeping it would lock this very process out of its own directory. The first refusal is what the caller gets, after every other table had its turn.