migration.ts 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081
  1. export * as DatabaseMigration from "./migration"
  2. import { sql } from "drizzle-orm"
  3. import { Effect, Semaphore } from "effect"
  4. import type { EffectDrizzleSqlite } from "@opencode-ai/effect-drizzle-sqlite"
  5. import { migrations } from "./migration.gen"
  6. import schema from "./schema.gen"
  7. type Database = EffectDrizzleSqlite.EffectSQLiteDatabase
  8. type Transaction = Parameters<Parameters<Database["transaction"]>[0]>[0]
  9. const lock = Semaphore.makeUnsafe(1)
  10. export type Migration = {
  11. id: string
  12. up: (tx: Transaction) => Effect.Effect<void, unknown>
  13. }
  14. export function apply(db: Database) {
  15. return lock.withPermit(
  16. Effect.gen(function* () {
  17. const tables = yield* db.all<{ name: string }>(
  18. sql`SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'`,
  19. )
  20. if (tables.some((table) => table.name === "session")) return yield* applyOnly(db, migrations)
  21. if (tables.length > 0) return yield* Effect.die("Database is not empty and has no session table")
  22. yield* db.transaction((tx) =>
  23. Effect.gen(function* () {
  24. yield* schema.up(tx)
  25. yield* tx.run(
  26. sql`CREATE TABLE ${sql.identifier("migration")} (id TEXT PRIMARY KEY, time_completed INTEGER NOT NULL)`,
  27. )
  28. yield* Effect.forEach(migrations, (migration) =>
  29. tx.run(
  30. sql`INSERT INTO ${sql.identifier("migration")} (id, time_completed) VALUES (${migration.id}, ${Date.now()})`,
  31. ),
  32. )
  33. }),
  34. )
  35. }),
  36. )
  37. }
  38. export function applyOnly(db: Database, input: Migration[]) {
  39. return Effect.gen(function* () {
  40. yield* db.run(
  41. sql`CREATE TABLE IF NOT EXISTS ${sql.identifier("migration")} (id TEXT PRIMARY KEY, time_completed INTEGER NOT NULL)`,
  42. )
  43. let completed = new Set(
  44. (yield* db.all<{ id: string }>(sql`SELECT id FROM ${sql.identifier("migration")}`)).map((row) => row.id),
  45. )
  46. if (completed.size === 0) {
  47. // Existing installs used Drizzle's migration journal. Seed the new
  48. // journal once so TypeScript migrations don't replay old SQL.
  49. if (
  50. yield* db.get(sql`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ${"__drizzle_migrations"}`)
  51. ) {
  52. yield* db.run(sql`
  53. INSERT OR IGNORE INTO ${sql.identifier("migration")} (id, time_completed)
  54. SELECT name, ${Date.now()}
  55. FROM ${sql.identifier("__drizzle_migrations")}
  56. WHERE name IS NOT NULL
  57. `)
  58. completed = new Set(
  59. (yield* db.all<{ id: string }>(sql`SELECT id FROM ${sql.identifier("migration")}`)).map((row) => row.id),
  60. )
  61. }
  62. }
  63. for (const migration of input) {
  64. if (completed.has(migration.id)) continue
  65. yield* db.transaction((tx) =>
  66. Effect.gen(function* () {
  67. yield* migration.up(tx)
  68. yield* tx.run(
  69. sql`INSERT INTO ${sql.identifier("migration")} (id, time_completed) VALUES (${migration.id}, ${Date.now()})`,
  70. )
  71. }),
  72. )
  73. }
  74. })
  75. }