database-migration.test.ts 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
  1. import { describe, expect, test } from "bun:test"
  2. import { $ } from "bun"
  3. import { fileURLToPath } from "url"
  4. import { SqliteClient } from "@effect/sql-sqlite-bun"
  5. import { EffectDrizzleSqlite } from "@opencode-ai/effect-drizzle-sqlite"
  6. import { Effect } from "effect"
  7. import { sql } from "drizzle-orm"
  8. import { DatabaseMigration } from "@opencode-ai/core/database/migration"
  9. import sessionUsageMigration from "@opencode-ai/core/database/migration/20260510033149_session_usage"
  10. import type { SqlClient as SqlClientService } from "effect/unstable/sql/SqlClient"
  11. const run = <A, E>(effect: Effect.Effect<A, E, SqlClientService>) =>
  12. Effect.runPromise(effect.pipe(Effect.provide(SqliteClient.layer({ filename: ":memory:", disableWAL: true })), Effect.scoped))
  13. const makeDb = EffectDrizzleSqlite.makeWithDefaults()
  14. describe("DatabaseMigration", () => {
  15. if (process.platform === "linux") {
  16. test("declared schema has no ungenerated migrations", async () => {
  17. const result = await $`bun ${fileURLToPath(new URL("../script/migration.ts", import.meta.url))} --check`.quiet().nothrow()
  18. expect(result.exitCode, result.stderr.toString()).toBe(0)
  19. expect(result.stdout.toString()).toContain("No schema changes, nothing to migrate")
  20. }, 30_000)
  21. }
  22. test("applies tracked migrations to an empty database", async () => {
  23. await run(
  24. Effect.gen(function* () {
  25. const db = yield* makeDb
  26. yield* DatabaseMigration.apply(db)
  27. expect(yield* db.get(sql`SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'session'`)).toEqual({
  28. name: "session",
  29. })
  30. expect(yield* db.get(sql`SELECT count(*) as count FROM migration`)).toEqual({ count: 21 })
  31. }),
  32. )
  33. })
  34. test("runs session usage backfill in order with schema changes", async () => {
  35. await run(
  36. Effect.gen(function* () {
  37. const db = yield* makeDb
  38. yield* db.run(sql`CREATE TABLE session (id text PRIMARY KEY, time_updated integer NOT NULL)`)
  39. yield* db.run(sql`CREATE TABLE message (id text PRIMARY KEY, session_id text NOT NULL, data text NOT NULL)`)
  40. yield* db.run(sql`INSERT INTO session (id, time_updated) VALUES ('session_1', 1)`)
  41. yield* db.run(
  42. 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}}}')`,
  43. )
  44. yield* DatabaseMigration.applyOnly(db, [sessionUsageMigration])
  45. expect(
  46. yield* db.get(
  47. sql`SELECT cost, tokens_input, tokens_output, tokens_reasoning, tokens_cache_read, tokens_cache_write FROM session WHERE id = 'session_1'`,
  48. ),
  49. ).toEqual({
  50. cost: 1.25,
  51. tokens_input: 2,
  52. tokens_output: 3,
  53. tokens_reasoning: 4,
  54. tokens_cache_read: 5,
  55. tokens_cache_write: 6,
  56. })
  57. }),
  58. )
  59. })
  60. test("imports existing drizzle migration state", async () => {
  61. await run(
  62. Effect.gen(function* () {
  63. const db = yield* makeDb
  64. yield* db.run(sql`CREATE TABLE __drizzle_migrations (id INTEGER PRIMARY KEY, hash text NOT NULL, created_at numeric, name text, applied_at TEXT)`)
  65. yield* db.run(sql`
  66. INSERT INTO __drizzle_migrations (hash, created_at, name, applied_at)
  67. VALUES ('hash', 1, '20260127222353_familiar_lady_ursula', ${new Date().toISOString()})
  68. `)
  69. yield* DatabaseMigration.applyOnly(db, [])
  70. expect(yield* db.get(sql`SELECT id FROM migration`)).toEqual({ id: "20260127222353_familiar_lady_ursula" })
  71. }),
  72. )
  73. })
  74. test("skips drizzle import when migration table already has state", async () => {
  75. await run(
  76. Effect.gen(function* () {
  77. const db = yield* makeDb
  78. yield* db.run(sql`CREATE TABLE migration (id TEXT PRIMARY KEY, time_completed INTEGER NOT NULL)`)
  79. yield* db.run(sql`INSERT INTO migration (id, time_completed) VALUES ('existing', 1)`)
  80. yield* db.run(sql`CREATE TABLE __drizzle_migrations (id INTEGER PRIMARY KEY, hash text NOT NULL, created_at numeric, name text, applied_at TEXT)`)
  81. yield* db.run(sql`
  82. INSERT INTO __drizzle_migrations (hash, created_at, name, applied_at)
  83. VALUES ('hash', 1, '20260127222353_familiar_lady_ursula', ${new Date().toISOString()})
  84. `)
  85. yield* DatabaseMigration.applyOnly(db, [])
  86. expect(yield* db.all(sql`SELECT id FROM migration ORDER BY id`)).toEqual([{ id: "existing" }])
  87. }),
  88. )
  89. })
  90. })