// Code generated by sqlc. DO NOT EDIT. // versions: // sqlc v1.31.1 // source: issue.sql package db import ( "context" "github.com/jackc/pgx/v5/pgtype" ) const childIssueProgress = `-- name: ChildIssueProgress :many SELECT parent_issue_id, COUNT(*)::bigint AS total, COUNT(*) FILTER (WHERE status IN ('done', 'cancelled'))::bigint AS done FROM issue WHERE workspace_id = $1 AND parent_issue_id IS NOT NULL GROUP BY parent_issue_id ` type ChildIssueProgressRow struct { ParentIssueID pgtype.UUID `json:"parent_issue_id"` Total int64 `json:"total"` Done int64 `json:"done"` } func (q *Queries) ChildIssueProgress(ctx context.Context, workspaceID pgtype.UUID) ([]ChildIssueProgressRow, error) { rows, err := q.db.Query(ctx, childIssueProgress, workspaceID) if err != nil { return nil, err } defer rows.Close() items := []ChildIssueProgressRow{} for rows.Next() { var i ChildIssueProgressRow if err := rows.Scan(&i.ParentIssueID, &i.Total, &i.Done); err != nil { return nil, err } items = append(items, i) } if err := rows.Err(); err != nil { return nil, err } return items, nil } const countCreatedIssueAssignees = `-- name: CountCreatedIssueAssignees :many SELECT assignee_type, assignee_id, COUNT(*)::bigint as frequency FROM issue WHERE workspace_id = $1 AND creator_id = $2 AND creator_type = 'member' AND assignee_type IS NOT NULL AND assignee_id IS NOT NULL GROUP BY assignee_type, assignee_id ` type CountCreatedIssueAssigneesParams struct { WorkspaceID pgtype.UUID `json:"workspace_id"` CreatorID pgtype.UUID `json:"creator_id"` } type CountCreatedIssueAssigneesRow struct { AssigneeType pgtype.Text `json:"assignee_type"` AssigneeID pgtype.UUID `json:"assignee_id"` Frequency int64 `json:"frequency"` } // Count assignees on issues created by a specific user. func (q *Queries) CountCreatedIssueAssignees(ctx context.Context, arg CountCreatedIssueAssigneesParams) ([]CountCreatedIssueAssigneesRow, error) { rows, err := q.db.Query(ctx, countCreatedIssueAssignees, arg.WorkspaceID, arg.CreatorID) if err != nil { return nil, err } defer rows.Close() items := []CountCreatedIssueAssigneesRow{} for rows.Next() { var i CountCreatedIssueAssigneesRow if err := rows.Scan(&i.AssigneeType, &i.AssigneeID, &i.Frequency); err != nil { return nil, err } items = append(items, i) } if err := rows.Err(); err != nil { return nil, err } return items, nil } const countIssues = `-- name: CountIssues :one SELECT count(*) FROM issue i WHERE i.workspace_id = $1 AND ($2::text IS NULL OR i.status = $2) AND ($3::text IS NULL OR i.priority = $3) AND ($4::uuid IS NULL OR i.assignee_id = $4) AND ($5::uuid[] IS NULL OR i.assignee_id = ANY($5::uuid[])) AND ($6::uuid IS NULL OR i.creator_id = $6) AND ($7::uuid IS NULL OR i.project_id = $7) AND ($8::bool IS NULL OR (i.start_date IS NOT NULL OR i.due_date IS NOT NULL)) AND ($9::jsonb IS NULL OR i.metadata @> $9::jsonb) AND ( $10::uuid IS NULL OR (i.assignee_type = 'agent' AND i.assignee_id IN ( SELECT a.id FROM agent a WHERE a.workspace_id = $1 AND a.owner_id = $10::uuid )) OR (i.assignee_type = 'squad' AND i.assignee_id IN ( SELECT sm.squad_id FROM squad_member sm JOIN squad s ON s.id = sm.squad_id WHERE s.workspace_id = $1 AND sm.member_type = 'member' AND sm.member_id = $10::uuid UNION SELECT s.id FROM squad s JOIN agent a ON a.id = s.leader_id WHERE s.workspace_id = $1 AND a.workspace_id = $1 AND a.owner_id = $10::uuid UNION SELECT sm.squad_id FROM squad_member sm JOIN squad s ON s.id = sm.squad_id JOIN agent a ON a.id = sm.member_id WHERE s.workspace_id = $1 AND sm.member_type = 'agent' AND a.workspace_id = $1 AND a.owner_id = $10::uuid )) ) ` type CountIssuesParams struct { WorkspaceID pgtype.UUID `json:"workspace_id"` Status pgtype.Text `json:"status"` Priority pgtype.Text `json:"priority"` AssigneeID pgtype.UUID `json:"assignee_id"` AssigneeIds []pgtype.UUID `json:"assignee_ids"` CreatorID pgtype.UUID `json:"creator_id"` ProjectID pgtype.UUID `json:"project_id"` Scheduled pgtype.Bool `json:"scheduled"` MetadataFilter []byte `json:"metadata_filter"` InvolvesUserID pgtype.UUID `json:"involves_user_id"` } // See ListIssues for the semantics of involves_user_id. func (q *Queries) CountIssues(ctx context.Context, arg CountIssuesParams) (int64, error) { row := q.db.QueryRow(ctx, countIssues, arg.WorkspaceID, arg.Status, arg.Priority, arg.AssigneeID, arg.AssigneeIds, arg.CreatorID, arg.ProjectID, arg.Scheduled, arg.MetadataFilter, arg.InvolvesUserID, ) var count int64 err := row.Scan(&count) return count, err } const createIssue = `-- name: CreateIssue :one INSERT INTO issue ( workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, position, start_date, due_date, number, project_id ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15 ) RETURNING id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, position, due_date, created_at, updated_at, number, project_id, origin_type, origin_id, first_executed_at, start_date, metadata ` type CreateIssueParams struct { WorkspaceID pgtype.UUID `json:"workspace_id"` Title string `json:"title"` Description pgtype.Text `json:"description"` Status string `json:"status"` Priority string `json:"priority"` AssigneeType pgtype.Text `json:"assignee_type"` AssigneeID pgtype.UUID `json:"assignee_id"` CreatorType string `json:"creator_type"` CreatorID pgtype.UUID `json:"creator_id"` ParentIssueID pgtype.UUID `json:"parent_issue_id"` Position float64 `json:"position"` StartDate pgtype.Date `json:"start_date"` DueDate pgtype.Date `json:"due_date"` Number int32 `json:"number"` ProjectID pgtype.UUID `json:"project_id"` } func (q *Queries) CreateIssue(ctx context.Context, arg CreateIssueParams) (Issue, error) { row := q.db.QueryRow(ctx, createIssue, arg.WorkspaceID, arg.Title, arg.Description, arg.Status, arg.Priority, arg.AssigneeType, arg.AssigneeID, arg.CreatorType, arg.CreatorID, arg.ParentIssueID, arg.Position, arg.StartDate, arg.DueDate, arg.Number, arg.ProjectID, ) var i Issue err := row.Scan( &i.ID, &i.WorkspaceID, &i.Title, &i.Description, &i.Status, &i.Priority, &i.AssigneeType, &i.AssigneeID, &i.CreatorType, &i.CreatorID, &i.ParentIssueID, &i.AcceptanceCriteria, &i.ContextRefs, &i.Position, &i.DueDate, &i.CreatedAt, &i.UpdatedAt, &i.Number, &i.ProjectID, &i.OriginType, &i.OriginID, &i.FirstExecutedAt, &i.StartDate, &i.Metadata, ) return i, err } const createIssueWithOrigin = `-- name: CreateIssueWithOrigin :one INSERT INTO issue ( workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, position, start_date, due_date, number, project_id, origin_type, origin_id ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17 ) RETURNING id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, position, due_date, created_at, updated_at, number, project_id, origin_type, origin_id, first_executed_at, start_date, metadata ` type CreateIssueWithOriginParams struct { WorkspaceID pgtype.UUID `json:"workspace_id"` Title string `json:"title"` Description pgtype.Text `json:"description"` Status string `json:"status"` Priority string `json:"priority"` AssigneeType pgtype.Text `json:"assignee_type"` AssigneeID pgtype.UUID `json:"assignee_id"` CreatorType string `json:"creator_type"` CreatorID pgtype.UUID `json:"creator_id"` ParentIssueID pgtype.UUID `json:"parent_issue_id"` Position float64 `json:"position"` StartDate pgtype.Date `json:"start_date"` DueDate pgtype.Date `json:"due_date"` Number int32 `json:"number"` ProjectID pgtype.UUID `json:"project_id"` OriginType pgtype.Text `json:"origin_type"` OriginID pgtype.UUID `json:"origin_id"` } func (q *Queries) CreateIssueWithOrigin(ctx context.Context, arg CreateIssueWithOriginParams) (Issue, error) { row := q.db.QueryRow(ctx, createIssueWithOrigin, arg.WorkspaceID, arg.Title, arg.Description, arg.Status, arg.Priority, arg.AssigneeType, arg.AssigneeID, arg.CreatorType, arg.CreatorID, arg.ParentIssueID, arg.Position, arg.StartDate, arg.DueDate, arg.Number, arg.ProjectID, arg.OriginType, arg.OriginID, ) var i Issue err := row.Scan( &i.ID, &i.WorkspaceID, &i.Title, &i.Description, &i.Status, &i.Priority, &i.AssigneeType, &i.AssigneeID, &i.CreatorType, &i.CreatorID, &i.ParentIssueID, &i.AcceptanceCriteria, &i.ContextRefs, &i.Position, &i.DueDate, &i.CreatedAt, &i.UpdatedAt, &i.Number, &i.ProjectID, &i.OriginType, &i.OriginID, &i.FirstExecutedAt, &i.StartDate, &i.Metadata, ) return i, err } const deleteIssue = `-- name: DeleteIssue :exec DELETE FROM issue WHERE id = $1 AND workspace_id = $2 ` type DeleteIssueParams struct { ID pgtype.UUID `json:"id"` WorkspaceID pgtype.UUID `json:"workspace_id"` } // Defense-in-depth: the workspace_id predicate makes the tenant invariant a // SQL-layer guarantee rather than a handler-layer one. Handler loaders // (loadIssueForUser / GetIssueInWorkspace) already enforce membership today, // but a future loader bypass or a new caller skipping the loader would be // silently catastrophic without this guard. See incident #1661. func (q *Queries) DeleteIssue(ctx context.Context, arg DeleteIssueParams) error { _, err := q.db.Exec(ctx, deleteIssue, arg.ID, arg.WorkspaceID) return err } const deleteIssueMetadataKey = `-- name: DeleteIssueMetadataKey :one UPDATE issue SET metadata = metadata - $1::text, updated_at = now() WHERE id = $2 AND workspace_id = $3 RETURNING id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, position, due_date, created_at, updated_at, number, project_id, origin_type, origin_id, first_executed_at, start_date, metadata ` type DeleteIssueMetadataKeyParams struct { Key string `json:"key"` ID pgtype.UUID `json:"id"` WorkspaceID pgtype.UUID `json:"workspace_id"` } // Atomically removes a single key from the issue's metadata JSONB. // Deleting a missing key is a no-op (still returns the row). func (q *Queries) DeleteIssueMetadataKey(ctx context.Context, arg DeleteIssueMetadataKeyParams) (Issue, error) { row := q.db.QueryRow(ctx, deleteIssueMetadataKey, arg.Key, arg.ID, arg.WorkspaceID) var i Issue err := row.Scan( &i.ID, &i.WorkspaceID, &i.Title, &i.Description, &i.Status, &i.Priority, &i.AssigneeType, &i.AssigneeID, &i.CreatorType, &i.CreatorID, &i.ParentIssueID, &i.AcceptanceCriteria, &i.ContextRefs, &i.Position, &i.DueDate, &i.CreatedAt, &i.UpdatedAt, &i.Number, &i.ProjectID, &i.OriginType, &i.OriginID, &i.FirstExecutedAt, &i.StartDate, &i.Metadata, ) return i, err } const findActiveDuplicateIssue = `-- name: FindActiveDuplicateIssue :one SELECT id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, position, due_date, created_at, updated_at, number, project_id, origin_type, origin_id, first_executed_at, start_date, metadata FROM issue WHERE workspace_id = $1 AND status NOT IN ('done', 'cancelled') AND project_id IS NOT DISTINCT FROM $2::uuid AND parent_issue_id IS NOT DISTINCT FROM $3::uuid AND lower(btrim(regexp_replace(title, '[[:space:]]+', ' ', 'g'))) = $4 ORDER BY created_at ASC LIMIT 1 ` type FindActiveDuplicateIssueParams struct { WorkspaceID pgtype.UUID `json:"workspace_id"` ProjectID pgtype.UUID `json:"project_id"` ParentIssueID pgtype.UUID `json:"parent_issue_id"` NormalizedTitle string `json:"normalized_title"` } func (q *Queries) FindActiveDuplicateIssue(ctx context.Context, arg FindActiveDuplicateIssueParams) (Issue, error) { row := q.db.QueryRow(ctx, findActiveDuplicateIssue, arg.WorkspaceID, arg.ProjectID, arg.ParentIssueID, arg.NormalizedTitle, ) var i Issue err := row.Scan( &i.ID, &i.WorkspaceID, &i.Title, &i.Description, &i.Status, &i.Priority, &i.AssigneeType, &i.AssigneeID, &i.CreatorType, &i.CreatorID, &i.ParentIssueID, &i.AcceptanceCriteria, &i.ContextRefs, &i.Position, &i.DueDate, &i.CreatedAt, &i.UpdatedAt, &i.Number, &i.ProjectID, &i.OriginType, &i.OriginID, &i.FirstExecutedAt, &i.StartDate, &i.Metadata, ) return i, err } const getIssue = `-- name: GetIssue :one SELECT id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, position, due_date, created_at, updated_at, number, project_id, origin_type, origin_id, first_executed_at, start_date, metadata FROM issue WHERE id = $1 ` func (q *Queries) GetIssue(ctx context.Context, id pgtype.UUID) (Issue, error) { row := q.db.QueryRow(ctx, getIssue, id) var i Issue err := row.Scan( &i.ID, &i.WorkspaceID, &i.Title, &i.Description, &i.Status, &i.Priority, &i.AssigneeType, &i.AssigneeID, &i.CreatorType, &i.CreatorID, &i.ParentIssueID, &i.AcceptanceCriteria, &i.ContextRefs, &i.Position, &i.DueDate, &i.CreatedAt, &i.UpdatedAt, &i.Number, &i.ProjectID, &i.OriginType, &i.OriginID, &i.FirstExecutedAt, &i.StartDate, &i.Metadata, ) return i, err } const getIssueByNumber = `-- name: GetIssueByNumber :one SELECT id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, position, due_date, created_at, updated_at, number, project_id, origin_type, origin_id, first_executed_at, start_date, metadata FROM issue WHERE workspace_id = $1 AND number = $2 ` type GetIssueByNumberParams struct { WorkspaceID pgtype.UUID `json:"workspace_id"` Number int32 `json:"number"` } func (q *Queries) GetIssueByNumber(ctx context.Context, arg GetIssueByNumberParams) (Issue, error) { row := q.db.QueryRow(ctx, getIssueByNumber, arg.WorkspaceID, arg.Number) var i Issue err := row.Scan( &i.ID, &i.WorkspaceID, &i.Title, &i.Description, &i.Status, &i.Priority, &i.AssigneeType, &i.AssigneeID, &i.CreatorType, &i.CreatorID, &i.ParentIssueID, &i.AcceptanceCriteria, &i.ContextRefs, &i.Position, &i.DueDate, &i.CreatedAt, &i.UpdatedAt, &i.Number, &i.ProjectID, &i.OriginType, &i.OriginID, &i.FirstExecutedAt, &i.StartDate, &i.Metadata, ) return i, err } const getIssueByOrigin = `-- name: GetIssueByOrigin :one SELECT id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, position, due_date, created_at, updated_at, number, project_id, origin_type, origin_id, first_executed_at, start_date, metadata FROM issue WHERE workspace_id = $1 AND origin_type = $2 AND origin_id = $3 LIMIT 1 ` type GetIssueByOriginParams struct { WorkspaceID pgtype.UUID `json:"workspace_id"` OriginType pgtype.Text `json:"origin_type"` OriginID pgtype.UUID `json:"origin_id"` } // Finds the issue stamped with a specific (origin_type, origin_id) pair. // Used by quick-create completion to deterministically locate the issue // produced by a given agent_task_queue.id — robust against concurrent // issue creates by the same agent (assignment task + quick-create both // running with max_concurrent_tasks > 1). func (q *Queries) GetIssueByOrigin(ctx context.Context, arg GetIssueByOriginParams) (Issue, error) { row := q.db.QueryRow(ctx, getIssueByOrigin, arg.WorkspaceID, arg.OriginType, arg.OriginID) var i Issue err := row.Scan( &i.ID, &i.WorkspaceID, &i.Title, &i.Description, &i.Status, &i.Priority, &i.AssigneeType, &i.AssigneeID, &i.CreatorType, &i.CreatorID, &i.ParentIssueID, &i.AcceptanceCriteria, &i.ContextRefs, &i.Position, &i.DueDate, &i.CreatedAt, &i.UpdatedAt, &i.Number, &i.ProjectID, &i.OriginType, &i.OriginID, &i.FirstExecutedAt, &i.StartDate, &i.Metadata, ) return i, err } const getIssueInWorkspace = `-- name: GetIssueInWorkspace :one SELECT id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, position, due_date, created_at, updated_at, number, project_id, origin_type, origin_id, first_executed_at, start_date, metadata FROM issue WHERE id = $1 AND workspace_id = $2 ` type GetIssueInWorkspaceParams struct { ID pgtype.UUID `json:"id"` WorkspaceID pgtype.UUID `json:"workspace_id"` } func (q *Queries) GetIssueInWorkspace(ctx context.Context, arg GetIssueInWorkspaceParams) (Issue, error) { row := q.db.QueryRow(ctx, getIssueInWorkspace, arg.ID, arg.WorkspaceID) var i Issue err := row.Scan( &i.ID, &i.WorkspaceID, &i.Title, &i.Description, &i.Status, &i.Priority, &i.AssigneeType, &i.AssigneeID, &i.CreatorType, &i.CreatorID, &i.ParentIssueID, &i.AcceptanceCriteria, &i.ContextRefs, &i.Position, &i.DueDate, &i.CreatedAt, &i.UpdatedAt, &i.Number, &i.ProjectID, &i.OriginType, &i.OriginID, &i.FirstExecutedAt, &i.StartDate, &i.Metadata, ) return i, err } const listChildIssues = `-- name: ListChildIssues :many SELECT id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, position, due_date, created_at, updated_at, number, project_id, origin_type, origin_id, first_executed_at, start_date, metadata FROM issue WHERE parent_issue_id = $1 ORDER BY position ASC, created_at DESC ` func (q *Queries) ListChildIssues(ctx context.Context, parentIssueID pgtype.UUID) ([]Issue, error) { rows, err := q.db.Query(ctx, listChildIssues, parentIssueID) if err != nil { return nil, err } defer rows.Close() items := []Issue{} for rows.Next() { var i Issue if err := rows.Scan( &i.ID, &i.WorkspaceID, &i.Title, &i.Description, &i.Status, &i.Priority, &i.AssigneeType, &i.AssigneeID, &i.CreatorType, &i.CreatorID, &i.ParentIssueID, &i.AcceptanceCriteria, &i.ContextRefs, &i.Position, &i.DueDate, &i.CreatedAt, &i.UpdatedAt, &i.Number, &i.ProjectID, &i.OriginType, &i.OriginID, &i.FirstExecutedAt, &i.StartDate, &i.Metadata, ); err != nil { return nil, err } items = append(items, i) } if err := rows.Err(); err != nil { return nil, err } return items, nil } const listChildrenByParents = `-- name: ListChildrenByParents :many SELECT id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, position, due_date, created_at, updated_at, number, project_id, origin_type, origin_id, first_executed_at, start_date, metadata FROM issue WHERE workspace_id = $1 AND parent_issue_id = ANY($2::uuid[]) ORDER BY parent_issue_id, position ASC, created_at DESC ` type ListChildrenByParentsParams struct { WorkspaceID pgtype.UUID `json:"workspace_id"` ParentIds []pgtype.UUID `json:"parent_ids"` } // Batched variant of ListChildIssues: returns all children for the given // parent set in one round trip. Used by Swimlane to avoid an N+1 fan-out // (one request per visible parent lane). Result is grouped client-side by // parent_issue_id; the workspace filter is also enforced so callers can't // enumerate children of parents in workspaces they don't belong to. func (q *Queries) ListChildrenByParents(ctx context.Context, arg ListChildrenByParentsParams) ([]Issue, error) { rows, err := q.db.Query(ctx, listChildrenByParents, arg.WorkspaceID, arg.ParentIds) if err != nil { return nil, err } defer rows.Close() items := []Issue{} for rows.Next() { var i Issue if err := rows.Scan( &i.ID, &i.WorkspaceID, &i.Title, &i.Description, &i.Status, &i.Priority, &i.AssigneeType, &i.AssigneeID, &i.CreatorType, &i.CreatorID, &i.ParentIssueID, &i.AcceptanceCriteria, &i.ContextRefs, &i.Position, &i.DueDate, &i.CreatedAt, &i.UpdatedAt, &i.Number, &i.ProjectID, &i.OriginType, &i.OriginID, &i.FirstExecutedAt, &i.StartDate, &i.Metadata, ); err != nil { return nil, err } items = append(items, i) } if err := rows.Err(); err != nil { return nil, err } return items, nil } const listIssues = `-- name: ListIssues :many SELECT i.id, i.workspace_id, i.title, i.description, i.status, i.priority, i.assignee_type, i.assignee_id, i.creator_type, i.creator_id, i.parent_issue_id, i.position, i.start_date, i.due_date, i.created_at, i.updated_at, i.number, i.project_id, i.metadata FROM issue i WHERE i.workspace_id = $1 AND ($4::text IS NULL OR i.status = $4) AND ($5::text IS NULL OR i.priority = $5) AND ($6::uuid IS NULL OR i.assignee_id = $6) AND ($7::uuid[] IS NULL OR i.assignee_id = ANY($7::uuid[])) AND ($8::uuid IS NULL OR i.creator_id = $8) AND ($9::uuid IS NULL OR i.project_id = $9) AND ($10::bool IS NULL OR (i.start_date IS NOT NULL OR i.due_date IS NOT NULL)) AND ($11::jsonb IS NULL OR i.metadata @> $11::jsonb) AND ( $12::uuid IS NULL -- (1) assignee is an agent owned by the user OR (i.assignee_type = 'agent' AND i.assignee_id IN ( SELECT a.id FROM agent a WHERE a.workspace_id = $1 AND a.owner_id = $12::uuid )) -- (2)(3)(4) assignee is a squad related to the user — three relations OR (i.assignee_type = 'squad' AND i.assignee_id IN ( -- (2) the user is a human member of the squad SELECT sm.squad_id FROM squad_member sm JOIN squad s ON s.id = sm.squad_id WHERE s.workspace_id = $1 AND sm.member_type = 'member' AND sm.member_id = $12::uuid UNION -- (3) the squad's canonical leader is an agent owned by the user. -- We read squad.leader_id directly rather than relying on a -- squad_member row, because the leader copy in squad_member is -- best-effort (see squad.go AddSquadMember error handling). SELECT s.id FROM squad s JOIN agent a ON a.id = s.leader_id WHERE s.workspace_id = $1 AND a.workspace_id = $1 AND a.owner_id = $12::uuid UNION -- (4) the squad has an agent member owned by the user SELECT sm.squad_id FROM squad_member sm JOIN squad s ON s.id = sm.squad_id JOIN agent a ON a.id = sm.member_id WHERE s.workspace_id = $1 AND sm.member_type = 'agent' AND a.workspace_id = $1 AND a.owner_id = $12::uuid )) ) ORDER BY i.position ASC, i.created_at DESC LIMIT $2 OFFSET $3 ` type ListIssuesParams struct { WorkspaceID pgtype.UUID `json:"workspace_id"` Limit int32 `json:"limit"` Offset int32 `json:"offset"` Status pgtype.Text `json:"status"` Priority pgtype.Text `json:"priority"` AssigneeID pgtype.UUID `json:"assignee_id"` AssigneeIds []pgtype.UUID `json:"assignee_ids"` CreatorID pgtype.UUID `json:"creator_id"` ProjectID pgtype.UUID `json:"project_id"` Scheduled pgtype.Bool `json:"scheduled"` MetadataFilter []byte `json:"metadata_filter"` InvolvesUserID pgtype.UUID `json:"involves_user_id"` } type ListIssuesRow struct { ID pgtype.UUID `json:"id"` WorkspaceID pgtype.UUID `json:"workspace_id"` Title string `json:"title"` Description pgtype.Text `json:"description"` Status string `json:"status"` Priority string `json:"priority"` AssigneeType pgtype.Text `json:"assignee_type"` AssigneeID pgtype.UUID `json:"assignee_id"` CreatorType string `json:"creator_type"` CreatorID pgtype.UUID `json:"creator_id"` ParentIssueID pgtype.UUID `json:"parent_issue_id"` Position float64 `json:"position"` StartDate pgtype.Date `json:"start_date"` DueDate pgtype.Date `json:"due_date"` CreatedAt pgtype.Timestamptz `json:"created_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"` Number int32 `json:"number"` ProjectID pgtype.UUID `json:"project_id"` Metadata []byte `json:"metadata"` } // involves_user_id widens the assignee filter to surface issues where the user // is *indirectly* the assignee — via an owned agent or a squad they belong to / // lead / have an agent inside. The semantics intentionally exclude direct // member assignment (`assignee_type='member' AND assignee_id=involves_user_id`) // because that is already the meaning of the `assignee_id` filter (tab 1 // "Assigned to me"), and the two filters must produce disjoint result sets. func (q *Queries) ListIssues(ctx context.Context, arg ListIssuesParams) ([]ListIssuesRow, error) { rows, err := q.db.Query(ctx, listIssues, arg.WorkspaceID, arg.Limit, arg.Offset, arg.Status, arg.Priority, arg.AssigneeID, arg.AssigneeIds, arg.CreatorID, arg.ProjectID, arg.Scheduled, arg.MetadataFilter, arg.InvolvesUserID, ) if err != nil { return nil, err } defer rows.Close() items := []ListIssuesRow{} for rows.Next() { var i ListIssuesRow if err := rows.Scan( &i.ID, &i.WorkspaceID, &i.Title, &i.Description, &i.Status, &i.Priority, &i.AssigneeType, &i.AssigneeID, &i.CreatorType, &i.CreatorID, &i.ParentIssueID, &i.Position, &i.StartDate, &i.DueDate, &i.CreatedAt, &i.UpdatedAt, &i.Number, &i.ProjectID, &i.Metadata, ); err != nil { return nil, err } items = append(items, i) } if err := rows.Err(); err != nil { return nil, err } return items, nil } const listOpenIssues = `-- name: ListOpenIssues :many SELECT i.id, i.workspace_id, i.title, i.description, i.status, i.priority, i.assignee_type, i.assignee_id, i.creator_type, i.creator_id, i.parent_issue_id, i.position, i.start_date, i.due_date, i.created_at, i.updated_at, i.number, i.project_id, i.metadata FROM issue i WHERE i.workspace_id = $1 AND i.status NOT IN ('done', 'cancelled') AND ($2::text IS NULL OR i.priority = $2) AND ($3::uuid IS NULL OR i.assignee_id = $3) AND ($4::uuid[] IS NULL OR i.assignee_id = ANY($4::uuid[])) AND ($5::uuid IS NULL OR i.creator_id = $5) AND ($6::uuid IS NULL OR i.project_id = $6) AND ($7::jsonb IS NULL OR i.metadata @> $7::jsonb) AND ( $8::uuid IS NULL OR (i.assignee_type = 'agent' AND i.assignee_id IN ( SELECT a.id FROM agent a WHERE a.workspace_id = $1 AND a.owner_id = $8::uuid )) OR (i.assignee_type = 'squad' AND i.assignee_id IN ( SELECT sm.squad_id FROM squad_member sm JOIN squad s ON s.id = sm.squad_id WHERE s.workspace_id = $1 AND sm.member_type = 'member' AND sm.member_id = $8::uuid UNION SELECT s.id FROM squad s JOIN agent a ON a.id = s.leader_id WHERE s.workspace_id = $1 AND a.workspace_id = $1 AND a.owner_id = $8::uuid UNION SELECT sm.squad_id FROM squad_member sm JOIN squad s ON s.id = sm.squad_id JOIN agent a ON a.id = sm.member_id WHERE s.workspace_id = $1 AND sm.member_type = 'agent' AND a.workspace_id = $1 AND a.owner_id = $8::uuid )) ) ORDER BY i.position ASC, i.created_at DESC ` type ListOpenIssuesParams struct { WorkspaceID pgtype.UUID `json:"workspace_id"` Priority pgtype.Text `json:"priority"` AssigneeID pgtype.UUID `json:"assignee_id"` AssigneeIds []pgtype.UUID `json:"assignee_ids"` CreatorID pgtype.UUID `json:"creator_id"` ProjectID pgtype.UUID `json:"project_id"` MetadataFilter []byte `json:"metadata_filter"` InvolvesUserID pgtype.UUID `json:"involves_user_id"` } type ListOpenIssuesRow struct { ID pgtype.UUID `json:"id"` WorkspaceID pgtype.UUID `json:"workspace_id"` Title string `json:"title"` Description pgtype.Text `json:"description"` Status string `json:"status"` Priority string `json:"priority"` AssigneeType pgtype.Text `json:"assignee_type"` AssigneeID pgtype.UUID `json:"assignee_id"` CreatorType string `json:"creator_type"` CreatorID pgtype.UUID `json:"creator_id"` ParentIssueID pgtype.UUID `json:"parent_issue_id"` Position float64 `json:"position"` StartDate pgtype.Date `json:"start_date"` DueDate pgtype.Date `json:"due_date"` CreatedAt pgtype.Timestamptz `json:"created_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"` Number int32 `json:"number"` ProjectID pgtype.UUID `json:"project_id"` Metadata []byte `json:"metadata"` } // See ListIssues for the semantics of involves_user_id (mirrors the 4-branch // filter; member-direct assignment is intentionally excluded). func (q *Queries) ListOpenIssues(ctx context.Context, arg ListOpenIssuesParams) ([]ListOpenIssuesRow, error) { rows, err := q.db.Query(ctx, listOpenIssues, arg.WorkspaceID, arg.Priority, arg.AssigneeID, arg.AssigneeIds, arg.CreatorID, arg.ProjectID, arg.MetadataFilter, arg.InvolvesUserID, ) if err != nil { return nil, err } defer rows.Close() items := []ListOpenIssuesRow{} for rows.Next() { var i ListOpenIssuesRow if err := rows.Scan( &i.ID, &i.WorkspaceID, &i.Title, &i.Description, &i.Status, &i.Priority, &i.AssigneeType, &i.AssigneeID, &i.CreatorType, &i.CreatorID, &i.ParentIssueID, &i.Position, &i.StartDate, &i.DueDate, &i.CreatedAt, &i.UpdatedAt, &i.Number, &i.ProjectID, &i.Metadata, ); err != nil { return nil, err } items = append(items, i) } if err := rows.Err(); err != nil { return nil, err } return items, nil } const lockIssueDuplicateKey = `-- name: LockIssueDuplicateKey :exec SELECT pg_advisory_xact_lock(hashtextextended($1::text, 0)) ` func (q *Queries) LockIssueDuplicateKey(ctx context.Context, dollar_1 string) error { _, err := q.db.Exec(ctx, lockIssueDuplicateKey, dollar_1) return err } const markIssueFirstExecuted = `-- name: MarkIssueFirstExecuted :one UPDATE issue SET first_executed_at = now() WHERE id = $1 AND first_executed_at IS NULL RETURNING id, workspace_id, creator_type, creator_id, first_executed_at ` type MarkIssueFirstExecutedRow struct { ID pgtype.UUID `json:"id"` WorkspaceID pgtype.UUID `json:"workspace_id"` CreatorType string `json:"creator_type"` CreatorID pgtype.UUID `json:"creator_id"` FirstExecutedAt pgtype.Timestamptz `json:"first_executed_at"` } // Flips first_executed_at from NULL to now() atomically. Returns the row if // this was the first time the issue was executed; no rows otherwise. The // analytics issue_executed event fires exactly when this returns a row — // retries and re-assignments hit the WHERE clause and no-op. func (q *Queries) MarkIssueFirstExecuted(ctx context.Context, id pgtype.UUID) (MarkIssueFirstExecutedRow, error) { row := q.db.QueryRow(ctx, markIssueFirstExecuted, id) var i MarkIssueFirstExecutedRow err := row.Scan( &i.ID, &i.WorkspaceID, &i.CreatorType, &i.CreatorID, &i.FirstExecutedAt, ) return i, err } const setIssueMetadataKey = `-- name: SetIssueMetadataKey :one UPDATE issue SET metadata = jsonb_set(metadata, ARRAY[$1::text], $2::jsonb), updated_at = now() WHERE id = $3 AND workspace_id = $4 RETURNING id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, position, due_date, created_at, updated_at, number, project_id, origin_type, origin_id, first_executed_at, start_date, metadata ` type SetIssueMetadataKeyParams struct { Key string `json:"key"` Value []byte `json:"value"` ID pgtype.UUID `json:"id"` WorkspaceID pgtype.UUID `json:"workspace_id"` } // SearchIssues: moved to handler (dynamic SQL for multi-word search support). // Atomically sets a single key in the issue's metadata JSONB. The // workspace_id filter is the authorization gate — handler resolves the // issue first so this is also the tenant check. func (q *Queries) SetIssueMetadataKey(ctx context.Context, arg SetIssueMetadataKeyParams) (Issue, error) { row := q.db.QueryRow(ctx, setIssueMetadataKey, arg.Key, arg.Value, arg.ID, arg.WorkspaceID, ) var i Issue err := row.Scan( &i.ID, &i.WorkspaceID, &i.Title, &i.Description, &i.Status, &i.Priority, &i.AssigneeType, &i.AssigneeID, &i.CreatorType, &i.CreatorID, &i.ParentIssueID, &i.AcceptanceCriteria, &i.ContextRefs, &i.Position, &i.DueDate, &i.CreatedAt, &i.UpdatedAt, &i.Number, &i.ProjectID, &i.OriginType, &i.OriginID, &i.FirstExecutedAt, &i.StartDate, &i.Metadata, ) return i, err } const updateIssue = `-- name: UpdateIssue :one UPDATE issue SET title = COALESCE($2, title), description = COALESCE($3, description), status = COALESCE($4, status), priority = COALESCE($5, priority), assignee_type = $6, assignee_id = $7, position = COALESCE($8, position), start_date = $9, due_date = $10, parent_issue_id = $11, project_id = $12, updated_at = now() WHERE id = $1 RETURNING id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, position, due_date, created_at, updated_at, number, project_id, origin_type, origin_id, first_executed_at, start_date, metadata ` type UpdateIssueParams struct { ID pgtype.UUID `json:"id"` Title pgtype.Text `json:"title"` Description pgtype.Text `json:"description"` Status pgtype.Text `json:"status"` Priority pgtype.Text `json:"priority"` AssigneeType pgtype.Text `json:"assignee_type"` AssigneeID pgtype.UUID `json:"assignee_id"` Position pgtype.Float8 `json:"position"` StartDate pgtype.Date `json:"start_date"` DueDate pgtype.Date `json:"due_date"` ParentIssueID pgtype.UUID `json:"parent_issue_id"` ProjectID pgtype.UUID `json:"project_id"` } func (q *Queries) UpdateIssue(ctx context.Context, arg UpdateIssueParams) (Issue, error) { row := q.db.QueryRow(ctx, updateIssue, arg.ID, arg.Title, arg.Description, arg.Status, arg.Priority, arg.AssigneeType, arg.AssigneeID, arg.Position, arg.StartDate, arg.DueDate, arg.ParentIssueID, arg.ProjectID, ) var i Issue err := row.Scan( &i.ID, &i.WorkspaceID, &i.Title, &i.Description, &i.Status, &i.Priority, &i.AssigneeType, &i.AssigneeID, &i.CreatorType, &i.CreatorID, &i.ParentIssueID, &i.AcceptanceCriteria, &i.ContextRefs, &i.Position, &i.DueDate, &i.CreatedAt, &i.UpdatedAt, &i.Number, &i.ProjectID, &i.OriginType, &i.OriginID, &i.FirstExecutedAt, &i.StartDate, &i.Metadata, ) return i, err } const updateIssueStatus = `-- name: UpdateIssueStatus :one UPDATE issue SET status = $2, updated_at = now() WHERE id = $1 AND workspace_id = $3 RETURNING id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, position, due_date, created_at, updated_at, number, project_id, origin_type, origin_id, first_executed_at, start_date, metadata ` type UpdateIssueStatusParams struct { ID pgtype.UUID `json:"id"` Status string `json:"status"` WorkspaceID pgtype.UUID `json:"workspace_id"` } // Workspace_id in the WHERE clause is a SQL-layer tenant guard; see DeleteIssue. func (q *Queries) UpdateIssueStatus(ctx context.Context, arg UpdateIssueStatusParams) (Issue, error) { row := q.db.QueryRow(ctx, updateIssueStatus, arg.ID, arg.Status, arg.WorkspaceID) var i Issue err := row.Scan( &i.ID, &i.WorkspaceID, &i.Title, &i.Description, &i.Status, &i.Priority, &i.AssigneeType, &i.AssigneeID, &i.CreatorType, &i.CreatorID, &i.ParentIssueID, &i.AcceptanceCriteria, &i.ContextRefs, &i.Position, &i.DueDate, &i.CreatedAt, &i.UpdatedAt, &i.Number, &i.ProjectID, &i.OriginType, &i.OriginID, &i.FirstExecutedAt, &i.StartDate, &i.Metadata, ) return i, err }