From fd4887d45d2ac8965447222e69b8c52c3e9e928c Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Sat, 2 May 2026 23:13:28 -0400 Subject: [PATCH] refactor(effect-drizzle-sqlite): simplify single-row reads --- packages/effect-drizzle-sqlite/src/index.ts | 13 ++++++++++++- packages/effect-drizzle-sqlite/test/sqlite.test.ts | 11 ++++++++++- packages/opencode/src/permission/index.ts | 14 ++++++++------ packages/opencode/src/share/share-next.ts | 10 ++++------ 4 files changed, 34 insertions(+), 14 deletions(-) diff --git a/packages/effect-drizzle-sqlite/src/index.ts b/packages/effect-drizzle-sqlite/src/index.ts index 57ffdecb7e..f8ffda9c9e 100644 --- a/packages/effect-drizzle-sqlite/src/index.ts +++ b/packages/effect-drizzle-sqlite/src/index.ts @@ -63,6 +63,10 @@ type SelectLike = EffectLikeQuery & { readonly all: () => A } +type GetLike = EffectLikeQuery & { + readonly get: () => A +} + type MutationLike = EffectLikeQuery & { readonly all: () => A readonly run: () => A @@ -104,6 +108,8 @@ const fromMutation = (query: MutationLike) => fromSync(query, () => (query.confi const fromCount = (query: CountLike) => fromSync(query, () => Number(query.session.values(query.sql)[0]?.[0] ?? 0)) +export const getOne = (query: GetLike) => fromSync(query, () => query.get()) + const fromExecuteResult = (result: unknown) => { if (result && typeof result === "object" && "sync" in result && typeof result.sync === "function") { return result.sync() @@ -155,6 +161,7 @@ const attachTransaction = < TRelations extends AnyRelations = EmptyRelations, >(db: SQLiteBunDatabase & { readonly $client: Database }): EffectSQLiteDatabase => { const txStack: Array> = [] + const bound = new WeakMap>() const current = () => txStack.at(-1) ?? db const runTransaction = (target: SQLiteBunDatabase | SQLiteTransaction<"sync", void, TSchema, TRelations>) => target.transaction.bind(target) as ( @@ -195,7 +202,11 @@ const attachTransaction = < const target = current() const value = Reflect.get(target, property) - return typeof value === "function" ? value.bind(target) : value + if (typeof value !== "function") return value + const methods = bound.get(target) ?? new Map() + bound.set(target, methods) + if (!methods.has(property)) methods.set(property, value.bind(target)) + return methods.get(property) }, }) as EffectSQLiteDatabase } diff --git a/packages/effect-drizzle-sqlite/test/sqlite.test.ts b/packages/effect-drizzle-sqlite/test/sqlite.test.ts index 5c02c18c5f..b4c34e70af 100644 --- a/packages/effect-drizzle-sqlite/test/sqlite.test.ts +++ b/packages/effect-drizzle-sqlite/test/sqlite.test.ts @@ -5,7 +5,7 @@ import { relations } from "drizzle-orm/_relations" import { drizzle as drizzleBun } from "drizzle-orm/bun-sqlite" import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core" import { Cause, Effect, Exit } from "effect" -import { EffectDrizzleQueryError, make, type EffectSQLiteDatabase } from "../src" +import { EffectDrizzleQueryError, getOne, make, type EffectSQLiteDatabase } from "../src" const users = sqliteTable("users", { id: integer().primaryKey(), @@ -176,6 +176,15 @@ describe("effect drizzle sqlite", () => { }), ) + testEffect("supports single-row select effects", () => + Effect.gen(function* () { + yield* db.insert(users).values({ id: 1, name: "Ada" }) + + expect(yield* getOne(db.select().from(users).where(eq(users.id, 1)))).toEqual({ id: 1, name: "Ada" }) + expect(yield* getOne(db.select().from(users).where(eq(users.id, 2)))).toBeUndefined() + }), + ) + testEffect("nested pipeable transactions commit or roll back with the outer transaction", () => Effect.gen(function* () { yield* Effect.gen(function* () { diff --git a/packages/opencode/src/permission/index.ts b/packages/opencode/src/permission/index.ts index d0baa9b042..daa73cb8fe 100644 --- a/packages/opencode/src/permission/index.ts +++ b/packages/opencode/src/permission/index.ts @@ -6,6 +6,7 @@ import { ProjectID } from "@/project/schema" import { MessageID, SessionID } from "@/session/schema" import { PermissionTable } from "@/session/session.sql" import { DatabaseEffect } from "@/storage/db-effect" +import { getOne } from "@opencode-ai/effect-drizzle-sqlite" import { eq } from "drizzle-orm" import { zod } from "@/util/effect-zod" import * as Log from "@opencode-ai/core/util/log" @@ -156,14 +157,15 @@ export const layer = Layer.effect( const db = yield* DatabaseEffect.Service const state = yield* InstanceState.make( Effect.fn("Permission.state")(function* (ctx) { - const rows = yield* db - .select() - .from(PermissionTable) - .where(eq(PermissionTable.project_id, ctx.project.id)) - .pipe(Effect.orDie) + const row = yield* getOne( + db + .select() + .from(PermissionTable) + .where(eq(PermissionTable.project_id, ctx.project.id)), + ).pipe(Effect.orDie) const state = { pending: new Map(), - approved: rows[0]?.data ?? [], + approved: row?.data ?? [], } yield* Effect.addFinalizer(() => diff --git a/packages/opencode/src/share/share-next.ts b/packages/opencode/src/share/share-next.ts index 754dd817db..d6f25b459a 100644 --- a/packages/opencode/src/share/share-next.ts +++ b/packages/opencode/src/share/share-next.ts @@ -10,6 +10,7 @@ import { Session } from "@/session/session" import { MessageV2 } from "@/session/message-v2" import type { SessionID } from "@/session/schema" import { DatabaseEffect } from "@/storage/db-effect" +import { getOne } from "@opencode-ai/effect-drizzle-sqlite" import { eq } from "drizzle-orm" import { Config } from "@/config/config" import * as Log from "@opencode-ai/core/util/log" @@ -224,12 +225,9 @@ export const layer = Layer.effect( }) const get = Effect.fnUntraced(function* (sessionID: SessionID) { - const rows = yield* db - .select() - .from(SessionShareTable) - .where(eq(SessionShareTable.session_id, sessionID)) - .pipe(Effect.orDie) - const row = rows[0] + const row = yield* getOne( + db.select().from(SessionShareTable).where(eq(SessionShareTable.session_id, sessionID)), + ).pipe(Effect.orDie) if (!row) return return { id: row.id, secret: row.secret, url: row.url } satisfies Share })