@@ -70,12 +70,12 @@ make this package uninstallable outside the project that defines it.
7070
7171## Migrations
7272
73- Shipped migrations are immutable. Each ledger row records a checksum of the migration's
74- rendered SQL , so editing one that has already been applied fails with
75- ` MigrationChecksumError ` on the next boot rather than letting fresh and existing
76- databases diverge. Add a new migration instead .
73+ Migrations are SQL files under ` migrations/ ` , applied in filename order on every
74+ boot , so every statement must be idempotent ( ` IF NOT EXISTS ` , ` IF EXISTS ` ). There
75+ is no ledger: a schema change is a new file whose statements are safe to re-run,
76+ never an edit that assumes it runs once .
7777
78- ` schema.ts ` and ` migrations.ts ` must agree — every query goes through the drizzle
78+ ` schema.ts ` and ` migrations/ ` must agree — every query goes through the drizzle
7979table objects, and a test asserts the migrations create exactly the tables
8080` schema.ts ` declares, no more and no less. Change one, change the other, in the
8181same commit.
@@ -235,20 +235,20 @@ which store is installed.
235235### Data model
236236
237237Four physical tables — ` artifact ` , ` artifact_version ` , ` upload ` ,
238- ` mail_attachment_ref ` — plus this package's own migration ledger .
238+ ` mail_attachment_ref ` .
239239
240240** Hard control-plane foreign keys, by design.** ` tenant_id ` is ` NOT NULL ` and
241- references ` public. tenant(id)` (` ON DELETE CASCADE ` — a deleted tenant takes its
241+ references the host's ` tenant(id) ` (` ON DELETE CASCADE ` — a deleted tenant takes its
242242artifacts with it) and ` principal_id ` / ` owner_principal_id ` reference
243- ` public. principal(id)` (` ON DELETE SET NULL ` — a removed principal detaches its
243+ the host's ` principal(id) ` (` ON DELETE SET NULL ` — a removed principal detaches its
244244artifacts rather than destroying them). This package is coupled to Interchange:
245245it mounts on Interchange-shaped hosts only, and the host's own migrations must
246246have run before ` runArtifactMigrations ` . The internal key —
247247` artifact_version.artifact_id ` — cascades with its artifact.
248248
249249** Cheap row-local CHECKs.** ` artifact.version ` and ` artifact_version.version `
250250must be ≥ 1; ` upload.size ` and ` mail_attachment_ref.size ` must be ≥ 0. These are
251- single-column constraints applied by a ledgered migration — free at write time.
251+ single-column constraints — free at write time.
252252
253253** Principal↔tenant alignment is host-owned.** The package FKs each column into
254254the control plane independently; it does ** not** enforce that ` principal_id ` (or
@@ -302,9 +302,7 @@ path that promises it.
302302Separately, this package also has no way to confirm existing tenants are
303303already free of duplicate ` (title, kind) ` rows, which would make even a
304304scoped constraint risky to backfill. That is not the main reason for
305- rejecting the constraint, and it is not by itself decisive. See
306- ` 0003_schema_invariants ` for this repo's own pattern for guarding a
307- migration against exactly that kind of bad existing data.
305+ rejecting the constraint, and it is not by itself decisive.
308306
309307Instead, ` findOrVersionArtifact(db, args) ` (in ` artifacts.ts ` ) closes the
310308race with a transaction-scoped advisory lock keyed by
@@ -336,55 +334,35 @@ behind it.
336334
337335### Migration runner
338336
339- ` runArtifactMigrations(db) ` is idempotent and safe to call unconditionally on
340- every boot of every replica.
337+ ` runArtifactMigrations(config, { schema }) ` takes the same arguments as
338+ Interchange's ` runMigrations ` , and a host calls it right after that, with the
339+ same values. ` schema ` is where the host's ` tenant ` and ` principal ` tables live;
340+ the runner rewrites the ` "public". ` foreign-key references in the SQL files to
341+ it. It is idempotent and safe to call on every boot of every replica.
341342
342- - The whole run is one transaction whose first statements are
343- ` SET LOCAL client_min_messages = warning ` and a ** transaction-scoped**
344- advisory lock. A transaction pins one pooled connection, so the lock, the
345- ledger read and the DDL are the same session; the lock releases on commit or
346- rollback, so there is no unlock call to lose on an error path.
343+ - The whole run is one transaction whose first statement takes a
344+ ** transaction-scoped** advisory lock, so the lock releases on commit or
345+ rollback and there is no unlock call to lose on an error path.
347346 ` CREATE TABLE IF NOT EXISTS ` is not itself race-safe, so the lock — not the
348347 ` IF NOT EXISTS ` — is what makes concurrent cold starts safe.
349- - Lowering ` client_min_messages ` is why a re-run prints ** nothing** : every
350- statement is ` IF NOT EXISTS ` , and on the second boot Postgres answers each with
351- a NOTICE that postgres.js would otherwise dump to the console, making a clean
352- re-boot look like a wall of errors. ` SET LOCAL ` scopes it to the transaction
353- and stops at NOTICE — WARNING and above still reach the host.
354- - Each migration applies inside a nested transaction (a savepoint) together with
355- its ledger row, so a migration can never be recorded as applied with only some
356- of its statements run.
357- - The ledger is this package's own table, ` artifacts.migrations ` ,
358- never shared with a host's. Each row records a ** checksum of the migration's
359- rendered SQL** , so editing a shipped migration fails with
360- ` MigrationChecksumError ` on the next boot instead of letting existing and
361- fresh databases diverge silently. Ship a new migration instead. The column is
362- ` NOT NULL ` , so the guarantee is unconditional: there is no unrecorded row for
363- the runner to adopt and wave through.
348+ - The runner opens its own single-connection client and discards NOTICEs, so a
349+ re-run, where Postgres answers every ` IF NOT EXISTS ` with a NOTICE, prints
350+ nothing.
364351- Event timestamps (` created_at ` , ` updated_at ` , ` archived_at ` ) are
365- ** ` timestamptz ` ** . The initial create migration still lays them down as
366- zoneless ` timestamp ` ; a follow-on migration retypes them with
367- ` USING col AT TIME ZONE 'UTC' ` , treating existing walls as the UTC clocks the
368- package always assumed. List keyset cursors project through
369- ` AT TIME ZONE 'UTC' ` and compare with ` ::timestamptz ` , so paging and date
370- filters stay on the absolute instant under any session ` TimeZone ` . Rollback is
371- the reverse cast (` TYPE timestamp USING col AT TIME ZONE 'UTC' ` ) plus a new
372- ledgered migration — never edit a shipped one.
373- - A later ledgered migration sets ` artifact.tenant_id NOT NULL ` and adds the
374- version/size CHECKs. If null-tenant rows still exist, that migration raises
375- before altering the column so the operator can clean them up first.
376- - Empty ledger + pre-existing package objects fails closed
377- (` MigrationAdoptError ` ). ` { adopt: true } ` records checksums without re-DDL
378- only after shape validation: tables, column types, required nullability, and
379- the named CHECK constraints. Column presence alone is not enough.
380-
381- ** The package owns its own Postgres schema.** Every table, index and the ledger
382- live in ` artifacts ` , created by the runner and qualified in every
383- DDL statement and every query — nothing resolves through ` search_path ` , so the
384- package shares a database with the host's control plane without ever being able
385- to collide with (or silently adopt) a host table of the same name. The coupling
386- to the host is explicit instead: ` tenant_id ` and the principal columns are hard
387- FKs into ` public.tenant ` / ` public.principal ` (see the data model).
352+ ** ` timestamptz ` ** . List keyset cursors project through ` AT TIME ZONE 'UTC' `
353+ and compare with ` ::timestamptz ` , so paging and date filters stay on the
354+ absolute instant under any session ` TimeZone ` .
355+ - Releases up to 0.1.0 kept a checksum ledger in ` artifacts.migrations ` .
356+ ` 0002_drop_migration_ledger.sql ` removes it; a database migrated by 0.1.0
357+ already has the shape ` 0001_artifacts.sql ` creates, so its statements no-op.
358+
359+ ** The package owns its own Postgres schema.** Every table and index lives in
360+ ` artifacts ` , created by the runner and qualified in every DDL statement and
361+ every query — nothing resolves through ` search_path ` , so the package shares a
362+ database with the host's control plane without ever being able to collide with
363+ (or silently adopt) a host table of the same name. The coupling to the host is
364+ explicit instead: ` tenant_id ` and the principal columns are hard FKs into the
365+ host schema's ` tenant ` / ` principal ` (see the data model).
388366
389367### Boundaries
390368
0 commit comments