mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-17 12:56:41 +02:00
refactor(v2): keep test database setup explicit
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import { type SQLiteBunDatabase } from "drizzle-orm/bun-sqlite"
|
||||
import { migrate } from "drizzle-orm/bun-sqlite/migrator"
|
||||
import type { MigrationsJournal } from "drizzle-orm/migrator"
|
||||
import { type SQLiteTransaction } from "drizzle-orm/sqlite-core"
|
||||
export * from "drizzle-orm"
|
||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
@@ -48,10 +47,13 @@ export type Transaction = SQLiteTransaction<"sync", void>
|
||||
|
||||
type Client = ReturnType<typeof init>
|
||||
|
||||
type Journal = MigrationsJournal
|
||||
type Journal = { sql: string; timestamp: number; name: string }[]
|
||||
|
||||
// Drizzle's migrate overloads trigger expensive variance checks here; narrow to the journal overload we actually use.
|
||||
const migrateFromJournal = migrate as unknown as (db: SQLiteBunDatabase, entries: Journal) => void
|
||||
|
||||
function applyMigrations(db: SQLiteBunDatabase, entries: Journal) {
|
||||
migrate(db, entries)
|
||||
migrateFromJournal(db, entries)
|
||||
}
|
||||
|
||||
function time(tag: string) {
|
||||
@@ -72,17 +74,17 @@ function migrations(dir: string): Journal {
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.map((entry) => entry.name)
|
||||
|
||||
const sql: Journal = dirs
|
||||
const sql = dirs
|
||||
.map((name) => {
|
||||
const file = path.join(dir, name, "migration.sql")
|
||||
if (!existsSync(file)) return undefined
|
||||
if (!existsSync(file)) return
|
||||
return {
|
||||
sql: readFileSync(file, "utf-8"),
|
||||
timestamp: time(name),
|
||||
name,
|
||||
}
|
||||
})
|
||||
.filter((entry) => entry !== undefined)
|
||||
.filter(Boolean) as Journal
|
||||
|
||||
return sql.sort((a, b) => a.timestamp - b.timestamp)
|
||||
}
|
||||
@@ -92,7 +94,7 @@ let loaded = false
|
||||
|
||||
export const Client = Object.assign(
|
||||
(flags: DatabaseFlags = readRuntimeFlags()): Client => {
|
||||
if (loaded && client) return client
|
||||
if (loaded) return client as Client
|
||||
|
||||
const dbPath = getPath(flags)
|
||||
log.info("opening database", { path: dbPath })
|
||||
@@ -157,19 +159,19 @@ export function use<T>(callback: (trx: TxOrDb) => T): T {
|
||||
if (err instanceof LocalContext.NotFound) {
|
||||
const effects: (() => void | Promise<void>)[] = []
|
||||
const result = ctx.provide({ effects, tx: Client() }, () => callback(Client()))
|
||||
for (const effect of effects) void effect()
|
||||
for (const effect of effects) effect()
|
||||
return result
|
||||
}
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
export function effect(fn: () => void | Promise<void>) {
|
||||
export function effect(fn: () => any | Promise<any>) {
|
||||
const bound = EffectBridge.bind(fn)
|
||||
try {
|
||||
ctx.use().effects.push(bound)
|
||||
} catch {
|
||||
void bound()
|
||||
bound()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,9 +190,7 @@ export function transaction<T>(
|
||||
const effects: (() => void | Promise<void>)[] = []
|
||||
const txCallback = EffectBridge.bind((tx: TxOrDb) => ctx.provide({ tx, effects }, () => callback(tx)))
|
||||
const result = Client().transaction(txCallback, { behavior: options?.behavior })
|
||||
for (const effect of effects) void effect()
|
||||
// Drizzle's transaction type does not preserve our NotPromise<T> constraint through the callback wrapper.
|
||||
// oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion
|
||||
for (const effect of effects) effect()
|
||||
return result as NotPromise<T>
|
||||
}
|
||||
throw err
|
||||
|
||||
@@ -2,35 +2,31 @@ import { Database as LegacyDatabase } from "@/storage/db"
|
||||
import { SqliteClient } from "@effect/sql-sqlite-bun"
|
||||
import { EffectDrizzleSqlite } from "@opencode-ai/effect-drizzle-sqlite"
|
||||
import { Context, Effect, Layer } from "effect"
|
||||
import path from "path"
|
||||
|
||||
const makeDatabase = EffectDrizzleSqlite.makeWithDefaults()
|
||||
type DatabaseShape = Effect.Success<typeof makeDatabase>
|
||||
|
||||
export class Service extends Context.Service<Service, DatabaseShape>()("@opencode/v2/storage/Database") {}
|
||||
|
||||
export const layerForPath = (filename: string) =>
|
||||
Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const db = yield* makeDatabase
|
||||
yield* db.run("PRAGMA journal_mode = WAL")
|
||||
yield* db.run("PRAGMA synchronous = NORMAL")
|
||||
yield* db.run("PRAGMA busy_timeout = 5000")
|
||||
yield* db.run("PRAGMA cache_size = -64000")
|
||||
yield* db.run("PRAGMA foreign_keys = ON")
|
||||
yield* db.run("PRAGMA wal_checkpoint(PASSIVE)")
|
||||
return db
|
||||
}),
|
||||
).pipe(Layer.provide(SqliteClient.layer({ filename })))
|
||||
|
||||
export const layer = Layer.unwrap(
|
||||
Effect.sync(() => {
|
||||
const filename = LegacyDatabase.getPath()
|
||||
return Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
LegacyDatabase.Client()
|
||||
const db = yield* makeDatabase
|
||||
yield* db.run("PRAGMA journal_mode = WAL")
|
||||
yield* db.run("PRAGMA synchronous = NORMAL")
|
||||
yield* db.run("PRAGMA busy_timeout = 5000")
|
||||
yield* db.run("PRAGMA cache_size = -64000")
|
||||
yield* db.run("PRAGMA foreign_keys = ON")
|
||||
yield* db.run("PRAGMA wal_checkpoint(PASSIVE)")
|
||||
if (filename === ":memory:") {
|
||||
yield* EffectDrizzleSqlite.migrate(db, {
|
||||
migrationsFolder: path.join(import.meta.dirname, "../../../migration"),
|
||||
})
|
||||
}
|
||||
return db
|
||||
}),
|
||||
).pipe(Layer.provide(SqliteClient.layer({ filename, disableWAL: filename === ":memory:" })))
|
||||
LegacyDatabase.Client()
|
||||
return layerForPath(LegacyDatabase.getPath())
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -9,8 +9,12 @@ import { SessionStorageMemory } from "@/v2/storage/session-memory"
|
||||
import { SessionStorageSql } from "@/v2/storage/session-sql"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
import { SessionMessage } from "@opencode-ai/core/session-message"
|
||||
import { EffectDrizzleSqlite } from "@opencode-ai/effect-drizzle-sqlite"
|
||||
import { eq, or } from "@/storage/db"
|
||||
import { DateTime, Effect, Layer, Schema } from "effect"
|
||||
import fs from "fs/promises"
|
||||
import os from "os"
|
||||
import path from "path"
|
||||
import { testEffect } from "../lib/effect"
|
||||
|
||||
const projectID = ProjectID.make("project-session-storage")
|
||||
@@ -157,7 +161,24 @@ function sessionStorageContract<R, E>(name: string, layer: Layer.Layer<SessionSt
|
||||
)
|
||||
}
|
||||
|
||||
const sqlLayer = SessionStorageSql.layer.pipe(Layer.provideMerge(StorageDatabase.defaultLayer))
|
||||
const testDatabaseLayer = Layer.unwrap(
|
||||
Effect.gen(function* () {
|
||||
const dir = yield* Effect.acquireRelease(
|
||||
Effect.promise(() => fs.mkdtemp(path.join(os.tmpdir(), "opencode-storage-test-"))),
|
||||
(dir) => Effect.promise(() => fs.rm(dir, { recursive: true, force: true })),
|
||||
)
|
||||
return Layer.effect(
|
||||
StorageDatabase.Service,
|
||||
Effect.gen(function* () {
|
||||
const db = yield* StorageDatabase.Service
|
||||
yield* EffectDrizzleSqlite.migrate(db, { migrationsFolder: path.join(import.meta.dirname, "../../migration") })
|
||||
return db
|
||||
}),
|
||||
).pipe(Layer.provide(StorageDatabase.layerForPath(path.join(dir, "storage.db"))))
|
||||
}),
|
||||
)
|
||||
|
||||
const sqlLayer = SessionStorageSql.layer.pipe(Layer.provideMerge(testDatabaseLayer))
|
||||
|
||||
const sqlSeeds: Seeds<StorageDatabase.Service> = {
|
||||
reset: resetSqlSeeds(),
|
||||
|
||||
Reference in New Issue
Block a user