Oloka-Video-Dev / apps /server /src /database /migration-runner.integration.test.ts
phucsd's picture
Deploy GitHub commit cf61358f7d8603b9bb93c3aa54d5b408797f6770
146ca6d verified
Raw History Blame Contribute Delete
24.2 kB
import { mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { DatabaseSync } from "node:sqlite";
import { pathToFileURL } from "node:url";
import { afterEach, describe, expect, it } from "vitest";
import { SqliteSystemDatabase } from "./sqlite-system-database.js";
import {
restoreVerifiedBackup,
verifyBackupDirectory,
} from "./pre-migration-backup.js";
import { sha256Hex } from "../kernel/canonical-json.js";
const temporaryDirectories: string[] = [];
afterEach(async () => {
await Promise.all(
temporaryDirectories
.splice(0)
.map((directory) => rm(directory, { recursive: true, force: true })),
);
});
describe("database migrations", () => {
it("preserves prior tables and adds only the authorized Phase 3F tables", async () => {
const directory = await mkdtemp(join(tmpdir(), "oloka-migration-"));
temporaryDirectories.push(directory);
const database = await SqliteSystemDatabase.connect(
pathToFileURL(join(directory, "database.sqlite")).href,
);
await database.migrate();
expect(database.listApplicationTables()).toEqual([
"assets",
"audit_events",
"composition_asset_references",
"composition_versions",
"delivery_capabilities",
"idempotency_records",
"job_events",
"job_steps",
"jobs",
"oauth_identities",
"oauth_transactions",
"outbox_events",
"preview_artifacts",
"projects",
"provider_credential_references",
"quota_policies",
"quota_reservations",
"schema_migrations",
"sessions",
"system_metadata",
"upload_sessions",
"users",
]);
const ledger = database.transactions.run(
"read",
({ database: connection }) =>
connection
.prepare(
"SELECT version, name, checksum_sha256, applied_at, execution_ms, app_build_sha FROM schema_migrations ORDER BY version",
)
.all(),
) as Array<Record<string, unknown>>;
expect(ledger).toHaveLength(7);
expect(ledger[0]).toMatchObject({
version: 1,
name: "foundation_system_tables",
checksum_sha256:
"34b305e2ec0bd443813108c8fa68bdb1008e1fef90d3d394f76a5e27e32619c9",
app_build_sha: "foundation-bootstrap-v1",
});
expect(ledger[1]).toMatchObject({
version: 2,
name: "persistence-kernel",
checksum_sha256: sha256Hex(
await readFile(
new URL(
"../../migrations/0002-persistence-kernel.sql",
import.meta.url,
),
),
),
});
expect(ledger[2]).toMatchObject({
version: 3,
name: "identity-and-approval",
checksum_sha256: sha256Hex(
await readFile(
new URL(
"../../migrations/0003-identity-and-approval.sql",
import.meta.url,
),
),
),
});
expect(ledger[3]).toMatchObject({
version: 4,
name: "canonical-project",
checksum_sha256: sha256Hex(
await readFile(
new URL(
"../../migrations/0004-canonical-project.sql",
import.meta.url,
),
),
),
});
expect(ledger[4]).toMatchObject({
version: 5,
name: "private-assets",
checksum_sha256: sha256Hex(
await readFile(
new URL("../../migrations/0005-private-assets.sql", import.meta.url),
),
),
});
expect(ledger[5]).toMatchObject({
version: 6,
name: "durable-job-kernel",
checksum_sha256: sha256Hex(
await readFile(
new URL(
"../../migrations/0006-durable-job-kernel.sql",
import.meta.url,
),
),
),
});
expect(ledger[6]).toMatchObject({
version: 7,
name: "compositions-preview",
checksum_sha256: sha256Hex(
await readFile(
new URL(
"../../migrations/0007-compositions-preview.sql",
import.meta.url,
),
),
),
});
await database.close();
});
it("applies migrations v1 through v7 on a fresh database", async () => {
const directory = await mkdtemp(join(tmpdir(), "oloka-migration-"));
temporaryDirectories.push(directory);
const database = await SqliteSystemDatabase.connect(
pathToFileURL(join(directory, "database.sqlite")).href,
);
await database.migrate();
const ledger = database.transactions.run(
"read",
({ database: connection }) =>
connection
.prepare(
"SELECT version, name FROM schema_migrations ORDER BY version",
)
.all(),
);
expect(ledger).toEqual([
{ version: 1, name: "foundation_system_tables" },
{ version: 2, name: "persistence-kernel" },
{ version: 3, name: "identity-and-approval" },
{ version: 4, name: "canonical-project" },
{ version: 5, name: "private-assets" },
{ version: 6, name: "durable-job-kernel" },
{ version: 7, name: "compositions-preview" },
]);
await database.close();
});
it("qualifies the canonical Project foreign keys after migration v7", async () => {
const directory = await mkdtemp(join(tmpdir(), "oloka-migration-v7-fk-"));
temporaryDirectories.push(directory);
const database = await SqliteSystemDatabase.connect(
pathToFileURL(join(directory, "database.sqlite")).href,
);
await database.migrate();
const evidence = database.transactions.run(
"read",
({ database: connection }) => ({
compositionForeignKeys: connection
.prepare("PRAGMA foreign_key_list(composition_versions)")
.all(),
projectForeignKeys: connection
.prepare("PRAGMA foreign_key_list(projects)")
.all(),
legacyProject: connection
.prepare("SELECT type FROM sqlite_schema WHERE name = 'projects_v6'")
.get(),
foreignKeyFailures: connection
.prepare("PRAGMA foreign_key_check")
.all(),
}),
);
expect(evidence.compositionForeignKeys).toEqual(
expect.arrayContaining([expect.objectContaining({ table: "projects" })]),
);
expect(evidence.compositionForeignKeys).not.toEqual(
expect.arrayContaining([
expect.objectContaining({ table: "projects_v6" }),
]),
);
expect(evidence.projectForeignKeys).toEqual(
expect.arrayContaining([
expect.objectContaining({
from: "current_composition_version_id",
table: "composition_versions",
}),
]),
);
expect(evidence.legacyProject).toBeUndefined();
expect(evidence.foreignKeyFailures).toEqual([]);
await database.close();
});
it("refuses to migrate an existing v1 database without an application key", async () => {
const directory = await mkdtemp(join(tmpdir(), "oloka-migration-"));
temporaryDirectories.push(directory);
const databasePath = join(directory, "database.sqlite");
const legacy = new DatabaseSync(databasePath);
legacy.exec(
await readFile(
new URL(
"../../migrations/0001-foundation-system-tables.sql",
import.meta.url,
),
"utf8",
),
);
legacy.close();
const database = await SqliteSystemDatabase.connect(
pathToFileURL(databasePath).href,
);
await expect(database.migrate()).rejects.toThrow(/application key/i);
await database.close();
});
it("rejects a pre-created zero-byte database instead of treating it as fresh", async () => {
const directory = await mkdtemp(join(tmpdir(), "oloka-migration-"));
temporaryDirectories.push(directory);
const databasePath = join(directory, "database.sqlite");
await writeFile(databasePath, new Uint8Array());
await expect(
SqliteSystemDatabase.connect(pathToFileURL(databasePath).href),
).rejects.toThrow(/zero bytes/i);
});
it("creates and verifies an authenticated online backup before upgrading v1", async () => {
const directory = await mkdtemp(join(tmpdir(), "oloka-migration-"));
temporaryDirectories.push(directory);
const databasePath = join(directory, "database.sqlite");
const legacy = new DatabaseSync(databasePath);
legacy.exec(
await readFile(
new URL(
"../../migrations/0001-foundation-system-tables.sql",
import.meta.url,
),
"utf8",
),
);
legacy.close();
const applicationKey = Buffer.alloc(32, 9);
const backupRoot = join(directory, "backups");
const database = await SqliteSystemDatabase.connect(
pathToFileURL(databasePath).href,
{
appKey: applicationKey,
backupRoot,
idGenerator: { generate: () => "backup-set-opaque" },
appBuildSha: "test-build",
},
);
await database.migrate();
const backupDirectory = join(
backupRoot,
"pre-migration",
"backup-set-opaque",
);
const manifest = await verifyBackupDirectory(
backupDirectory,
applicationKey,
);
expect(manifest).toMatchObject({
sourceSchemaVersion: 1,
targetSchemaVersion: 7,
migrationVersionsPending: [2, 3, 4, 5, 6, 7],
appBuildSha: "test-build",
});
await expect(
verifyBackupDirectory(backupDirectory, Buffer.alloc(32, 3)),
).rejects.toThrow(/authentication/i);
const restoredPath = join(directory, "restore", "database.sqlite");
await restoreVerifiedBackup(backupDirectory, restoredPath, applicationKey);
const restored = new DatabaseSync(restoredPath, { readOnly: true });
expect(
restored.prepare("SELECT version, name FROM schema_migrations").get(),
).toEqual({ version: 1, name: "foundation_system_tables" });
restored.close();
await database.close();
});
it("is a verified no-op when every migration is already applied", async () => {
const directory = await mkdtemp(join(tmpdir(), "oloka-migration-"));
temporaryDirectories.push(directory);
const database = await SqliteSystemDatabase.connect(
pathToFileURL(join(directory, "database.sqlite")).href,
);
await database.migrate();
const before = database.transactions.run(
"read",
({ database: connection }) =>
connection
.prepare("SELECT * FROM schema_migrations ORDER BY version")
.all(),
);
await database.migrate();
const after = database.transactions.run(
"read",
({ database: connection }) =>
connection
.prepare("SELECT * FROM schema_migrations ORDER BY version")
.all(),
);
expect(after).toEqual(before);
await database.close();
});
it("creates one verified v2-to-v7 backup and no new backup on restart", async () => {
const directory = await mkdtemp(join(tmpdir(), "oloka-migration-"));
temporaryDirectories.push(directory);
const databasePath = join(directory, "database.sqlite");
const legacy = new DatabaseSync(databasePath);
legacy.exec(
await readFile(
new URL(
"../../migrations/0001-foundation-system-tables.sql",
import.meta.url,
),
"utf8",
),
);
const v2Bytes = await readFile(
new URL("../../migrations/0002-persistence-kernel.sql", import.meta.url),
);
legacy.exec(v2Bytes.toString("utf8"));
legacy
.prepare(
`INSERT INTO schema_migrations
(version, name, checksum_sha256, applied_at, execution_ms, app_build_sha)
VALUES (2, 'persistence-kernel', ?, 2, 0, 'slice-3a1')`,
)
.run(sha256Hex(v2Bytes));
legacy.close();
const backupRoot = join(directory, "backups");
let id = 0;
const applicationKey = Buffer.alloc(32, 5);
const database = await SqliteSystemDatabase.connect(
pathToFileURL(databasePath).href,
{
appKey: applicationKey,
backupRoot,
appBuildSha: "slice-3b",
idGenerator: { generate: () => `backup-v3-${++id}` },
},
);
await database.migrate();
await expect(
verifyBackupDirectory(
join(backupRoot, "pre-migration", "backup-v3-1"),
applicationKey,
),
).resolves.toMatchObject({
sourceSchemaVersion: 2,
targetSchemaVersion: 7,
migrationVersionsPending: [3, 4, 5, 6, 7],
});
await database.migrate();
expect(await readdir(join(backupRoot, "pre-migration"))).toEqual([
"backup-v3-1",
]);
await database.close();
});
it("creates one verified exact-v3-to-v7 backup and applies later migrations once", async () => {
const directory = await mkdtemp(join(tmpdir(), "oloka-migration-v3-"));
temporaryDirectories.push(directory);
const databasePath = join(directory, "database.sqlite");
const legacy = new DatabaseSync(databasePath);
legacy.exec(
await readFile(
new URL(
"../../migrations/0001-foundation-system-tables.sql",
import.meta.url,
),
"utf8",
),
);
const v2Bytes = await readFile(
new URL("../../migrations/0002-persistence-kernel.sql", import.meta.url),
);
legacy.exec(v2Bytes.toString("utf8"));
legacy
.prepare(
`INSERT INTO schema_migrations
(version, name, checksum_sha256, applied_at, execution_ms, app_build_sha)
VALUES (2, 'persistence-kernel', ?, 2, 0, 'slice-3a1')`,
)
.run(sha256Hex(v2Bytes));
const v3Bytes = await readFile(
new URL(
"../../migrations/0003-identity-and-approval.sql",
import.meta.url,
),
);
legacy.exec(v3Bytes.toString("utf8"));
legacy
.prepare(
`INSERT INTO schema_migrations
(version, name, checksum_sha256, applied_at, execution_ms, app_build_sha)
VALUES (3, 'identity-and-approval', ?, 3, 0, 'slice-3b')`,
)
.run(sha256Hex(v3Bytes));
legacy.close();
const backupRoot = join(directory, "backups");
const applicationKey = Buffer.alloc(32, 7);
const database = await SqliteSystemDatabase.connect(
pathToFileURL(databasePath).href,
{
appKey: applicationKey,
backupRoot,
appBuildSha: "slice-3c",
idGenerator: { generate: () => "backup-v4-exact-v3" },
},
);
await database.migrate();
await expect(
verifyBackupDirectory(
join(backupRoot, "pre-migration", "backup-v4-exact-v3"),
applicationKey,
),
).resolves.toMatchObject({
sourceSchemaVersion: 3,
targetSchemaVersion: 7,
migrationVersionsPending: [4, 5, 6, 7],
});
const beforeRestart = database.transactions.run("read", ({ database }) =>
database
.prepare("SELECT * FROM schema_migrations ORDER BY version")
.all(),
);
await database.migrate();
const afterRestart = database.transactions.run("read", ({ database }) =>
database
.prepare("SELECT * FROM schema_migrations ORDER BY version")
.all(),
);
expect(afterRestart).toEqual(beforeRestart);
expect(await readdir(join(backupRoot, "pre-migration"))).toEqual([
"backup-v4-exact-v3",
]);
await database.close();
});
it.each([4, 5, 6])(
"qualifies an exact v%s database through v7",
async (sourceVersion) => {
const directory = await mkdtemp(
join(tmpdir(), `oloka-migration-v${sourceVersion}-`),
);
temporaryDirectories.push(directory);
const databasePath = join(directory, "database.sqlite");
await createDatabaseAtVersion(databasePath, sourceVersion);
const backupRoot = join(directory, "backups");
const applicationKey = Buffer.alloc(32, sourceVersion);
const backupId = `backup-v${sourceVersion}-to-v7`;
const database = await SqliteSystemDatabase.connect(
pathToFileURL(databasePath).href,
{
appKey: applicationKey,
backupRoot,
appBuildSha: "phase3f-qualification",
idGenerator: { generate: () => backupId },
},
);
await database.migrate();
const evidence = database.transactions.run(
"read",
({ database: connection }) => ({
versions: connection
.prepare("SELECT version FROM schema_migrations ORDER BY version")
.all()
.map((row) => (row as { version: number }).version),
quickCheck: connection.prepare("PRAGMA quick_check").get(),
foreignKeys: connection.prepare("PRAGMA foreign_key_check").all(),
}),
);
expect(evidence.versions).toEqual([1, 2, 3, 4, 5, 6, 7]);
expect(Object.values(evidence.quickCheck as object)).toContain("ok");
expect(evidence.foreignKeys).toEqual([]);
await expect(
verifyBackupDirectory(
join(backupRoot, "pre-migration", backupId),
applicationKey,
),
).resolves.toMatchObject({
sourceSchemaVersion: sourceVersion,
targetSchemaVersion: 7,
migrationVersionsPending: Array.from(
{ length: 7 - sourceVersion },
(_, index) => sourceVersion + index + 1,
),
});
await database.close();
},
15_000,
);
it("rolls back an injected v7 failure without changing the v6 ledger", async () => {
const directory = await mkdtemp(
join(tmpdir(), "oloka-migration-v7-failure-"),
);
temporaryDirectories.push(directory);
const databasePath = join(directory, "database.sqlite");
await createDatabaseAtVersion(databasePath, 6);
const injected = new DatabaseSync(databasePath);
injected.exec(
"CREATE VIEW composition_versions AS SELECT id FROM projects",
);
injected.close();
const database = await SqliteSystemDatabase.connect(
pathToFileURL(databasePath).href,
{
appKey: Buffer.alloc(32, 7),
backupRoot: join(directory, "backups"),
idGenerator: { generate: () => "backup-before-v7-failure" },
},
);
await expect(database.migrate()).rejects.toThrow(/already exists/i);
const state = database.transactions.run(
"read",
({ database: connection }) => ({
versions: connection
.prepare("SELECT version FROM schema_migrations ORDER BY version")
.all()
.map((row) => (row as { version: number }).version),
injectedView: connection
.prepare(
"SELECT type FROM sqlite_schema WHERE name = 'composition_versions'",
)
.get(),
previewTable: connection
.prepare(
"SELECT type FROM sqlite_schema WHERE name = 'preview_artifacts'",
)
.get(),
foreignKeys: connection.prepare("PRAGMA foreign_key_check").all(),
}),
);
expect(state.versions).toEqual([1, 2, 3, 4, 5, 6]);
expect(state.injectedView).toEqual({ type: "view" });
expect(state.previewTable).toBeUndefined();
expect(state.foreignKeys).toEqual([]);
await database.close();
});
it("rolls back v4 when the migration SQL cannot create its table", async () => {
const directory = await mkdtemp(
join(tmpdir(), "oloka-migration-v4-failure-"),
);
temporaryDirectories.push(directory);
const databasePath = join(directory, "database.sqlite");
const legacy = new DatabaseSync(databasePath);
legacy.exec(
await readFile(
new URL(
"../../migrations/0001-foundation-system-tables.sql",
import.meta.url,
),
"utf8",
),
);
for (const [version, name, filename] of [
[2, "persistence-kernel", "0002-persistence-kernel.sql"],
[3, "identity-and-approval", "0003-identity-and-approval.sql"],
] as const) {
const bytes = await readFile(
new URL(`../../migrations/${filename}`, import.meta.url),
);
legacy.exec(bytes.toString("utf8"));
legacy
.prepare(
`INSERT INTO schema_migrations
(version, name, checksum_sha256, applied_at, execution_ms, app_build_sha)
VALUES (?, ?, ?, ?, 0, 'rollback-fixture')`,
)
.run(version, name, sha256Hex(bytes), version);
}
legacy.exec("CREATE VIEW projects AS SELECT id FROM users");
legacy.close();
const database = await SqliteSystemDatabase.connect(
pathToFileURL(databasePath).href,
{
appKey: Buffer.alloc(32, 6),
backupRoot: join(directory, "backups"),
idGenerator: { generate: () => "backup-before-v4-failure" },
},
);
await expect(database.migrate()).rejects.toThrow(/already exists/i);
const state = database.transactions.run("read", ({ database }) => ({
versions: database
.prepare("SELECT version FROM schema_migrations ORDER BY version")
.all()
.map((row) => (row as { version: number }).version),
projectObject: database
.prepare("SELECT type FROM sqlite_schema WHERE name = 'projects'")
.get(),
projectIndexes: database
.prepare(
"SELECT name FROM sqlite_schema WHERE name LIKE 'projects_%_idx'",
)
.all(),
}));
expect(state.versions).toEqual([1, 2, 3]);
expect(state.projectObject).toEqual({ type: "view" });
expect(state.projectIndexes).toEqual([]);
await database.close();
});
it("backs up then fails closed on unknown non-empty legacy metadata", async () => {
const directory = await mkdtemp(join(tmpdir(), "oloka-migration-"));
temporaryDirectories.push(directory);
const databasePath = join(directory, "database.sqlite");
const legacy = new DatabaseSync(databasePath);
legacy.exec(
await readFile(
new URL(
"../../migrations/0001-foundation-system-tables.sql",
import.meta.url,
),
"utf8",
),
);
legacy
.prepare(
"INSERT INTO system_metadata (key, value, updated_at) VALUES ('unknown', 'value', datetime('now'))",
)
.run();
legacy.close();
const backupRoot = join(directory, "backups");
const database = await SqliteSystemDatabase.connect(
pathToFileURL(databasePath).href,
{
appKey: Buffer.alloc(32, 8),
backupRoot,
idGenerator: { generate: () => "before-metadata-rejection" },
},
);
await expect(database.migrate()).rejects.toThrow(/legacy system metadata/i);
await expect(
verifyBackupDirectory(
join(backupRoot, "pre-migration", "before-metadata-rejection"),
Buffer.alloc(32, 8),
),
).resolves.toMatchObject({ sourceSchemaVersion: 1 });
const ledger = database.transactions.run(
"read",
({ database: connection }) =>
connection.prepare("SELECT version, name FROM schema_migrations").get(),
);
expect(ledger).toEqual({ version: 1, name: "foundation_system_tables" });
await database.close();
});
});
async function createDatabaseAtVersion(
databasePath: string,
targetVersion: number,
): Promise<void> {
const migrations = [
[1, "foundation_system_tables", "0001-foundation-system-tables.sql"],
[2, "persistence-kernel", "0002-persistence-kernel.sql"],
[3, "identity-and-approval", "0003-identity-and-approval.sql"],
[4, "canonical-project", "0004-canonical-project.sql"],
[5, "private-assets", "0005-private-assets.sql"],
[6, "durable-job-kernel", "0006-durable-job-kernel.sql"],
] as const;
const connection = new DatabaseSync(databasePath);
try {
for (const [version, name, filename] of migrations) {
if (version > targetVersion) break;
const bytes = await readFile(
new URL(`../../migrations/${filename}`, import.meta.url),
);
connection.exec(bytes.toString("utf8"));
if (version > 1) {
connection
.prepare(
`INSERT INTO schema_migrations (version,name,checksum_sha256,applied_at,execution_ms,app_build_sha) VALUES (?,?,?,?,0,'qualification-fixture')`,
)
.run(version, name, sha256Hex(bytes), version);
}
}
} finally {
connection.close();
}
}