mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-27 03:51:21 -04:00
fix(core): recover legacy database migration history (#45062)
Co-authored-by: neriousy <34747899+neriousy@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
263a442a6c
commit
d562f6df1e
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user