diff --git a/packages/core/migration/20260511173437_session-metadata/migration.sql b/packages/core/migration/20260511173437_session-metadata/migration.sql new file mode 100644 index 000000000..1f8fcaf64 --- /dev/null +++ b/packages/core/migration/20260511173437_session-metadata/migration.sql @@ -0,0 +1 @@ +ALTER TABLE `session` ADD `metadata` text; diff --git a/packages/core/migration/20260530232709_lovely_romulus/snapshot.json b/packages/core/migration/20260511173437_session-metadata/snapshot.json similarity index 100% rename from packages/core/migration/20260530232709_lovely_romulus/snapshot.json rename to packages/core/migration/20260511173437_session-metadata/snapshot.json diff --git a/packages/core/migration/20260530232709_lovely_romulus/migration.sql b/packages/core/migration/20260530232709_lovely_romulus/migration.sql deleted file mode 100644 index 0ce73631f..000000000 --- a/packages/core/migration/20260530232709_lovely_romulus/migration.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE `session` ADD `metadata` text; \ No newline at end of file diff --git a/packages/core/src/database/migration.gen.ts b/packages/core/src/database/migration.gen.ts index 1a6918b33..ee7631848 100644 --- a/packages/core/src/database/migration.gen.ts +++ b/packages/core/src/database/migration.gen.ts @@ -22,6 +22,6 @@ export const migrations = ( import("./migration/20260507164347_add_workspace_time"), import("./migration/20260510033149_session_usage"), import("./migration/20260511000411_data_migration_state"), - import("./migration/20260530232709_lovely_romulus"), + import("./migration/20260511173437_session-metadata"), ]) ).map((module) => module.default) satisfies DatabaseMigration.Migration[] diff --git a/packages/core/src/database/migration/20260511173437_session-metadata.ts b/packages/core/src/database/migration/20260511173437_session-metadata.ts new file mode 100644 index 000000000..413f08667 --- /dev/null +++ b/packages/core/src/database/migration/20260511173437_session-metadata.ts @@ -0,0 +1,16 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260511173437_session-metadata", + up(tx) { + return Effect.gen(function* () { + // This column briefly shipped again under 20260530232709_lovely_romulus. + if ( + (yield* tx.all<{ name: string }>(`PRAGMA table_info(\`session\`)`)).some((column) => column.name === "metadata") + ) + return + yield* tx.run(`ALTER TABLE \`session\` ADD \`metadata\` text;`) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260530232709_lovely_romulus.ts b/packages/core/src/database/migration/20260530232709_lovely_romulus.ts deleted file mode 100644 index 2fe023543..000000000 --- a/packages/core/src/database/migration/20260530232709_lovely_romulus.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Effect } from "effect" -import type { DatabaseMigration } from "../migration" - -export default { - id: "20260530232709_lovely_romulus", - up(tx) { - return Effect.gen(function* () { - yield* tx.run(`ALTER TABLE \`session\` ADD \`metadata\` text;`) - }) - }, -} satisfies DatabaseMigration.Migration diff --git a/packages/core/test/database-migration.test.ts b/packages/core/test/database-migration.test.ts index 5b0b08c96..316974de8 100644 --- a/packages/core/test/database-migration.test.ts +++ b/packages/core/test/database-migration.test.ts @@ -7,6 +7,7 @@ import { Effect } from "effect" import { sql } from "drizzle-orm" import { DatabaseMigration } from "@opencode-ai/core/database/migration" import sessionUsageMigration from "@opencode-ai/core/database/migration/20260510033149_session_usage" +import sessionMetadataMigration from "@opencode-ai/core/database/migration/20260511173437_session-metadata" import type { SqlClient as SqlClientService } from "effect/unstable/sql/SqlClient" const run = (effect: Effect.Effect) => @@ -89,6 +90,44 @@ describe("DatabaseMigration", () => { ) }) + test("does not replay a migrated session metadata column", async () => { + await run( + Effect.gen(function* () { + const db = yield* makeDb + yield* db.run(sql`CREATE TABLE session (id text PRIMARY KEY, metadata text)`) + 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(sql`SELECT id FROM migration`)).toEqual([{ id: "20260511173437_session-metadata" }]) + }), + ) + }) + + test("accepts the temporary replacement session metadata migration id", async () => { + await run( + Effect.gen(function* () { + const db = yield* makeDb + yield* db.run(sql`CREATE TABLE session (id text PRIMARY KEY, metadata text)`) + yield* db.run(sql`CREATE TABLE migration (id TEXT PRIMARY KEY, time_completed INTEGER NOT NULL)`) + yield* db.run(sql`INSERT INTO migration (id, time_completed) VALUES ('20260530232709_lovely_romulus', 1)`) + + yield* DatabaseMigration.applyOnly(db, [sessionMetadataMigration]) + + expect(yield* db.all(sql`SELECT id FROM migration ORDER BY id`)).toEqual([ + { id: "20260511173437_session-metadata" }, + { id: "20260530232709_lovely_romulus" }, + ]) + }), + ) + }) + test("skips drizzle import when migration table already has state", async () => { await run( Effect.gen(function* () {