A declared link between two tables. enforce: true is checked at put/patch
(Database::put_checked/patch_checked) and on_delete at delete
(Database::delete_checked) — both opt-in per relation; a relation naming neither is still
reported by crate::catalog::check_integrity and Database::relations, but nothing about a
write or delete checks it.
The write a trigger performs after its event commits: a document assembled by fields from
old/new, then put into table (which may be the trigger’s own source table or another
one) — always inside the same unit of work as the write that fired it.
A declarative AFTER trigger: fires once event commits on table, atomically appending
effect in the same unit of work as that write. v1 is AFTER-only and unconditional.
When a markdown field’s links get parsed (schema::extract_links) against the table’s
virtual-path index — on every write (the default), or only when the caller asks for it.
The event a trigger fires after, matched against what a live write actually did to the key —
Insert when it did not exist before the write, Update when it did, Delete when a live
delete removed an existing key.
Where one field of a trigger’s effect document gets its value from — the closed set of
transformations v1 supports. There is no scripting/plugin layer in this engine, so “effect”
cannot be arbitrary code the way it would be in the TS engine; it is this declared mapping,
evaluated by the engine itself against the firing operation’s old/new record.
Parses the catalog’s own serialized form back before it is allowed near the disk — a catalog
this build could write but not read back would leave the next open() unable to see its tables.
Builds the ordered document serialize_catalog renders to bytes — sections and known fields in
the fixed order of the format (docs/format.md, „hayson.json w formacie 2“), tables and
fields sorted alphabetically by key, indexes/checks/relations/views sorted by name,
and history left in the order it arrived.
db.checkIntegrity() — walks every declared relation and reports the records whose reference
has no counterpart. A diagnosis, not a gate: nothing here is repaired, removed or written.
tables supplies the open handles this call can read; a relation naming a table not in it
reads as empty, exactly like the TS reference’s this.#tables.has(name) ? ... : [].
db.createTable() — addytywna zmiana albo nowa tabela, patrz docs/schema.md „Ewolucja schemy“.
Migracja niekompatybilna (zmiana typu, zwężenie itd.) nie jest tu sprawdzana: schema już
przeszła przez schema::parse_schema, a diff rewizji zostaje na liście TODO niżej.
db.declareIndex() — adds a named index to tables.<table>.indexes[] and, when it is unique,
refuses the whole operation if any live record (read through table_handle) already shares a
value on it. Nothing is written when it refuses: the catalog handed back on success is the only
side effect, write_catalog is left to the caller (mirrors create_table/drop_field).
db.declareRelation() — adds a named relation to the catalog. Mirrors declare_index’s shape:
parses and validates against the catalog held in memory, refuses without writing anything, and
leaves write_catalog to the caller.
db.declareTrigger() — adds a declarative AFTER trigger to triggers[]. trigger_spec.table
and trigger_spec.effect.table must already be declared tables of catalog (checked by
parse_trigger_entry before anything else runs, ADR-s21j/0029): a spec naming an undeclared
table is refused here, before Database::declare_trigger ever reaches disk. Nothing is written
when it refuses — the catalog handed back on success is the only side effect, write_catalog is
left to the caller (mirrors declare_index).
db.dropField(table, field) na poziomie samego katalogu — docs/format.md
„dropField odmawia jednolicie na wszystkich czterech klasach zależności“ i ADR-qtr2 (cerebro,
„0030 — force nie wyburza deklaracji“): force potwierdza tylko rekordy nadal niosące pole
w logu, nigdy deklaracje katalogu — ten test jest więc zawsze egzekwowany, bez parametru
force, bo nic tutaj go nie omija.
Merges declared indexes[] with the sugar the field modifiers (index, fulltext) expand to,
deduplicated by index_identity — an explicit declaration wins over the name sugar generates.
Every <table>.schema.json a format-1 database at dir declares a table through, sorted.
migrate_format_1_to_2 reads exactly these, so the caller that persists the catalog it
returns can retire exactly these once the catalog is safely on disk.
Whether migrate_format1(dir) would find something to rewrite right now — the same guard
clauses it runs before touching disk, without the write. false on a directory already at
format 2, or on one with neither a catalog nor any <table>.schema.json (a fresh, empty
database directory): a caller deciding whether to call migrate_format1 “just in case” on
every open does not have to reinvent this check, which duplicates schema_files_of’s own
notion of what counts as format 1.
Rewrites the format-1 database at dir — a <table>.schema.json per table, next to the logs —
into the format-2 metadata catalog the engine reads, in place and offline. The catalog lands
first, atomically, and only then do the schema files go: interrupted in between, the directory
is a working format-2 database with residue, which is the state Database::open already reads.
Reads every *.schema.json of a format-1 database at dir and rewrites it into one format-2
Catalog, ready for write_catalog — the migration itself is left to the caller, which also
decides when createdAt/updatedAt are pinned. Refuses while .hayson.txn sits on disk
(assertJournalGone in packages/hayson/src/catalog/migrate.ts): an unfinished unit of work is
redone through the layout it was written in, so the format cannot change out from under it.
Also refuses — without writing anything — when a field declared unique already has two live
records sharing a value (uniqueViolationsOf in migrate.ts).
Validates a parsed hayson.json of a format-2 database and returns it typed, without opening
the database. Every message names the path it failed on, such as tables.users.indexes[1].fields.
Writes the catalog atomically (.tmp → fsync → rename, via write_atomic), inheriting
the permissions of the file it replaces so .tmp is never briefly world-readable
(see docs/format.md, “Zapis, kolejność kluczy, uprawnienia”). Refuses first, via
assert_catalog_readable, leaving the file on disk untouched.