db
24 modules
db/assert_row.ts
Assertion helper for INSERT RETURNING results.
db/cell_audit_queries.ts
Audit-log query for the per-cell timeline. Matches rows whose
metadatajsonb names the cell on any of the keys used by cell-domain event envelopes —cell_id(cell mutations + grants),source_id/new_id(clone),source_id/target_id(cell_field events),parent_id/child_id(cell_item events).Each
metadata @> '{...}'::jsonbclause hits the existing GIN onaudit_log.metadata; Postgres bitmap-ORs the index scans together.db/cell_ddl.ts
Cell PG schema.
The universal content primitive: a single
celltable whosedataJSONB is interpreted by view shape. Parent→child membership and named relationships live in two sibling tables —cell_item(ordered children, fractional-indexing keyed) andcell_field(named edges).refs text[]carriesblake3:fact hashes auto-extracted fromdataby application code on every write.Soft delete via
deleted_at. Most indexes are partial ondeleted_at IS NULLso active-cell queries skip tombstones.pathis the global namespace axis — a partial unique index enforces uniqueness across all active rows (PostgreSQL UNIQUE constraints don't support WHERE clauses, so it's expressed as a partial unique index). It additionally filters ondeleted_at IS NULLso a soft-deleted cell doesn't block reuse of its path. Path writes are admin-only at the action layer; user-namespaced paths are a future extension.Ownership columns (
created_by,updated_by) are nullable FKs toactor: NULL = system origin (well-known cells, daemon/agent cells). The non-admin authz path treats NULLcreated_byas admin-only via an explicit equality check (auth/cell_authorize.ts).Timestamp naming (
created_at,updated_at) aligns with fuz_app's_at-everywhere convention used byaccount,actor,audit_log,role_grant, etc.Single-migration shape:
full_cell_schemacreates the canonical cell + cell_grant + cell_field + cell_item layout in one shot from the live exported constants. The dormantcell_historytable lives in the separatefuz_cell_historynamespace (cell_history_ddl.ts).db/cell_field_queries.ts
Raw queries against the
cell_fieldtable.Named-relation primitive: each row is
(source_id, name) → target_id, a JSON-object-shaped edge from one cell to another.(source_id, name)is unique — one target per name per source. Multiplicity by composition (target a collection cell whoseitems[]are the multi-valued tags), not by allowing duplicate field rows.Reads filter both endpoints by
cell.deleted_at IS NULLso relations dangling off a soft-deleted cell don't surface to the live graph.query_cell_field_set upserts on the
(source_id, name)PK so re-pointing a name updatestarget_idin place — JSON-object semantics (obj.foo = baroverwrites whatever was there).db/cell_grant_queries.ts
Raw queries against the
cell_granttable.Resource-side ACL for cells: each row admits a principal at a
level(viewer|editor). Principal is discriminated by which columns are set —actor_id(single actor) xor(role, scope_id?)(any holder of a matching role_grant). Owner is implicit oncell.created_byand never appears in this table.Convention:
deps: QueryDepsfirst, no audit side effects, mutations return the affected row (ornullfor not-found).query_cell_grant_create upserts on the relevant partial unique index so re-granting the same principal updates
levelrather than producing duplicate rows. The two principal shapes use different indexes:- Actor-shaped:
idx_cell_grant_unique_actoron(cell_id, actor_id). - Role-shaped:
idx_cell_grant_unique_role_scopeon(cell_id, role, scope_id)withNULLS NOT DISTINCTso two(role, NULL)grants on the same cell collide.
- Actor-shaped:
db/cell_history_ddl.ts
Cell history PG schema (schema only).
Lightweight references to cell state snapshots. Heavy data (serialized cell bytes) lives in the fact store;
fact_hashpoints there. The snapshot lifecycle (when to serialize, hash, store, and record) is deferred to a future iteration — this only stages the table so downstream code can target a stable schema. The table ships present-but-unwritten.fact_hashis intentionally not a foreign key tofact(hash)— snapshots may be evicted by GC policy while history rows remain as audit traces, and federation may target facts on another instance.Depends on CELL_MIGRATION_NS (FK on
cell.id).db/cell_item_queries.ts
Raw queries against the
cell_itemtable.Ordered-child membership: each row is
(parent_id, position) → child_id.positionis opaque text (fractional-indexing key) — lex ordering is the contract. The PK on(parent_id, position)enforces one cell per slot but allows the samechild_idto appear at multiple positions (the primitive is JSON-array-shaped — ordered multiset, not set; domain dedup rules ride on top).Reads filter both endpoints by
cell.deleted_at IS NULLso items dangling off a soft-deleted cell don't surface.query_cell_item_insert returns the inserted row OR throws the underlying
23505(Postgres unique violation) on a `(parent_id, position)` collision. Handlers convert this into thecell_item_position_takenJSON-RPC error so the client retries with a refreshed bracket.db/cell_queries.ts
Raw queries against the
celltable.Convention:
deps: QueryDepsfirst, no audit side effects, mutations return the affected row (ornullfor not-found).cell.refsis auto-extracted fromdataon every create and update viafact_hash_extract_refs(depth-first walk forblake3:-prefixed strings). Callers never passrefsdirectly — the column is a derived projection ofdatafor cells-by-fact discovery, mirroring what a fact store does for JSON facts.Soft delete via
deleted_at. Allget/listqueries exclude tombstones by default;include_deleted: trueopts in for admin / audit views.pathuniqueness is global, enforced byidx_cell_path_unique(partial on active rows). Path reuse after soft delete falls out of the partial index — queries do not need special handling.db/create_db.ts
Database initialization with driver auto-detection.
Selects the appropriate database driver based on
database_url:postgres://orpostgresql://— usespg(PostgreSQL)file://— uses@electric-sql/pglite(file-based)memory://— uses@electric-sql/pglite(in-memory)
Both
pgand@electric-sql/pgliteare optional peer dependencies, dynamically imported only when needed. For direct driver construction without auto-detection, use db/db_pg.ts or db/db_pglite.ts.db/db_pg.ts
PostgreSQL driver adapter for Db.
Provides
create_pg_db()to construct a Db backed by apg.Pool. Statically imports onlypgtypes; thepgruntime is dynamically imported where needed — by callers (e.g., create_db) that construct thePool, and by register_pg_type_parsers to reachpg.types. pglite-only consumers never reach either path, sopgstays an optional peer dep.db/db_pglite.ts
db/db.ts
Database wrapper with duck-typed interface.
Accepts any client with a query(text, values) method. Both
pg.Pooland@electric-sql/pglitesatisfy this interface.Transaction safety is provided by an injected
transactioncallback — the driver adapters (db/db_pg.ts, db/db_pglite.ts) supply the driver-appropriate implementation. Close is handled externally (returned alongside the Db as DbDriverResult), not as a method on this class.db/fact_ddl.ts
Fact + memo PG schema.
Three tables:
fact— content-addressed bytes.hash = 'blake3:<hex64>'. Either embedded (bytes) or referenced (external_url); the CHECK constraint enforces exactly one populated. Idempotent: same bytes always produce the same hash, soINSERT … ON CONFLICT DO NOTHINGis the put primitive.fact_ref— declared dependency edges (source fact → target fact).target_hashis intentionally not a foreign key: in federation a reference may target a fact stored on another instance.memo—(fn_id, input_hash) → output_hashfor memoized computations.
db/fact_disk_storage.ts
Filesystem CAS for externally-stored fact bytes — the disk half of PgFactStore, threaded over the injectable
runtime/*Depsrather than rawnode:fs, so it runs unchanged under Node, Deno, and a mock runtime.Large facts (over the embedded threshold) live on disk at the canonical sharded layout
<facts_dir>/<shard>/<rest>—<shard>is the first 2 hex chars of the blake3 digest,<rest>the remaining 62 — with thefactrow carryingexternal_url = file:<shard>/<rest>(disk-root-relative). The layout is single-sourced by fact_disk_path in db/file_fact_url.ts, so the write path here and the URL minted into the row can't drift. The TS twin of the Rustfuz_factdisk CAS.Writes land through
<facts_dir>/.tmp/<rand>.tmp, arefsynced, thenrenamed into the content-addressed final path. Therenameis atomic on POSIX (a *concurrent reader* observing the path sees either the full content or nothing), but atomicity is not durability — thefsyncbefore the rename is what guards against a *host crash* leaving a torn/zero file at a published CAS path, because the serving path streams the hash-named file without re-hashing it (server/serve_fact_route.ts). This twins the Rustfuz_fact§fsync posture: data-sync before the rename; the parent-dir fsync stays deliberately waived (a lost dirent is regenerable under content addressing). If the final path already exists the temp is dropped instead of renamed over — idempotent dedup (same hash → byte-identical content), mirroring the Rust commit path..tmp/is a sibling of<shard>/under the samefacts_dirsorenameis always same-filesystem (no EXDEV).db/fact_queries.ts
Raw queries against the
factandfact_reftables.Convention:
deps: QueryDepsfirst, no audit side effects, mutations are idempotent (ON CONFLICT DO NOTHING) so the same hash can be written by two callers without the second observing an error.Higher-level lifecycle (verify-on-read, JSON ref auto-extraction, embedded-vs-referenced selection) lives in db/fact_store.ts. Queries here are deliberately mechanical.
db/fact_store_errors.ts
Typed errors thrown by
PgFactStore.put_streamso a file-store route can map them to the canonical wire responses.The Rust twin uses
FactError::PayloadTooLarge/::StorageFull(fuz_fact); these TS classes carry the same two cases so the upload handler can branch identically and return the same status + body shape (413/507).db/fact_store.ts
PG-backed
FactStoreimplementation.Wraps the raw queries in db/fact_queries.ts with the lifecycle the
FactStoreinterface promises:- sync hash on
put, stream hash onput_ref(counting bytes against the caller-suppliedsize) - idempotent insert (
ON CONFLICT DO NOTHINGin the queries layer) - JSON ref auto-extraction when
content_typesignals JSON and the caller didn't pass an explicitrefsarray - verify-on-read for external content; embedded reads skip verify because PG storage IS the hash table
- mismatched external bytes return
null+ log warning (treat as unavailable; GC / repair is a separate concern)
Embedded vs disk split: writes route by size. Bytes
<= embedded_thresholdland in the PGbytescolumn; larger bytes go to the disk CAS at<facts_dir>/<shard>/<rest>(db/fact_disk_storage.ts) and the row records afile:<shard>/<rest>external_url.puttakes fully-buffered bytes;put_streamis the bounded-memory streaming twin (hash BLAKE3 + SHA-256 in one pass, spill past the threshold, enforcemax_bytes/ENOSPC). Both needdisk_root+fs(theruntime/*Deps) configured for the over-threshold path; without them, an oversizeputthrows and the caller mustput_refagainst an externally-managed URL (federation / stub-fetcher tests).- sync hash on
db/file_fact_url.ts
Canonical filesystem-fact URL shape + on-disk layout.
external_urlon the genericfactrow isstring | nullbecause theFactStoreinterface stays federation-friendly (futurehttps://.../s3://...shapes). Filesystem-minted URLs are exactlyfile:<shard>/<rest>where<shard>is the first 2 hex chars of the blake3 digest and<rest>the remaining 62 — files land at<facts_dir>/<shard>/<rest>after the writer atomically temp+renames them in.Centralizing the regex + the fact_disk_path split keeps the shape in one place: PgFactStore's disk CAS (db/fact_disk_storage.ts), the
serve_fact_routedefense-in-depth check, and thefile_fact_fetcherresolver all derive the layout here, so the write path and the read path can't drift. The TS twin of the Rust fact_disk_path (fuz_fact).Defense-in-depth: a
..segment can't match (.isn't in[0-9a-f]), neither can absolute paths, query strings, or any non-hex character. Used in front ofpath.joinso the resolver never trusts the URL came from a fact row, even though it always does in practice.db/migrate.ts
Identity-tracked database migration runner.
Migrations are named
{name, up}objects in ordered arrays, grouped by namespace. Aschema_versiontable records one row per applied migration —(namespace, name, sequence, applied_at)— and the runner verifies the applied list is a name-prefix of the code's migration array at boot.nameis descriptive identity, not a version ordinal. The runner orders by thesequencecolumn and matchesnameby array position — it never parses the name — sonamecarries *what the migration is* (full_cell_schema,role_grant_offer_and_scoped_role_grants), and the redundant_vNordinal is avoided (it would only duplicatesequence, and acell_v0name tempts editing "v0" in place).nameis half theschema_versionPK (withnamespace), so a rename is an identity change every already-migrated DB sees asname-divergence-at-N. This is also the cross-impl contract: the TS and Rust spines must record byte-identical(namespace, name, sequence)so a consumer can swap backends over one DB.Schema is not stabilized yet — append-only is NOT the rule. While fuz_app is pre-stable, migration bodies, names, and positions can change freely between versions; consumers upgrading across a schema change are expected to drop and re-bootstrap their dev/test databases (production deployments are not yet a supported use case). Once the schema is declared stable a hard append-only-after-publish rule will apply and the cliff will be called out in that release's notes; until then, body edits to a published migration slip past the runner (no content hashing) by design — they're the recommended way to evolve the schema.
Chain-level transactions: All pending migrations in a namespace run in a single transaction. Any failure rolls back every migration in that run — no partial-state recovery. This rules out non-transactional DDL (e.g.,
CREATE INDEX CONCURRENTLY); run those out of band.Chain idempotency, not migration idempotency: the chain-tx wraps every migration replayed in a single boot, so an individual migration may temporarily produce intermediate state that a later migration reverses (e.g. v0's ROLE_GRANT_INDEXES recreates an index that v1 drops; chain-tx hides this from observers). What matters is that the *committed end state* matches; the in-tx steps may not be individually idempotent against an arbitrary mid-chain target.
Forward-only: No down-migrations. Schema changes are additive.
Advisory locking: Per-namespace
pg_advisory_lockreduces contention in multi-instance deployments — best-effort, not load-bearing. The locks are session-scoped, butDb.queryruns against a pool that may check out a different backend per call, so two concurrent boots can both "hold" the lock on different sessions. The real serialization comes from chain- tx atomicity + the(namespace, name)PK onschema_version: the loser's INSERT hits a PK violation, the chain-tx rolls back, and the next boot reads the committed state and proceeds cleanly. Environments withoutpg_advisory_lock(some PGlite versions) silently fall through.db/pg_error.ts
PostgreSQL error utilities.
Works with both
pgand@electric-sql/pglite— both set.codeon error objects using standard PostgreSQL error codes.db/query_deps.ts
Shared query dependency type.
All
query_*functions takedeps: QueryDepsas their first argument. Widened per-function when additional capabilities are needed (e.g.,logfor token validation).db/schema_ready.ts
Readiness probe core: live-DB schema-drift detection.
/healthis a dumb liveness probe (no DB)./readyis the deploy gate — it introspects the live database's column set and compares it against a committed expected column map (what a fresh full migration-chain bootstrap produces). A live DB missing an expected column is exactly the failure mode that silently broke login when the auth schema gainedaccount.deleted_atvia an in-place base-DDL edit instead of an appended migration: the deployed code required a column an older bootstrapped DB never got, and aSELECT *+ JSdeleted_at === nullfilter rejected every account. The/readyroute (http/common_routes.ts) turns that drift into a loud503so a deploy poll rolls the release back instead of promoting code that can't authenticate anyone. The discipline that prevents the drift is the frozen append-only migration chain (auth/migrations.ts); this probe is the runtime net for a lapse.The check is intentionally column-presence only — not type / constraint / index parity. Column names are DDL-deterministic and engine-portable, so a map generated against PGlite at gen-time compares exactly against a live Postgres at runtime; finer-grained parity would false-positive across the two engines, and a false positive here means a rolled-back deploy — an outage you caused. Full structural parity stays the dev-time cross-backend schema-snapshot suite's job (testing/schema_introspect.ts). In-place *type* changes (a column kept by name, retyped) are out of scope — they rely on the query-time column-named failures instead.
This module is pure DB introspection + comparison: no HTTP, no filesystem, no fixture-path knowledge. The route factory and the committed-fixture loader live in http/common_routes.ts; the gen-time fixture-regeneration helper lives in testing/schema_ready_fixture.ts.
db/sql_identifier.ts
SQL identifier validation for dynamic DDL queries.
PostgreSQL DDL operations (DROP TABLE, TRUNCATE, ALTER) do not support parameterized table/column names — only values can be parameterized. This validator ensures identifiers are safe for string interpolation in those specific cases.
db/status.ts
Database status utility for CLI and dev workflows.
Queries migration state and table info without a running server. Returns structured data that consumer scripts can print however they like.
The migration check is name-divergence aware: it name-prefix-verifies the applied migrations against the code's list (mirroring run_migrations), so a divergent history (same count, different names) reports a
divergenceand rendersDIVERGEDrather than a falseup_to_date.