fix(core): migrate sync database clients
This commit is contained in:
@@ -1,51 +1,80 @@
|
||||
export * as DatabaseMigration from "./migration"
|
||||
|
||||
import { sql } from "drizzle-orm"
|
||||
import { sql, type SQLWrapper } from "drizzle-orm"
|
||||
import { Effect } from "effect"
|
||||
import type { EffectDrizzleSqlite } from "@opencode-ai/effect-drizzle-sqlite"
|
||||
import { migrations } from "./migration.gen"
|
||||
|
||||
type Database = EffectDrizzleSqlite.EffectSQLiteDatabase
|
||||
type Transaction = Parameters<Parameters<Database["transaction"]>[0]>[0]
|
||||
type EffectDatabase = EffectDrizzleSqlite.EffectSQLiteDatabase
|
||||
type Query = string | SQLWrapper
|
||||
type MigrationEffect<A> = Effect.Effect<A, unknown, never>
|
||||
export type Transaction = {
|
||||
run: (query: Query) => MigrationEffect<unknown>
|
||||
}
|
||||
type SyncDatabase = {
|
||||
run: (query: Query) => unknown
|
||||
all: <A = unknown>(query: Query) => A[]
|
||||
get: <A = unknown>(query: Query) => A | undefined
|
||||
}
|
||||
type SyncTransaction = {
|
||||
run: (query: Query) => unknown
|
||||
}
|
||||
type Database = EffectDatabase | SyncDatabase
|
||||
type Target = {
|
||||
run: (query: Query) => MigrationEffect<unknown>
|
||||
all: <A = unknown>(query: Query) => MigrationEffect<A[]>
|
||||
get: <A = unknown>(query: Query) => MigrationEffect<A | undefined>
|
||||
transaction: <A>(body: (tx: Transaction) => MigrationEffect<A>) => MigrationEffect<A>
|
||||
}
|
||||
|
||||
export type Migration = {
|
||||
id: string
|
||||
up: (tx: Transaction) => Effect.Effect<void, unknown>
|
||||
up: (tx: Transaction) => MigrationEffect<void>
|
||||
}
|
||||
|
||||
export function apply(db: EffectDatabase): MigrationEffect<void>
|
||||
export function apply(db: SyncDatabase): MigrationEffect<void>
|
||||
export function apply(db: Database) {
|
||||
return applyOnly(db, migrations)
|
||||
return applyOnlyImpl(db, migrations)
|
||||
}
|
||||
|
||||
export function applyOnly(db: EffectDatabase, input: Migration[]): MigrationEffect<void>
|
||||
export function applyOnly(db: SyncDatabase, input: Migration[]): MigrationEffect<void>
|
||||
export function applyOnly(db: Database, input: Migration[]) {
|
||||
return applyOnlyImpl(db, input)
|
||||
}
|
||||
|
||||
function applyOnlyImpl(db: Database, input: Migration[]) {
|
||||
return Effect.gen(function* () {
|
||||
yield* db.run(
|
||||
const target = normalize(db)
|
||||
|
||||
yield* target.run(
|
||||
sql`CREATE TABLE IF NOT EXISTS ${sql.identifier("migration")} (id TEXT PRIMARY KEY, time_completed INTEGER NOT NULL)`,
|
||||
)
|
||||
let completed = new Set(
|
||||
(yield* db.all<{ id: string }>(sql`SELECT id FROM ${sql.identifier("migration")}`)).map((row) => row.id),
|
||||
(yield* target.all<{ id: string }>(sql`SELECT id FROM ${sql.identifier("migration")}`)).map((row) => row.id),
|
||||
)
|
||||
if (completed.size === 0) {
|
||||
// Existing installs used Drizzle's migration journal. Seed the new
|
||||
// journal once so TypeScript migrations don't replay old SQL.
|
||||
if (
|
||||
yield* db.get(sql`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ${"__drizzle_migrations"}`)
|
||||
yield* target.get(sql`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ${"__drizzle_migrations"}`)
|
||||
) {
|
||||
yield* db.run(sql`
|
||||
yield* target.run(sql`
|
||||
INSERT OR IGNORE INTO ${sql.identifier("migration")} (id, time_completed)
|
||||
SELECT name, ${Date.now()}
|
||||
FROM ${sql.identifier("__drizzle_migrations")}
|
||||
WHERE name IS NOT NULL
|
||||
`)
|
||||
completed = new Set(
|
||||
(yield* db.all<{ id: string }>(sql`SELECT id FROM ${sql.identifier("migration")}`)).map((row) => row.id),
|
||||
(yield* target.all<{ id: string }>(sql`SELECT id FROM ${sql.identifier("migration")}`)).map((row) => row.id),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
for (const migration of input) {
|
||||
if (completed.has(migration.id)) continue
|
||||
yield* db.transaction((tx) =>
|
||||
yield* target.transaction((tx) =>
|
||||
Effect.gen(function* () {
|
||||
yield* migration.up(tx)
|
||||
yield* tx.run(
|
||||
@@ -56,3 +85,49 @@ export function applyOnly(db: Database, input: Migration[]) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function normalize(db: Database): Target {
|
||||
if (isEffectDatabase(db)) return normalizeEffect(db)
|
||||
return normalizeSync(db)
|
||||
}
|
||||
|
||||
function normalizeEffect(db: EffectDatabase): Target {
|
||||
return {
|
||||
run: (query) => db.run(query).pipe(Effect.as(undefined)),
|
||||
all: (query) => db.all(query),
|
||||
get: (query) => db.get(query),
|
||||
transaction: (body) => db.transaction((tx) => body(normalizeEffectTransaction(tx))),
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeSync(db: SyncDatabase): Target {
|
||||
const tx = normalizeSyncTransaction(db)
|
||||
return {
|
||||
run: tx.run,
|
||||
all: (query) => Effect.try({ try: () => db.all(query), catch: (err) => err }),
|
||||
get: (query) => Effect.try({ try: () => db.get(query), catch: (err) => err }),
|
||||
transaction: (body) =>
|
||||
Effect.gen(function* () {
|
||||
yield* tx.run("BEGIN")
|
||||
const result = yield* body(tx).pipe(Effect.catch((err) => tx.run("ROLLBACK").pipe(Effect.flatMap(() => Effect.fail(err)))))
|
||||
yield* tx.run("COMMIT")
|
||||
return result
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeEffectTransaction(tx: { run: (query: Query) => MigrationEffect<unknown> }): Transaction {
|
||||
return {
|
||||
run: (query) => tx.run(query),
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeSyncTransaction(tx: SyncTransaction): Transaction {
|
||||
return {
|
||||
run: (query) => Effect.try({ try: () => tx.run(query), catch: (err) => err }),
|
||||
}
|
||||
}
|
||||
|
||||
function isEffectDatabase(db: Database): db is EffectDatabase {
|
||||
return "raw" in db
|
||||
}
|
||||
|
||||
@@ -5,8 +5,9 @@ import * as Log from "@opencode-ai/core/util/log"
|
||||
import { NamedError } from "@opencode-ai/core/util/error"
|
||||
import { EffectBridge } from "@/effect/bridge"
|
||||
import { init } from "#db"
|
||||
import { Schema } from "effect"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { DatabaseMigration } from "@opencode-ai/core/database/migration"
|
||||
|
||||
export const NotFoundError = NamedError.create("NotFoundError", {
|
||||
message: Schema.String,
|
||||
@@ -30,8 +31,6 @@ export const Client = Object.assign(
|
||||
const dbPath = getPath()
|
||||
log.info("opening database", { path: dbPath })
|
||||
|
||||
Database.init()
|
||||
|
||||
const db = init(dbPath)
|
||||
|
||||
db.run("PRAGMA journal_mode = WAL")
|
||||
@@ -40,6 +39,7 @@ export const Client = Object.assign(
|
||||
db.run("PRAGMA cache_size = -64000")
|
||||
db.run("PRAGMA foreign_keys = ON")
|
||||
db.run("PRAGMA wal_checkpoint(PASSIVE)")
|
||||
Effect.runSync(DatabaseMigration.apply(db))
|
||||
|
||||
client = db
|
||||
loaded = true
|
||||
@@ -56,7 +56,7 @@ export const Client = Object.assign(
|
||||
|
||||
export function close() {
|
||||
if (!Client.loaded()) return
|
||||
Client().$client.close()
|
||||
client?.$client.close()
|
||||
Client.reset()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user