database-migration.test.ts 4.5 KB

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