Skip to content

Commit b72b500

Browse files
fix(core): recover legacy database migration history (anomalyco#45061)
Co-authored-by: neriousy <34747899+neriousy@users.noreply.github.com>
1 parent ac1c048 commit b72b500

3 files changed

Lines changed: 100 additions & 7 deletions

File tree

‎packages/core/src/database/migration.ts‎

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,39 @@ export function applyOnly(db: Database, input: Migration[]) {
5454
if (
5555
yield* db.get(sql`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ${"__drizzle_migrations"}`)
5656
) {
57-
yield* db.run(sql`
58-
INSERT OR IGNORE INTO ${sql.identifier("migration")} (id, time_completed)
59-
SELECT name, ${Date.now()}
60-
FROM ${sql.identifier("__drizzle_migrations")}
61-
WHERE name IS NOT NULL
62-
`)
57+
const named = (yield* db.all<{ name: string }>(
58+
sql`SELECT name FROM pragma_table_info('__drizzle_migrations')`,
59+
)).some((column) => column.name === "name")
60+
61+
if (named) {
62+
yield* db.run(sql`
63+
INSERT OR IGNORE INTO ${sql.identifier("migration")} (id, time_completed)
64+
SELECT name, ${Date.now()}
65+
FROM ${sql.identifier("__drizzle_migrations")}
66+
WHERE name IS NOT NULL
67+
`)
68+
}
69+
70+
if (!named) {
71+
const entries = yield* db.all<{ created_at: number; prefix: string | null }>(sql`
72+
SELECT created_at, strftime('%Y%m%d%H%M%S', created_at / 1000, 'unixepoch') AS prefix
73+
FROM ${sql.identifier("__drizzle_migrations")}
74+
WHERE created_at IS NOT NULL
75+
`)
76+
77+
for (const entry of entries) {
78+
const migration = input.find((item) => item.id.startsWith(`${entry.prefix}_`))
79+
if (!migration) {
80+
return yield* Effect.die(
81+
new Error(`Legacy migration timestamp ${entry.created_at} does not match any known migration`),
82+
)
83+
}
84+
yield* db.run(sql`
85+
INSERT OR IGNORE INTO ${sql.identifier("migration")} (id, time_completed)
86+
VALUES (${migration.id}, ${Date.now()})
87+
`)
88+
}
89+
}
6390
completed = new Set(
6491
(yield* db.all<{ id: string }>(sql`SELECT id FROM ${sql.identifier("migration")}`)).map((row) => row.id),
6592
)

‎packages/core/src/database/migration/20260410174513_workspace-name.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ export default {
55
id: "20260410174513_workspace-name",
66
up(tx) {
77
return Effect.gen(function* () {
8+
const columns = yield* tx.all<{ name: string }>(`PRAGMA table_info(\`workspace\`)`)
9+
const name = columns.some((column) => column.name === "name") ? "`name`" : "''"
10+
811
yield* tx.run(`PRAGMA foreign_keys=OFF;`)
912
yield* tx.run(`
1013
CREATE TABLE \`__new_workspace\` (
@@ -19,7 +22,7 @@ export default {
1922
);
2023
`)
2124
yield* tx.run(
22-
`INSERT INTO \`__new_workspace\`(\`id\`, \`type\`, \`branch\`, \`name\`, \`directory\`, \`extra\`, \`project_id\`) SELECT \`id\`, \`type\`, \`branch\`, \`name\`, \`directory\`, \`extra\`, \`project_id\` FROM \`workspace\`;`,
25+
`INSERT INTO \`__new_workspace\`(\`id\`, \`type\`, \`branch\`, \`name\`, \`directory\`, \`extra\`, \`project_id\`) SELECT \`id\`, \`type\`, \`branch\`, ${name}, \`directory\`, \`extra\`, \`project_id\` FROM \`workspace\`;`,
2326
)
2427
yield* tx.run(`DROP TABLE \`workspace\`;`)
2528
yield* tx.run(`ALTER TABLE \`__new_workspace\` RENAME TO \`workspace\`;`)

‎packages/core/test/database-migration.test.ts‎

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { Effect, Layer } from "effect"
88
import { eq, inArray, sql } from "drizzle-orm"
99
import { DatabaseMigration } from "@opencode-ai/core/database/migration"
1010
import { migrations } from "@opencode-ai/core/database/migration.gen"
11+
import workspaceNameMigration from "@opencode-ai/core/database/migration/20260410174513_workspace-name"
1112
import sessionUsageMigration from "@opencode-ai/core/database/migration/20260510033149_session_usage"
1213
import normalizeStoragePathsMigration from "@opencode-ai/core/database/migration/20260601010001_normalize_storage_paths"
1314
import sessionMessageProjectionOrderMigration from "@opencode-ai/core/database/migration/20260603040000_session_message_projection_order"
@@ -38,6 +39,68 @@ const run = <A, E>(effect: Effect.Effect<A, E, SqlClientService>) =>
3839
const makeDb = EffectDrizzleSqlite.makeWithDefaults()
3940

4041
describe("DatabaseMigration", () => {
42+
test("defaults missing workspace names while preserving legacy workspace data", async () => {
43+
await run(
44+
Effect.gen(function* () {
45+
const db = yield* makeDb
46+
yield* db.run(sql`
47+
CREATE TABLE workspace (
48+
id text PRIMARY KEY,
49+
type text NOT NULL,
50+
branch text,
51+
directory text,
52+
extra text,
53+
project_id text NOT NULL
54+
)
55+
`)
56+
yield* db.run(sql`
57+
INSERT INTO workspace (id, type, branch, directory, extra, project_id)
58+
VALUES ('wrk_legacy', 'remote', 'main', '/repo', '{}', 'proj_legacy')
59+
`)
60+
61+
yield* DatabaseMigration.applyOnly(db, [workspaceNameMigration])
62+
63+
expect(yield* db.get(sql`SELECT id, name, branch, directory, extra FROM workspace`)).toEqual({
64+
id: "wrk_legacy",
65+
name: "",
66+
branch: "main",
67+
directory: "/repo",
68+
extra: "{}",
69+
})
70+
}),
71+
)
72+
})
73+
74+
test("imports unnamed legacy Drizzle journal entries by their actual migration timestamps", async () => {
75+
await run(
76+
Effect.gen(function* () {
77+
const db = yield* makeDb
78+
yield* db.run(sql`CREATE TABLE __drizzle_migrations (id integer PRIMARY KEY, hash text, created_at integer)`)
79+
yield* db.run(sql`
80+
INSERT INTO __drizzle_migrations (hash, created_at)
81+
VALUES ('', ${Date.UTC(2026, 3, 10, 17, 45, 13)})
82+
`)
83+
84+
yield* DatabaseMigration.applyOnly(db, [workspaceNameMigration])
85+
86+
expect(yield* db.all(sql`SELECT id FROM migration`)).toEqual([{ id: "20260410174513_workspace-name" }])
87+
}),
88+
)
89+
})
90+
91+
test("rejects unknown legacy Drizzle journal timestamps instead of guessing completed migrations", async () => {
92+
await expect(
93+
run(
94+
Effect.gen(function* () {
95+
const db = yield* makeDb
96+
yield* db.run(sql`CREATE TABLE __drizzle_migrations (id integer PRIMARY KEY, hash text, created_at integer)`)
97+
yield* db.run(sql`INSERT INTO __drizzle_migrations (hash, created_at) VALUES ('', 1234567890000)`)
98+
yield* DatabaseMigration.applyOnly(db, [workspaceNameMigration])
99+
}),
100+
),
101+
).rejects.toThrow("does not match any known migration")
102+
})
103+
41104
test("serializes concurrent embedded initialization for one database path", async () => {
42105
await using tmp = await tmpdir()
43106
const filename = path.join(tmp.path, "embedded.sqlite")

0 commit comments

Comments
 (0)