Compare commits

...

1 Commits

Author SHA1 Message Date
Kit Langton ee92348be7 fix(core): repair poisoned session metadata migration 2026-06-05 10:00:48 -04:00
2 changed files with 98 additions and 1 deletions
+16 -1
View File
@@ -23,6 +23,7 @@ export function applyOnly(db: Database, input: Migration[]) {
yield* db.run(
sql`CREATE TABLE IF NOT EXISTS ${sql.identifier("migration")} (id TEXT PRIMARY KEY, time_completed INTEGER NOT NULL)`,
)
const skipMigrations = !!process.env.OPENCODE_SKIP_MIGRATIONS
let completed = new Set(
(yield* db.all<{ id: string }>(sql`SELECT id FROM ${sql.identifier("migration")}`)).map((row) => row.id),
)
@@ -43,12 +44,14 @@ export function applyOnly(db: Database, input: Migration[]) {
)
}
}
if (!skipMigrations) yield* repairKnownSchemaDrift(db, input, completed)
for (const migration of input) {
if (completed.has(migration.id)) continue
if (skipMigrations) continue
yield* db.transaction((tx) =>
Effect.gen(function* () {
if (!process.env.OPENCODE_SKIP_MIGRATIONS) yield* migration.up(tx)
yield* migration.up(tx)
yield* tx.run(
sql`INSERT INTO ${sql.identifier("migration")} (id, time_completed) VALUES (${migration.id}, ${Date.now()})`,
)
@@ -57,3 +60,15 @@ export function applyOnly(db: Database, input: Migration[]) {
}
})
}
function repairKnownSchemaDrift(db: Database, input: Migration[], completed: Set<string>) {
return Effect.gen(function* () {
const sessionMetadata = input.find((migration) => migration.id === "20260511173437_session-metadata")
if (!sessionMetadata) return
if (!completed.has(sessionMetadata.id)) return
if (!(yield* db.get(sql`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ${"session"}`))) return
// Older Drizzle skip-migration runs could record this id without applying its SQL.
yield* db.transaction((tx) => sessionMetadata.up(tx))
})
}
@@ -426,6 +426,88 @@ describe("DatabaseMigration", () => {
)
})
test("runs session metadata migration after importing older drizzle state", async () => {
await run(
Effect.gen(function* () {
const db = yield* makeDb
yield* db.run(sql`CREATE TABLE session (id text PRIMARY KEY)`)
yield* db.run(
sql`CREATE TABLE __drizzle_migrations (id INTEGER PRIMARY KEY, hash text NOT NULL, created_at numeric, name text, applied_at TEXT)`,
)
yield* db.run(sql`
INSERT INTO __drizzle_migrations (hash, created_at, name, applied_at)
VALUES ('hash', 1, '20260511000411_data_migration_state', ${new Date().toISOString()})
`)
yield* DatabaseMigration.applyOnly(db, [sessionMetadataMigration])
expect(
(yield* db.all<{ name: string }>(sql`PRAGMA table_info(session)`)).map((column) => column.name),
).toContain("metadata")
expect(yield* db.all(sql`SELECT id FROM migration ORDER BY id`)).toEqual([
{ id: "20260511000411_data_migration_state" },
{ id: "20260511173437_session-metadata" },
])
}),
)
})
test("repairs imported session metadata migration state when the column is missing", async () => {
await run(
Effect.gen(function* () {
const db = yield* makeDb
yield* db.run(sql`CREATE TABLE session (id text PRIMARY KEY)`)
yield* db.run(
sql`CREATE TABLE __drizzle_migrations (id INTEGER PRIMARY KEY, hash text NOT NULL, created_at numeric, name text, applied_at TEXT)`,
)
yield* db.run(sql`
INSERT INTO __drizzle_migrations (hash, created_at, name, applied_at)
VALUES ('hash', 1, '20260511173437_session-metadata', ${new Date().toISOString()})
`)
yield* DatabaseMigration.applyOnly(db, [sessionMetadataMigration])
expect(
(yield* db.all<{ name: string }>(sql`PRAGMA table_info(session)`)).map((column) => column.name),
).toContain("metadata")
expect(yield* db.all(sql`SELECT id FROM migration ORDER BY id`)).toEqual([
{ id: "20260511173437_session-metadata" },
])
}),
)
})
test("does not record migrations as complete when migrations are skipped", async () => {
const previous = process.env.OPENCODE_SKIP_MIGRATIONS
try {
await run(
Effect.gen(function* () {
const db = yield* makeDb
yield* db.run(sql`CREATE TABLE session (id text PRIMARY KEY)`)
process.env.OPENCODE_SKIP_MIGRATIONS = "true"
yield* DatabaseMigration.applyOnly(db, [sessionMetadataMigration])
expect(yield* db.all(sql`SELECT id FROM migration`)).toEqual([])
expect(
(yield* db.all<{ name: string }>(sql`PRAGMA table_info(session)`)).map((column) => column.name),
).not.toContain("metadata")
delete process.env.OPENCODE_SKIP_MIGRATIONS
yield* DatabaseMigration.applyOnly(db, [sessionMetadataMigration])
expect(yield* db.all(sql`SELECT id FROM migration`)).toEqual([{ id: "20260511173437_session-metadata" }])
expect(
(yield* db.all<{ name: string }>(sql`PRAGMA table_info(session)`)).map((column) => column.name),
).toContain("metadata")
}),
)
} finally {
if (previous === undefined) delete process.env.OPENCODE_SKIP_MIGRATIONS
else process.env.OPENCODE_SKIP_MIGRATIONS = previous
}
})
test("does not replay a migrated session metadata column", async () => {
await run(
Effect.gen(function* () {