diff --git a/packages/core/script/migration.ts b/packages/core/script/migration.ts index df83857c6..8c5f1f6cc 100644 --- a/packages/core/script/migration.ts +++ b/packages/core/script/migration.ts @@ -1,13 +1,21 @@ #!/usr/bin/env bun import { $ } from "bun" +import fs from "fs/promises" +import os from "os" import path from "path" +import { pathToFileURL } from "url" const root = path.resolve(import.meta.dirname, "../../..") const sqlDir = path.join(root, "packages/core/migration") const tsDir = path.join(root, "packages/core/src/database/migration") const registry = path.join(root, "packages/core/src/database/migration.gen.ts") +if (Bun.argv.includes("--check")) { + await check() + process.exit(0) +} + await $`bun drizzle-kit generate`.cwd(path.join(root, "packages/core")) const sqlMigrations = (await Array.fromAsync(new Bun.Glob("*/migration.sql").scan({ cwd: sqlDir }))) @@ -22,6 +30,49 @@ for (const name of sqlMigrations) { await Bun.write(registry, renderRegistry(sqlMigrations)) +async function check() { + const temporary = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-core-migration-check-")) + const output = path.join(temporary, "migration") + try { + await fs.cp(sqlDir, output, { recursive: true }) + const config = path.join(temporary, "drizzle.config.ts") + await Bun.write( + config, + `import config from ${JSON.stringify(pathToFileURL(path.join(root, "packages/core/drizzle.config.ts")).href)} + +export default { ...config, out: ${JSON.stringify(output)} } +`, + ) + const before = await snapshot(output) + await $`bun drizzle-kit generate --config ${config}`.cwd(path.join(root, "packages/core")) + const after = await snapshot(output) + if (JSON.stringify(after) !== JSON.stringify(before)) { + throw new Error("Core schema has ungenerated database migrations. Run `bun script/migration.ts` from packages/core.") + } + + const migrations = before + .map((entry) => entry.path.split("/")[0]) + .filter((name, index, all) => name !== undefined && all.indexOf(name) === index) + .sort() + for (const name of migrations) { + if (await Bun.file(path.join(tsDir, `${name}.ts`)).exists()) continue + throw new Error(`Database migration TypeScript wrapper is missing for ${name}. Run \`bun script/migration.ts\` from packages/core.`) + } + if ((await Bun.file(registry).text()) !== renderRegistry(migrations)) { + throw new Error("Database migration registry is stale. Run `bun script/migration.ts` from packages/core.") + } + } finally { + await fs.rm(temporary, { recursive: true, force: true }) + } +} + +async function snapshot(directory: string) { + const files = await Array.fromAsync(new Bun.Glob("**/*").scan({ cwd: directory, onlyFiles: true })) + return Promise.all( + files.sort().map(async (file) => ({ path: file, contents: await Bun.file(path.join(directory, file)).text() })), + ) +} + function renderMigration(name: string, sql: string) { return `import { Effect } from "effect" import type { DatabaseMigration } from "../migration" diff --git a/packages/core/test/database-migration.test.ts b/packages/core/test/database-migration.test.ts index 4df5982e4..8e7ee59a4 100644 --- a/packages/core/test/database-migration.test.ts +++ b/packages/core/test/database-migration.test.ts @@ -1,4 +1,6 @@ import { describe, expect, test } from "bun:test" +import { $ } from "bun" +import { fileURLToPath } from "url" import { SqliteClient } from "@effect/sql-sqlite-bun" import { EffectDrizzleSqlite } from "@opencode-ai/effect-drizzle-sqlite" import { Effect } from "effect" @@ -13,6 +15,12 @@ const run = (effect: Effect.Effect) => const makeDb = EffectDrizzleSqlite.makeWithDefaults() describe("DatabaseMigration", () => { + test("declared schema has no ungenerated migrations", async () => { + const result = await $`bun ${fileURLToPath(new URL("../script/migration.ts", import.meta.url))} --check`.quiet().nothrow() + expect(result.exitCode, result.stderr.toString()).toBe(0) + expect(result.stdout.toString()).toContain("No schema changes, nothing to migrate") + }, 30_000) + test("applies tracked migrations to an empty database", async () => { await run( Effect.gen(function* () {