From 9e082c2d4321e99fd3f5d7e452ade9b831f6f80e Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Tue, 11 Aug 2026 13:07:05 -0400 Subject: [PATCH] refactor(core): centralize project upsert (#41795) --- packages/core/src/project.ts | 18 +++--------------- packages/core/src/project/sql.ts | 25 +++++++++++++++++++++++++ packages/core/src/session.ts | 21 +++------------------ packages/core/src/session/transfer.ts | 21 +++------------------ 4 files changed, 34 insertions(+), 51 deletions(-) diff --git a/packages/core/src/project.ts b/packages/core/src/project.ts index fece057b45..38df926600 100644 --- a/packages/core/src/project.ts +++ b/packages/core/src/project.ts @@ -2,7 +2,7 @@ export * as Project from "./project" import { Context, Effect, Layer, Schema } from "effect" import { ChildProcess } from "effect/unstable/process" -import { asc, desc, isNotNull, isNull, ne, or } from "drizzle-orm" +import { asc, desc } from "drizzle-orm" import path from "path" import { AbsolutePath } from "./schema" import { Database } from "./database/database" @@ -13,7 +13,7 @@ import { makeGlobalNode } from "@opencode-ai/util/effect/app-node" import { Hash } from "@opencode-ai/util/hash" import { ProjectDirectories } from "./project/directories" import { ProjectSchema } from "./project/schema" -import { ProjectTable } from "./project/sql" +import { ProjectTable, upsertProject } from "./project/sql" export const ID = ProjectSchema.ID export type ID = ProjectSchema.ID @@ -98,19 +98,7 @@ const layer = Layer.effect( yield* db .transaction((tx) => Effect.gen(function* () { - const vcs = project.vcs?.type - yield* tx - .insert(ProjectTable) - .values({ id: project.id, worktree: project.canonical, vcs, sandboxes: [] }) - .onConflictDoUpdate({ - target: ProjectTable.id, - set: { worktree: project.canonical, vcs: vcs ?? null }, - setWhere: or( - ne(ProjectTable.worktree, project.canonical), - vcs ? or(isNull(ProjectTable.vcs), ne(ProjectTable.vcs, vcs)) : isNotNull(ProjectTable.vcs), - ), - }) - .run() + yield* upsertProject(tx, project) if (!project.vcs) return yield* projectDirectories.create({ projectID: project.id, directory: project.canonical }, tx) if (project.directory === project.canonical) return diff --git a/packages/core/src/project/sql.ts b/packages/core/src/project/sql.ts index fc08241deb..9e87ee5a78 100644 --- a/packages/core/src/project/sql.ts +++ b/packages/core/src/project/sql.ts @@ -1,8 +1,14 @@ +import type { EffectDrizzleSqlite } from "@opencode-ai/effect-drizzle-sqlite" +import { isNotNull, isNull, ne, or } from "drizzle-orm" import { sqliteTable, text, integer, primaryKey } from "drizzle-orm/sqlite-core" import { absoluteArrayColumn, absoluteColumn } from "../database/path" import { Timestamps } from "../database/schema.sql" +import type { AbsolutePath } from "../schema" import { ProjectSchema } from "./schema" +type DatabaseClient = EffectDrizzleSqlite.EffectSQLiteDatabase +type Transaction = Parameters[0]>[0] + export const ProjectTable = sqliteTable("project", { id: text().$type().primaryKey(), worktree: absoluteColumn().notNull(), @@ -33,3 +39,22 @@ export const ProjectDirectoryTable = sqliteTable( }, (table) => [primaryKey({ columns: [table.project_id, table.directory] })], ) + +export function upsertProject( + db: DatabaseClient | Transaction, + project: { readonly id: ProjectSchema.ID; readonly canonical: AbsolutePath; readonly vcs?: ProjectSchema.Vcs }, +) { + const vcs = project.vcs?.type + return db + .insert(ProjectTable) + .values({ id: project.id, worktree: project.canonical, vcs, sandboxes: [] }) + .onConflictDoUpdate({ + target: ProjectTable.id, + set: { worktree: project.canonical, vcs: vcs ?? null }, + setWhere: or( + ne(ProjectTable.worktree, project.canonical), + vcs ? or(isNull(ProjectTable.vcs), ne(ProjectTable.vcs, vcs)) : isNotNull(ProjectTable.vcs), + ), + }) + .run() +} diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index 7836b333c4..3c8f8161b9 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -3,7 +3,7 @@ export * from "./session/schema" import { Effect, Layer, Schema, Context, Stream, Scope } from "effect" import { ListAnchor } from "@opencode-ai/schema/session" -import { and, asc, desc, eq, gt, isNotNull, isNull, like, lt, ne, or, type SQL } from "drizzle-orm" +import { and, asc, desc, eq, gt, isNull, like, lt, or, type SQL } from "drizzle-orm" import { Project } from "./project" import { Workspace } from "./workspace" import { Model } from "./model" @@ -21,7 +21,7 @@ import { Agent } from "./agent" import { Money } from "@opencode-ai/schema/money" import { App } from "./app" import { Slug } from "./util/slug" -import { ProjectTable } from "./project/sql" +import { upsertProject } from "./project/sql" import path from "path" import { fromRow } from "./session/info" import { SessionRunner } from "./session/runner/index" @@ -309,22 +309,7 @@ const layer = Layer.effect( const shellLocks = KeyedMutex.makeUnsafe() const decodeMessage = Schema.decodeUnknownEffect(SessionMessage.Info) const isDurableSessionEvent = Schema.is(SessionEvent.Durable) - const persistProject = (project: Project.Resolved) => { - const vcs = project.vcs?.type - return db - .insert(ProjectTable) - .values({ id: project.id, worktree: project.canonical, vcs, sandboxes: [] }) - .onConflictDoUpdate({ - target: ProjectTable.id, - set: { worktree: project.canonical, vcs: vcs ?? null }, - setWhere: or( - ne(ProjectTable.worktree, project.canonical), - vcs ? or(isNull(ProjectTable.vcs), ne(ProjectTable.vcs, vcs)) : isNotNull(ProjectTable.vcs), - ), - }) - .run() - .pipe(Effect.orDie) - } + const persistProject = (project: Project.Resolved) => upsertProject(db, project).pipe(Effect.orDie) const decode = (row: typeof SessionMessageTable.$inferSelect) => decodeMessage({ ...row.data, id: row.id, type: row.type }).pipe( Effect.mapError( diff --git a/packages/core/src/session/transfer.ts b/packages/core/src/session/transfer.ts index 89b3a15d5d..e9919deadb 100644 --- a/packages/core/src/session/transfer.ts +++ b/packages/core/src/session/transfer.ts @@ -3,7 +3,7 @@ export * as SessionTransfer from "./transfer" import { SessionTransfer } from "@opencode-ai/schema/session-transfer" import { Tool } from "@opencode-ai/schema/tool" import { Skill } from "@opencode-ai/schema/skill" -import { eq, isNotNull, isNull, ne, or } from "drizzle-orm" +import { eq } from "drizzle-orm" import { Context, DateTime, Effect, Layer, Schema } from "effect" import path from "path" import { makeGlobalNode } from "@opencode-ai/util/effect/app-node" @@ -12,7 +12,7 @@ import { Bus } from "../bus" import { Database } from "../database/database" import { Location } from "../location" import { Project } from "../project" -import { ProjectTable } from "../project/sql" +import { upsertProject } from "../project/sql" import { AbsolutePath, RelativePath } from "../schema" import { Session } from "../session" import { Slug } from "../util/slug" @@ -49,22 +49,7 @@ const layer = Layer.effect( const sessions = yield* Session.Service const encodeMessage = Schema.encodeSync(SessionMessage.Info) - const persistProject = (project: Project.Resolved) => { - const vcs = project.vcs?.type - return db - .insert(ProjectTable) - .values({ id: project.id, worktree: project.canonical, vcs, sandboxes: [] }) - .onConflictDoUpdate({ - target: ProjectTable.id, - set: { worktree: project.canonical, vcs: vcs ?? null }, - setWhere: or( - ne(ProjectTable.worktree, project.canonical), - vcs ? or(isNull(ProjectTable.vcs), ne(ProjectTable.vcs, vcs)) : isNotNull(ProjectTable.vcs), - ), - }) - .run() - .pipe(Effect.orDie) - } + const persistProject = (project: Project.Resolved) => upsertProject(db, project).pipe(Effect.orDie) return Service.of({ export: Effect.fn("SessionTransfer.export")(function* (input) {