152 lines
6.2 KiB
TypeScript
152 lines
6.2 KiB
TypeScript
import { describe, expect, test } from "bun:test"
|
|
import { $ } from "bun"
|
|
import { fileURLToPath } from "url"
|
|
import { SqliteClient } from "@effect/sql-sqlite-bun"
|
|
import { EffectDrizzleSqlite } from "@opencode-ai/effect-drizzle-sqlite"
|
|
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 = <A, E>(effect: Effect.Effect<A, E, SqlClientService>) =>
|
|
Effect.runPromise(
|
|
effect.pipe(Effect.provide(SqliteClient.layer({ filename: ":memory:", disableWAL: true })), Effect.scoped),
|
|
)
|
|
|
|
const makeDb = EffectDrizzleSqlite.makeWithDefaults()
|
|
|
|
describe("DatabaseMigration", () => {
|
|
if (process.platform === "linux") {
|
|
test("declared schema has no ungenerated migrations", async () => {
|
|
const result = await $`bun ${fileURLToPath(new URL("../script/migration.ts", import.meta.url))} --check`
|
|
.quiet()
|
|
.nothrow()
|
|
expect(result.exitCode, result.stderr.toString()).toBe(0)
|
|
expect(result.stdout.toString()).toContain("No schema changes, nothing to migrate")
|
|
}, 30_000)
|
|
}
|
|
|
|
test("applies tracked migrations to an empty database", async () => {
|
|
await run(
|
|
Effect.gen(function* () {
|
|
const db = yield* makeDb
|
|
yield* DatabaseMigration.apply(db)
|
|
|
|
expect(yield* db.get(sql`SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'session'`)).toEqual({
|
|
name: "session",
|
|
})
|
|
expect(yield* db.get(sql`SELECT count(*) as count FROM migration`)).toEqual({ count: 21 })
|
|
}),
|
|
)
|
|
})
|
|
|
|
test("runs session usage backfill in order with schema changes", async () => {
|
|
await run(
|
|
Effect.gen(function* () {
|
|
const db = yield* makeDb
|
|
yield* db.run(sql`CREATE TABLE session (id text PRIMARY KEY, time_updated integer NOT NULL)`)
|
|
yield* db.run(sql`CREATE TABLE message (id text PRIMARY KEY, session_id text NOT NULL, data text NOT NULL)`)
|
|
yield* db.run(sql`INSERT INTO session (id, time_updated) VALUES ('session_1', 1)`)
|
|
yield* db.run(
|
|
sql`INSERT INTO message (id, session_id, data) VALUES ('message_1', 'session_1', '{"role":"assistant","cost":1.25,"tokens":{"input":2,"output":3,"reasoning":4,"cache":{"read":5,"write":6}}}')`,
|
|
)
|
|
|
|
yield* DatabaseMigration.applyOnly(db, [sessionUsageMigration])
|
|
|
|
expect(
|
|
yield* db.get(
|
|
sql`SELECT cost, tokens_input, tokens_output, tokens_reasoning, tokens_cache_read, tokens_cache_write FROM session WHERE id = 'session_1'`,
|
|
),
|
|
).toEqual({
|
|
cost: 1.25,
|
|
tokens_input: 2,
|
|
tokens_output: 3,
|
|
tokens_reasoning: 4,
|
|
tokens_cache_read: 5,
|
|
tokens_cache_write: 6,
|
|
})
|
|
}),
|
|
)
|
|
})
|
|
|
|
test("imports existing drizzle migration state", async () => {
|
|
await run(
|
|
Effect.gen(function* () {
|
|
const db = yield* makeDb
|
|
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, '20260127222353_familiar_lady_ursula', ${new Date().toISOString()})
|
|
`)
|
|
|
|
yield* DatabaseMigration.applyOnly(db, [])
|
|
|
|
expect(yield* db.get(sql`SELECT id FROM migration`)).toEqual({ id: "20260127222353_familiar_lady_ursula" })
|
|
}),
|
|
)
|
|
})
|
|
|
|
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* () {
|
|
const db = yield* makeDb
|
|
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 ('existing', 1)`)
|
|
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, '20260127222353_familiar_lady_ursula', ${new Date().toISOString()})
|
|
`)
|
|
|
|
yield* DatabaseMigration.applyOnly(db, [])
|
|
|
|
expect(yield* db.all(sql`SELECT id FROM migration ORDER BY id`)).toEqual([{ id: "existing" }])
|
|
}),
|
|
)
|
|
})
|
|
})
|