fix(core): recover legacy database migration history (#45062)

Co-authored-by: neriousy <34747899+neriousy@users.noreply.github.com>
This commit is contained in:
opencode-agent[bot]
2026-08-25 22:02:26 +02:00
committed by GitHub
parent 263a442a6c
commit d562f6df1e
3 changed files with 100 additions and 7 deletions
+33 -6
View File
@@ -66,12 +66,39 @@ export function applyOnly(db: Database, input: Migration[]) {
if (
yield* db.get(sql`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ${"__drizzle_migrations"}`)
) {
yield* db.run(sql`
INSERT OR IGNORE INTO ${sql.identifier("migration")} (id, time_completed)
SELECT name, ${Date.now()}
FROM ${sql.identifier("__drizzle_migrations")}
WHERE name IS NOT NULL
`)
const named = (yield* db.all<{ name: string }>(
sql`SELECT name FROM pragma_table_info('__drizzle_migrations')`,
)).some((column) => column.name === "name")
if (named) {
yield* db.run(sql`
INSERT OR IGNORE INTO ${sql.identifier("migration")} (id, time_completed)
SELECT name, ${Date.now()}
FROM ${sql.identifier("__drizzle_migrations")}
WHERE name IS NOT NULL
`)
}
if (!named) {
const entries = yield* db.all<{ created_at: number; prefix: string | null }>(sql`
SELECT created_at, strftime('%Y%m%d%H%M%S', created_at / 1000, 'unixepoch') AS prefix
FROM ${sql.identifier("__drizzle_migrations")}
WHERE created_at IS NOT NULL
`)
for (const entry of entries) {
const migration = input.find((item) => item.id.startsWith(`${entry.prefix}_`))
if (!migration) {
return yield* Effect.die(
new Error(`Legacy migration timestamp ${entry.created_at} does not match any known migration`),
)
}
yield* db.run(sql`
INSERT OR IGNORE INTO ${sql.identifier("migration")} (id, time_completed)
VALUES (${migration.id}, ${Date.now()})
`)
}
}
completed = new Set(
(yield* db.all<{ id: string }>(sql`SELECT id FROM ${sql.identifier("migration")}`)).map((row) => row.id),
)
@@ -5,6 +5,9 @@ const migration: DatabaseMigration.Migration = {
id: "20260410174513_workspace-name",
up(tx) {
return Effect.gen(function* () {
const columns = yield* tx.all<{ name: string }>(`PRAGMA table_info(\`workspace\`)`)
const name = columns.some((column) => column.name === "name") ? "`name`" : "''"
yield* tx.run(`PRAGMA foreign_keys=OFF;`)
yield* tx.run(`
CREATE TABLE \`__new_workspace\` (
@@ -19,7 +22,7 @@ const migration: DatabaseMigration.Migration = {
);
`)
yield* tx.run(
`INSERT INTO \`__new_workspace\`(\`id\`, \`type\`, \`branch\`, \`name\`, \`directory\`, \`extra\`, \`project_id\`) SELECT \`id\`, \`type\`, \`branch\`, \`name\`, \`directory\`, \`extra\`, \`project_id\` FROM \`workspace\`;`,
`INSERT INTO \`__new_workspace\`(\`id\`, \`type\`, \`branch\`, \`name\`, \`directory\`, \`extra\`, \`project_id\`) SELECT \`id\`, \`type\`, \`branch\`, ${name}, \`directory\`, \`extra\`, \`project_id\` FROM \`workspace\`;`,
)
yield* tx.run(`DROP TABLE \`workspace\`;`)
yield* tx.run(`ALTER TABLE \`__new_workspace\` RENAME TO \`workspace\`;`)
@@ -8,6 +8,7 @@ import { Effect, Layer } from "effect"
import { sql } from "drizzle-orm"
import { DatabaseMigration } from "@opencode-ai/core/database/migration"
import { migrations } from "@opencode-ai/core/database/migration.gen"
import workspaceNameMigration from "@opencode-ai/core/database/migration/20260410174513_workspace-name"
import { Database } from "@opencode-ai/core/database/database"
import { tmpdir } from "./fixture/tmpdir"
import type { SqlClient } from "effect/unstable/sql/SqlClient"
@@ -35,6 +36,68 @@ const run = <A, E>(
const makeDb = EffectDrizzleSqlite.makeWithDefaults()
describe("DatabaseMigration", () => {
test("defaults missing workspace names while preserving legacy workspace data", async () => {
await run(
Effect.gen(function* () {
const db = yield* makeDb
yield* db.run(sql`
CREATE TABLE workspace (
id text PRIMARY KEY,
type text NOT NULL,
branch text,
directory text,
extra text,
project_id text NOT NULL
)
`)
yield* db.run(sql`
INSERT INTO workspace (id, type, branch, directory, extra, project_id)
VALUES ('wrk_legacy', 'remote', 'main', '/repo', '{}', 'proj_legacy')
`)
yield* DatabaseMigration.applyOnly(db, [workspaceNameMigration])
expect(yield* db.get(sql`SELECT id, name, branch, directory, extra FROM workspace`)).toEqual({
id: "wrk_legacy",
name: "",
branch: "main",
directory: "/repo",
extra: "{}",
})
}),
)
})
test("imports unnamed legacy Drizzle journal entries by their actual migration timestamps", async () => {
await run(
Effect.gen(function* () {
const db = yield* makeDb
yield* db.run(sql`CREATE TABLE __drizzle_migrations (id integer PRIMARY KEY, hash text, created_at integer)`)
yield* db.run(sql`
INSERT INTO __drizzle_migrations (hash, created_at)
VALUES ('', ${Date.UTC(2026, 3, 10, 17, 45, 13)})
`)
yield* DatabaseMigration.applyOnly(db, [workspaceNameMigration])
expect(yield* db.all(sql`SELECT id FROM migration`)).toEqual([{ id: "20260410174513_workspace-name" }])
}),
)
})
test("rejects unknown legacy Drizzle journal timestamps instead of guessing completed migrations", async () => {
await expect(
run(
Effect.gen(function* () {
const db = yield* makeDb
yield* db.run(sql`CREATE TABLE __drizzle_migrations (id integer PRIMARY KEY, hash text, created_at integer)`)
yield* db.run(sql`INSERT INTO __drizzle_migrations (hash, created_at) VALUES ('', 1234567890000)`)
yield* DatabaseMigration.applyOnly(db, [workspaceNameMigration])
}),
),
).rejects.toThrow("does not match any known migration")
})
test("serializes concurrent embedded initialization for one database path", async () => {
await using tmp = await tmpdir()
const filename = path.join(tmp.path, "embedded.sqlite")