Files
multica/server/pkg/db/generated/issue.sql.go
Bohan Jiang 54368fd826 feat(projects): scheduled-only Gantt data source + WS reactivity (MUL-1881) (#2856)
* feat(projects): scheduled-only Gantt data source + WS reactivity (MUL-1881)

Project Gantt now fetches its own scheduled-only data instead of riding the
Board/List pagination cache. The Unscheduled drawer and pagination warning
banner are gone, and any WS-driven issue change (create / update / delete)
invalidates the new cache so the timeline stays live.

- Backend: `GET /api/issues?scheduled=true` adds an
  `(i.start_date IS NOT NULL OR i.due_date IS NOT NULL)` predicate on both
  ListIssues and CountIssues. New SQL filter is plumbed through sqlc + handler.
- Frontend: new `projectGanttIssuesOptions(wsId, projectId)` issues a single
  fetch and lives under its own cache key. WS handlers and mutations
  invalidate the prefix on create/update/delete so the bar reacts to
  start_date / due_date changes from other tabs and from this tab without
  waiting on the WS round-trip.
- GanttView: drops the Unscheduled section, the pagination warning banner,
  and the load-all button; renders only scheduled rows.
- Removes now-dead `useLoadAllRemaining`, `myIssueListPaginationOptions`,
  `summarizeIssueListPagination`, and the gantt locale strings that
  supported the old plumbing.

Co-authored-by: multica-agent <github@multica.ai>

* fix(projects): page through Gantt fetch and isolate per-view data sources

- Walk paginated `scheduled=true` issues until total is reached so projects
  with more than 500 scheduled bars no longer silently truncate.
- Gantt mode disables the bucketed Board/List query and reads its own
  scheduled cache for the project empty-state check, so the page never
  short-circuits Gantt with a Board-derived "no issues" CTA.
- `onIssueLabelsChanged` patches matching rows in the Project Gantt cache
  in-place, keeping label filters consistent after attach/detach from
  other tabs or agents.

MUL-1881

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-05-19 17:04:16 +08:00

1046 lines
31 KiB
Go

// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// 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::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 = $9::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 = $9::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 = $9::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 = $9::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"`
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.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
`
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.Timestamptz `json:"start_date"`
DueDate pgtype.Timestamptz `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,
)
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
`
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.Timestamptz `json:"start_date"`
DueDate pgtype.Timestamptz `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,
)
return i, err
}
const deleteIssue = `-- name: DeleteIssue :exec
DELETE FROM issue WHERE id = $1
`
func (q *Queries) DeleteIssue(ctx context.Context, id pgtype.UUID) error {
_, err := q.db.Exec(ctx, deleteIssue, id)
return 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 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,
)
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 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,
)
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 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,
)
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 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,
)
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 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,
)
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 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,
); 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
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::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 = $11::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 = $11::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 = $11::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 = $11::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"`
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.Timestamptz `json:"start_date"`
DueDate pgtype.Timestamptz `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"`
}
// 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.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,
); 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
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::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 = $7::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 = $7::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 = $7::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 = $7::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"`
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.Timestamptz `json:"start_date"`
DueDate pgtype.Timestamptz `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"`
}
// 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.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,
); 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"`
}
// SearchIssues: moved to handler (dynamic SQL for multi-word search support).
// 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 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
`
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.Timestamptz `json:"start_date"`
DueDate pgtype.Timestamptz `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,
)
return i, err
}
const updateIssueStatus = `-- name: UpdateIssueStatus :one
UPDATE issue SET
status = $2,
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
`
type UpdateIssueStatusParams struct {
ID pgtype.UUID `json:"id"`
Status string `json:"status"`
}
func (q *Queries) UpdateIssueStatus(ctx context.Context, arg UpdateIssueStatusParams) (Issue, error) {
row := q.db.QueryRow(ctx, updateIssueStatus, arg.ID, arg.Status)
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,
)
return i, err
}