mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-27 04:56:20 +02:00
Adds self-hosted Git provider support (Forgejo, Gitea, GitLab) alongside GitHub: per-workspace token connection, a provider-dispatched webhook, PR/MR and CI mirroring, and the shared issue auto-link / auto-close machinery. Off until MULTICA_VCS_SECRET_KEY is set, so existing deployments are unaffected. Co-authored-by: Bohan <bohan@devv.ai>
438 lines
14 KiB
Go
438 lines
14 KiB
Go
// Code generated by sqlc. DO NOT EDIT.
|
|
// versions:
|
|
// sqlc v1.31.1
|
|
// source: workspace.sql
|
|
|
|
package db
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
const createWorkspace = `-- name: CreateWorkspace :one
|
|
INSERT INTO workspace (name, slug, description, context, issue_prefix)
|
|
VALUES ($1, $2, $3, $4, $5)
|
|
RETURNING id, name, slug, description, settings, created_at, updated_at, context, repos, issue_prefix, issue_counter, avatar_url, attribution_fail_closed
|
|
`
|
|
|
|
type CreateWorkspaceParams struct {
|
|
Name string `json:"name"`
|
|
Slug string `json:"slug"`
|
|
Description pgtype.Text `json:"description"`
|
|
Context pgtype.Text `json:"context"`
|
|
IssuePrefix string `json:"issue_prefix"`
|
|
}
|
|
|
|
func (q *Queries) CreateWorkspace(ctx context.Context, arg CreateWorkspaceParams) (Workspace, error) {
|
|
row := q.db.QueryRow(ctx, createWorkspace,
|
|
arg.Name,
|
|
arg.Slug,
|
|
arg.Description,
|
|
arg.Context,
|
|
arg.IssuePrefix,
|
|
)
|
|
var i Workspace
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Name,
|
|
&i.Slug,
|
|
&i.Description,
|
|
&i.Settings,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Context,
|
|
&i.Repos,
|
|
&i.IssuePrefix,
|
|
&i.IssueCounter,
|
|
&i.AvatarUrl,
|
|
&i.AttributionFailClosed,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const deleteWorkspace = `-- name: DeleteWorkspace :exec
|
|
WITH ws_installations AS (
|
|
SELECT id FROM channel_installation WHERE workspace_id = $1
|
|
),
|
|
ws_agents AS (
|
|
SELECT id FROM agent WHERE workspace_id = $1
|
|
),
|
|
ws_skills AS (
|
|
SELECT id FROM skill WHERE workspace_id = $1
|
|
),
|
|
cleared_agent_label_assignments AS (
|
|
DELETE FROM agent_to_label WHERE agent_id IN (SELECT id FROM ws_agents)
|
|
),
|
|
cleared_skill_label_assignments AS (
|
|
DELETE FROM skill_to_label WHERE skill_id IN (SELECT id FROM ws_skills)
|
|
),
|
|
cleared_chat_sessions AS (
|
|
DELETE FROM channel_chat_session_binding WHERE installation_id IN (SELECT id FROM ws_installations)
|
|
RETURNING chat_session_id
|
|
),
|
|
cleared_outbound_cards AS (
|
|
-- channel_outbound_card_message is keyed by chat_session_id (no FK); its own
|
|
-- chat_session rows cascade away with the workspace, so reach the cards through
|
|
-- the just-removed chat-session bindings, which still carry the id.
|
|
DELETE FROM channel_outbound_card_message
|
|
WHERE chat_session_id IN (SELECT chat_session_id FROM cleared_chat_sessions)
|
|
),
|
|
cleared_draft_restores AS (
|
|
-- chat_draft_restore is keyed by chat_session_id with no FK (MUL-3515) and has
|
|
-- no reaper, while its chat_session rows cascade away with the workspace. Reach
|
|
-- them directly through chat_session (unlike the cards above, this is not
|
|
-- limited to channel-bound sessions) or every pending restore — each holding a
|
|
-- user's prompt text — would outlive the workspace permanently (#5219).
|
|
--
|
|
-- This sweep only sees restores committed before the statement's snapshot, so
|
|
-- the caller must already hold LockChatSessionsByWorkspace: that lock is what
|
|
-- keeps FinalizeDeferredCancelledChat from inserting one behind it.
|
|
DELETE FROM chat_draft_restore
|
|
WHERE chat_session_id IN (SELECT id FROM chat_session WHERE workspace_id = $1)
|
|
),
|
|
cleared_inbound_dedup AS (
|
|
DELETE FROM channel_inbound_message_dedup WHERE installation_id IN (SELECT id FROM ws_installations)
|
|
),
|
|
cleared_audit AS (
|
|
-- Purge, don't detach: the workspace is gone and channel_inbound_audit has no
|
|
-- workspace_id and no reaper, so a detached (NULL) row would be permanently
|
|
-- unattributable. (Reclaim, where the workspace survives, still detaches.)
|
|
DELETE FROM channel_inbound_audit WHERE installation_id IN (SELECT id FROM ws_installations)
|
|
),
|
|
cleared_user_bindings AS (
|
|
DELETE FROM channel_user_binding WHERE workspace_id = $1
|
|
),
|
|
cleared_binding_tokens AS (
|
|
DELETE FROM channel_binding_token WHERE workspace_id = $1
|
|
),
|
|
cleared_installations AS (
|
|
DELETE FROM channel_installation WHERE workspace_id = $1
|
|
),
|
|
cleared_issue_properties AS (
|
|
DELETE FROM issue_property WHERE workspace_id = $1
|
|
),
|
|
deleted_pending_check_suites AS (
|
|
DELETE FROM github_pending_check_suite WHERE workspace_id = $1
|
|
),
|
|
ws_vcs_prs AS (
|
|
SELECT id FROM vcs_pull_request WHERE workspace_id = $1
|
|
),
|
|
ws_vcs_connections AS (
|
|
SELECT id FROM vcs_connection WHERE workspace_id = $1
|
|
),
|
|
cleared_vcs_pr_links AS (
|
|
DELETE FROM issue_vcs_pull_request
|
|
WHERE pull_request_id IN (SELECT id FROM ws_vcs_prs)
|
|
),
|
|
cleared_vcs_commit_statuses AS (
|
|
DELETE FROM vcs_commit_status
|
|
WHERE connection_id IN (SELECT id FROM ws_vcs_connections)
|
|
),
|
|
cleared_vcs_prs AS (
|
|
DELETE FROM vcs_pull_request WHERE workspace_id = $1
|
|
),
|
|
cleared_vcs_connections AS (
|
|
DELETE FROM vcs_connection WHERE workspace_id = $1
|
|
),
|
|
cleared_client_usage_workspace AS (
|
|
UPDATE client_usage_daily SET workspace_id = NULL WHERE workspace_id = $1
|
|
)
|
|
DELETE FROM workspace WHERE workspace.id = $1
|
|
`
|
|
|
|
// The channel_* tables (MUL-3515 §4), resource-label junctions, and custom issue
|
|
// property definitions carry NO FK to workspace, so — unlike the CASCADE-backed
|
|
// tables the DELETE below sweeps — they are not cleaned up implicitly. Remove
|
|
// their workspace-owned rows here so they commit or roll back atomically with
|
|
// the workspace row.
|
|
// VCS tables (migration 213) carry no FK to workspace, so they are not cascaded
|
|
// away by the DELETE below. Sweep the workspace's connections, mirrored PRs,
|
|
// their issue links, and CI statuses here. issue_vcs_pull_request has no
|
|
// workspace_id, so reach it through the workspace's PRs; vcs_commit_status has
|
|
// none either, so reach it through the workspace's connections.
|
|
func (q *Queries) DeleteWorkspace(ctx context.Context, id pgtype.UUID) error {
|
|
_, err := q.db.Exec(ctx, deleteWorkspace, id)
|
|
return err
|
|
}
|
|
|
|
const getDaemonWorkspace = `-- name: GetDaemonWorkspace :one
|
|
SELECT id, name
|
|
FROM workspace
|
|
WHERE id = $1
|
|
`
|
|
|
|
type GetDaemonWorkspaceRow struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
// Workspace-scoped daemon tokens do not carry a user ID. This narrow lookup
|
|
// lets them use the same endpoint without widening their token scope.
|
|
func (q *Queries) GetDaemonWorkspace(ctx context.Context, id pgtype.UUID) (GetDaemonWorkspaceRow, error) {
|
|
row := q.db.QueryRow(ctx, getDaemonWorkspace, id)
|
|
var i GetDaemonWorkspaceRow
|
|
err := row.Scan(&i.ID, &i.Name)
|
|
return i, err
|
|
}
|
|
|
|
const getWorkspace = `-- name: GetWorkspace :one
|
|
SELECT id, name, slug, description, settings, created_at, updated_at, context, repos, issue_prefix, issue_counter, avatar_url, attribution_fail_closed FROM workspace
|
|
WHERE id = $1
|
|
`
|
|
|
|
func (q *Queries) GetWorkspace(ctx context.Context, id pgtype.UUID) (Workspace, error) {
|
|
row := q.db.QueryRow(ctx, getWorkspace, id)
|
|
var i Workspace
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Name,
|
|
&i.Slug,
|
|
&i.Description,
|
|
&i.Settings,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Context,
|
|
&i.Repos,
|
|
&i.IssuePrefix,
|
|
&i.IssueCounter,
|
|
&i.AvatarUrl,
|
|
&i.AttributionFailClosed,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getWorkspaceAttributionFailClosed = `-- name: GetWorkspaceAttributionFailClosed :one
|
|
SELECT attribution_fail_closed FROM workspace
|
|
WHERE id = $1
|
|
`
|
|
|
|
// Lean read of the fail-closed attribution policy for the enqueue hot path
|
|
// (MUL-4302 §3.5), avoiding a full workspace-row fetch.
|
|
func (q *Queries) GetWorkspaceAttributionFailClosed(ctx context.Context, id pgtype.UUID) (bool, error) {
|
|
row := q.db.QueryRow(ctx, getWorkspaceAttributionFailClosed, id)
|
|
var attribution_fail_closed bool
|
|
err := row.Scan(&attribution_fail_closed)
|
|
return attribution_fail_closed, err
|
|
}
|
|
|
|
const getWorkspaceBySlug = `-- name: GetWorkspaceBySlug :one
|
|
SELECT id, name, slug, description, settings, created_at, updated_at, context, repos, issue_prefix, issue_counter, avatar_url, attribution_fail_closed FROM workspace
|
|
WHERE slug = $1
|
|
`
|
|
|
|
func (q *Queries) GetWorkspaceBySlug(ctx context.Context, slug string) (Workspace, error) {
|
|
row := q.db.QueryRow(ctx, getWorkspaceBySlug, slug)
|
|
var i Workspace
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Name,
|
|
&i.Slug,
|
|
&i.Description,
|
|
&i.Settings,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Context,
|
|
&i.Repos,
|
|
&i.IssuePrefix,
|
|
&i.IssueCounter,
|
|
&i.AvatarUrl,
|
|
&i.AttributionFailClosed,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const incrementIssueCounter = `-- name: IncrementIssueCounter :one
|
|
UPDATE workspace SET issue_counter = issue_counter + 1
|
|
WHERE id = $1
|
|
RETURNING issue_counter
|
|
`
|
|
|
|
func (q *Queries) IncrementIssueCounter(ctx context.Context, id pgtype.UUID) (int32, error) {
|
|
row := q.db.QueryRow(ctx, incrementIssueCounter, id)
|
|
var issue_counter int32
|
|
err := row.Scan(&issue_counter)
|
|
return issue_counter, err
|
|
}
|
|
|
|
const listDaemonWorkspaces = `-- name: ListDaemonWorkspaces :many
|
|
SELECT w.id, w.name
|
|
FROM member m
|
|
JOIN workspace w ON w.id = m.workspace_id
|
|
WHERE m.user_id = $1
|
|
ORDER BY w.id ASC
|
|
`
|
|
|
|
type ListDaemonWorkspacesRow struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
// Daemons only need the membership set and display name to discover which
|
|
// workspaces should have local runtimes. Keep this projection intentionally
|
|
// narrow so the periodic consistency check never reads UI-only JSON/text
|
|
// columns such as settings, repos, or context.
|
|
func (q *Queries) ListDaemonWorkspaces(ctx context.Context, userID pgtype.UUID) ([]ListDaemonWorkspacesRow, error) {
|
|
rows, err := q.db.Query(ctx, listDaemonWorkspaces, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []ListDaemonWorkspacesRow{}
|
|
for rows.Next() {
|
|
var i ListDaemonWorkspacesRow
|
|
if err := rows.Scan(&i.ID, &i.Name); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listWorkspaces = `-- name: ListWorkspaces :many
|
|
SELECT w.id, w.name, w.slug, w.description, w.settings,
|
|
w.created_at, w.updated_at, w.context, w.repos,
|
|
w.issue_prefix, w.issue_counter, w.avatar_url, w.attribution_fail_closed
|
|
FROM member m
|
|
JOIN workspace w ON w.id = m.workspace_id
|
|
WHERE m.user_id = $1
|
|
ORDER BY w.created_at ASC
|
|
`
|
|
|
|
func (q *Queries) ListWorkspaces(ctx context.Context, userID pgtype.UUID) ([]Workspace, error) {
|
|
rows, err := q.db.Query(ctx, listWorkspaces, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []Workspace{}
|
|
for rows.Next() {
|
|
var i Workspace
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.Name,
|
|
&i.Slug,
|
|
&i.Description,
|
|
&i.Settings,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Context,
|
|
&i.Repos,
|
|
&i.IssuePrefix,
|
|
&i.IssueCounter,
|
|
&i.AvatarUrl,
|
|
&i.AttributionFailClosed,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const lockWorkspaceForChatSessionCreate = `-- name: LockWorkspaceForChatSessionCreate :one
|
|
SELECT id FROM workspace WHERE id = $1 FOR KEY SHARE
|
|
`
|
|
|
|
// The creator half of the workspace delete/create protocol (#5219). Every
|
|
// production path that inserts a chat_session takes this FOR KEY SHARE lock on the
|
|
// parent workspace row, inside its transaction, before CreateChatSession. It
|
|
// conflicts with DeleteWorkspace's FOR UPDATE (so a create is blocked while a
|
|
// delete is in progress, and vice versa) but not with other creators (FOR KEY
|
|
// SHARE locks share), so concurrent session creation stays unserialized. This
|
|
// makes the mutual exclusion explicit rather than leaning on the workspace FK's
|
|
// implicit FOR KEY SHARE, which would vanish if that FK is dropped.
|
|
func (q *Queries) LockWorkspaceForChatSessionCreate(ctx context.Context, id pgtype.UUID) (pgtype.UUID, error) {
|
|
row := q.db.QueryRow(ctx, lockWorkspaceForChatSessionCreate, id)
|
|
var id_2 pgtype.UUID
|
|
err := row.Scan(&id_2)
|
|
return id_2, err
|
|
}
|
|
|
|
const lockWorkspaceForDelete = `-- name: LockWorkspaceForDelete :one
|
|
SELECT id FROM workspace WHERE id = $1 FOR UPDATE
|
|
`
|
|
|
|
// Taken first by DeleteWorkspace, before it enumerates the workspace's chat
|
|
// sessions. LockChatSessionsByWorkspace only covers sessions that exist when it
|
|
// runs; a CreateChatSession committing during the delete window would add one
|
|
// the lock set never saw, and a finalizer could then insert a restore for it
|
|
// after the sweep's snapshot — orphaning the prompt (#5219).
|
|
//
|
|
// The delete window is held closed against new sessions by an EXPLICIT protocol,
|
|
// not the chat_session.workspace_id FK: every session creator takes
|
|
// LockWorkspaceForChatSessionCreate (FOR KEY SHARE) on this row first, and this
|
|
// FOR UPDATE conflicts with it. Keeping the bar in the app layer means it does
|
|
// not silently break if that FK is ever dropped (the codebase is moving FK
|
|
// relationships into the application layer, MUL-3515). Lock order is
|
|
// workspace -> chat_session -> agent_task_queue; the finalizer never touches
|
|
// workspace, so this cannot deadlock against it.
|
|
func (q *Queries) LockWorkspaceForDelete(ctx context.Context, id pgtype.UUID) (pgtype.UUID, error) {
|
|
row := q.db.QueryRow(ctx, lockWorkspaceForDelete, id)
|
|
var id_2 pgtype.UUID
|
|
err := row.Scan(&id_2)
|
|
return id_2, err
|
|
}
|
|
|
|
const updateWorkspace = `-- name: UpdateWorkspace :one
|
|
UPDATE workspace SET
|
|
name = COALESCE($2, name),
|
|
description = COALESCE($3, description),
|
|
context = COALESCE($4, context),
|
|
settings = COALESCE($5, settings),
|
|
repos = COALESCE($6, repos),
|
|
issue_prefix = COALESCE($7, issue_prefix),
|
|
avatar_url = COALESCE($8, avatar_url),
|
|
updated_at = now()
|
|
WHERE id = $1
|
|
RETURNING id, name, slug, description, settings, created_at, updated_at, context, repos, issue_prefix, issue_counter, avatar_url, attribution_fail_closed
|
|
`
|
|
|
|
type UpdateWorkspaceParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
Name pgtype.Text `json:"name"`
|
|
Description pgtype.Text `json:"description"`
|
|
Context pgtype.Text `json:"context"`
|
|
Settings []byte `json:"settings"`
|
|
Repos []byte `json:"repos"`
|
|
IssuePrefix pgtype.Text `json:"issue_prefix"`
|
|
AvatarUrl pgtype.Text `json:"avatar_url"`
|
|
}
|
|
|
|
func (q *Queries) UpdateWorkspace(ctx context.Context, arg UpdateWorkspaceParams) (Workspace, error) {
|
|
row := q.db.QueryRow(ctx, updateWorkspace,
|
|
arg.ID,
|
|
arg.Name,
|
|
arg.Description,
|
|
arg.Context,
|
|
arg.Settings,
|
|
arg.Repos,
|
|
arg.IssuePrefix,
|
|
arg.AvatarUrl,
|
|
)
|
|
var i Workspace
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Name,
|
|
&i.Slug,
|
|
&i.Description,
|
|
&i.Settings,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Context,
|
|
&i.Repos,
|
|
&i.IssuePrefix,
|
|
&i.IssueCounter,
|
|
&i.AvatarUrl,
|
|
&i.AttributionFailClosed,
|
|
)
|
|
return i, err
|
|
}
|