/** * A version's SQL migrations, stored apart from its manifest (#1764), shared by both adapters. * * Since #1677 a pushed deploy manifest carries its whole cumulative migration set * (`migrations: { moduleId, version, sql }[]`). The manifest is one `manifest_json` column on * `vertical_versions`, so every read of a version moved that SQL too: `readVersion` on every * admit, promote and bind, and every `storedDeployManifest.parse` behind `/registry`, `/model`, * `/schedules` and the serving upload. Only the promote review reads the SQL. * * So `publishVersion` splits the field off: the SQL goes to `vertical_version_migrations`, one * row per migration, and the manifest is stored without it. The wire is unchanged, since the * CLI still sends `migrations` inside the manifest. Two columns on `vertical_versions` carry * what the rows alone cannot: * * - `migration_count`: how many rows the version stored. **NULL means the version carries no * SQL** (pushed before #1677, over the push's caps, or malformed), which a reader must show * as "not available", never as "no migrations". `0` is a version whose modules ship none. A * read whose row count disagrees with it answers null rather than a short list. * - `migrations_split`: 1 once the version's SQL is out of its manifest. NULL is a version * stored before this, whose manifest may still carry it. The backfill below moves those a * bounded batch at a time, and until it reaches a version the read falls back to that * version's manifest. The fallback is only for those rows. A version published by this code * is split on the way in. */ import { type DeclaredMigration } from '@substrat-run/contracts'; import type { SwitchSql } from './system-switch.js'; /** * The table, and the index the backfill walks. Interpolated into both adapters' directory DDL, * so `lint:spine-ddl` sees one spelling on each side. Anything that references this table or * this index goes in this fragment, which is what the adapters run after their column additions. * * The index is partial: it holds only versions still waiting for the backfill, so "is there * anything left" is one probe that costs nothing once the backfill is done. It names a column * a directory from before #1764 lacks, so the adapters run these statements after their * column additions, not in their DDL loop. */ export declare const VERSION_MIGRATIONS_DDL = "\n CREATE TABLE IF NOT EXISTS vertical_version_migrations (\n version_id TEXT NOT NULL,\n -- The migration's place in the order the host runs them, from 0.\n ordinal INTEGER NOT NULL,\n module_id TEXT NOT NULL,\n -- The module's SqlMigration.version, e.g. 0001-init.\n version TEXT NOT NULL,\n sql TEXT NOT NULL,\n PRIMARY KEY (version_id, ordinal)\n );\n CREATE INDEX IF NOT EXISTS vertical_versions_unsplit\n ON vertical_versions (id) WHERE migrations_split IS NULL;\n"; /** A manifest with its SQL taken out, and the SQL. */ export interface SplitManifest { /** The manifest to store: without `migrations`, or exactly as given when there was nothing to take. */ manifestJson: string | null; /** The migrations to store, or null when the version carries none that can be stored. */ migrations: DeclaredMigration[] | null; } /** * Take `migrations` out of a manifest. * * `push` holds the field to the push boundary's caps (`deployManifest`): at most 2000 entries * and 512 KiB of SQL, which also bounds every row under a Durable Object's 2 MB row limit. * `/deploy` already refuses an over-cap set, before the upload, because it parses the whole * manifest with `deployManifest`. This split caps whatever reaches `publishVersion` by any * route, including `POST /verticals/:slug/versions`, which stores `manifestJson` unparsed * (#1765). There, a field over the caps or not shaped like migrations is dropped and the * version reads as "SQL not available", so the promote dialog still asks. `stored` is for a * manifest already in the directory, whose history the caps must never make unreadable * (`storedDeployManifest`). * * A manifest that is not a JSON object is returned as it came, with no migrations. The * manifest is only rewritten when it has a `migrations` key to remove. */ export declare function splitManifestMigrations(manifestJson: string | null, mode: 'push' | 'stored'): SplitManifest; /** Replace one version's migration rows. The caller sets its `migration_count` and `migrations_split`. */ export declare function writeVersionMigrations(db: SwitchSql, versionId: string, migrations: readonly DeclaredMigration[] | null): void; /** * The two reads that find unsplit versions. Named so the kernel's test can EXPLAIN these * exact strings: each must be answered from `vertical_versions_unsplit`, never by a scan of * every version's row (and its manifest's overflow pages). */ export declare const UNSPLIT_IDS_SQL = "SELECT id FROM vertical_versions WHERE migrations_split IS NULL ORDER BY id LIMIT ?"; export declare const UNSPLIT_PROBE_SQL = "SELECT 1 AS present FROM vertical_versions WHERE migrations_split IS NULL LIMIT 1"; /** * How much manifest one batch reads at most, in UTF-8 bytes (the first version is always * moved). A stored manifest can be up to about 1.5 MiB, and a batch holds the directory DO. * Bytes, because what a batch costs is the bytes it reads: a manifest of four-byte characters * is four times its character count. */ export declare const BATCH_MANIFEST_BYTES: number; /** * Move the SQL out of the next versions stored before #1764: at most `limit` of them, and * at most `BATCH_MANIFEST_BYTES` of manifest. Returns how many it moved, and whether any are * left. * * Bounded so that no one run has to read the whole version history: the Durable-Object * adapter runs one batch per alarm and re-arms while any are left, so its constructor never * does per-version work. Resumable because each version is marked as it is moved, in the * caller's transaction with its rows. A version is moved by the same `splitManifestMigrations` * a push goes through, in `stored` mode, so history is read without the push caps. * * Idempotent: a version that is already split is never selected, and moving one replaces its * rows. A directory restored from a dump taken before the backfill comes back unsplit and is * simply moved again. */ export declare function splitVersionMigrationsBatch(db: SwitchSql, limit?: number): { moved: number; more: boolean; }; /** Whether any version still waits for the backfill. One probe of the partial index. */ export declare function versionsAwaitSplit(db: SwitchSql): boolean; /** * One version's migrations, in the order the host runs them. `undefined` when there is no * such version. `migrations` is null when the version carries none that can be shown, which * a reader must say as "not available", never as "no migrations". */ export declare function versionMigrationsOf(db: SwitchSql, versionId: string): { verticalSlug: string; migrations: DeclaredMigration[] | null; } | undefined; //# sourceMappingURL=version-migrations.d.ts.map