Study/tests/migrationPreflight.test.ts

151 lines
5.1 KiB
TypeScript

import { spawnSync } from "node:child_process";
import { readFileSync } from "node:fs";
import path from "node:path";
import Database from "better-sqlite3";
import { afterEach, describe, expect, it } from "vitest";
import {
applyCommittedMigrations,
createDisposableDatabase,
disposeDisposableDatabase,
type DisposableDatabase,
} from "./helpers/testDatabase";
const GROUP_MIGRATION = "20260807090000_add_material_groups";
const databases: DisposableDatabase[] = [];
afterEach(() => {
for (const database of databases.splice(0)) {
disposeDisposableDatabase(database);
}
});
function create(options: { excludeGroup?: boolean } = {}) {
const database = createDisposableDatabase();
databases.push(database);
applyCommittedMigrations(
database,
options.excludeGroup ? { through: "20260714010000_add_arcade" } : {}
);
return database;
}
function inspect(database: DisposableDatabase) {
const result = spawnSync(
process.execPath,
[path.join(process.cwd(), "scripts", "migration-preflight.mjs")],
{
cwd: process.cwd(),
env: { ...process.env, DATABASE_URL: database.databaseUrl },
encoding: "utf8",
}
);
return { status: result.status, output: JSON.parse(result.stdout) };
}
describe("material group migration preflight", () => {
it("preserves populated pre-group rows and relationships", () => {
const database = create({ excludeGroup: true });
const client = new Database(database.databasePath);
try {
client.pragma("foreign_keys = ON");
client.prepare('INSERT INTO "Class" ("id", "slug", "name") VALUES (?, ?, ?)').run("class-1", "class", "Class");
client.prepare('INSERT INTO "Deck" ("id", "classId", "name") VALUES (?, ?, ?)').run("deck-1", "class-1", "Deck");
client.prepare('INSERT INTO "QuizSet" ("id", "classId", "name") VALUES (?, ?, ?)').run("quiz-1", "class-1", "Quiz");
client.prepare('INSERT INTO "ShareLink" ("id", "targetType", "deckId") VALUES (?, ?, ?)').run("share-1", "DECK", "deck-1");
client.exec(readFileSync(
path.join(process.cwd(), "prisma", "migrations", GROUP_MIGRATION, "migration.sql"),
"utf8"
));
expect(client.prepare('SELECT COUNT(*) AS count FROM "Deck"').get()).toEqual({ count: 1 });
expect(client.prepare('SELECT COUNT(*) AS count FROM "QuizSet"').get()).toEqual({ count: 1 });
expect(client.prepare('SELECT "deckId", "groupId" FROM "ShareLink" WHERE "id" = ?').get("share-1")).toEqual({
deckId: "deck-1",
groupId: null,
});
expect(client.pragma("foreign_key_check")).toEqual([]);
} finally {
client.close();
}
});
it("classifies a tracked pre-group database for normal apply", () => {
expect(inspect(create({ excludeGroup: true }))).toMatchObject({
status: 0,
output: { classification: "APPLY", priorHistoryComplete: true },
});
});
it("classifies an exact schema-pushed database for explicit adoption", () => {
const database = create({ excludeGroup: true });
const client = new Database(database.databasePath);
client.exec(readFileSync(
path.join(process.cwd(), "prisma", "migrations", GROUP_MIGRATION, "migration.sql"),
"utf8"
));
client.close();
expect(inspect(database)).toMatchObject({
status: 0,
output: {
classification: "ADOPT",
priorHistoryComplete: true,
exactSchema: { adoptedGroup: true },
},
});
});
it("rejects a partial schema without modifying it", () => {
const database = create();
const client = new Database(database.databasePath);
client.exec('DROP INDEX "ShareLink_groupId_key"');
const before = client
.prepare("SELECT COUNT(*) AS count FROM _prisma_migrations")
.get() as { count: number };
client.close();
expect(inspect(database)).toMatchObject({
status: 2,
output: { classification: "CONFLICT" },
});
const verification = new Database(database.databasePath, { readonly: true });
const after = verification
.prepare("SELECT COUNT(*) AS count FROM _prisma_migrations")
.get() as { count: number };
verification.close();
expect(after.count).toBe(before.count);
});
it("does not accept the source database itself as the adoption backup", () => {
const database = create({ excludeGroup: true });
const client = new Database(database.databasePath);
client.exec(readFileSync(
path.join(process.cwd(), "prisma", "migrations", GROUP_MIGRATION, "migration.sql"),
"utf8"
));
client.close();
const result = spawnSync(
process.execPath,
[
path.join(process.cwd(), "scripts", "migration-preflight.mjs"),
"--resolve",
"--backup",
database.databasePath,
],
{
cwd: process.cwd(),
env: { ...process.env, DATABASE_URL: database.databaseUrl },
encoding: "utf8",
}
);
expect(result.status).not.toBe(0);
expect(result.stderr).toContain("separate database file");
});
it("recognizes an already applied exact schema", () => {
expect(inspect(create())).toMatchObject({
status: 0,
output: { classification: "CURRENT" },
});
});
});