// 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 = ANY($2::text[]))::bigint AS done FROM issue WHERE workspace_id = $1 AND parent_issue_id IS NOT NULL GROUP BY parent_issue_id ` type ChildIssueProgressParams struct { WorkspaceID pgtype.UUID `json:"workspace_id"` TerminalStatusKeys []string `json:"terminal_status_keys"` } 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, arg ChildIssueProgressParams) ([]ChildIssueProgressRow, error) { rows, err := q.db.Query(ctx, childIssueProgress, arg.WorkspaceID, arg.TerminalStatusKeys) 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 countIssuesUpTo = `-- name: CountIssuesUpTo :one SELECT COUNT(*)::bigint FROM ( SELECT 1 FROM issue WHERE workspace_id = $1 LIMIT $2::bigint ) bounded_issues ` type CountIssuesUpToParams struct { WorkspaceID pgtype.UUID `json:"workspace_id"` Limit int64 `json:"limit"` } // Bounded count for issue-limit admission and display. Callers pass only the // threshold needed for their decision, avoiding a full scan in an oversized // workspace. func (q *Queries) CountIssuesUpTo(ctx context.Context, arg CountIssuesUpToParams) (int64, error) { row := q.db.QueryRow(ctx, countIssuesUpTo, arg.WorkspaceID, arg.Limit) var column_1 int64 err := row.Scan(&column_1) return column_1, 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, stage, last_activity_at, id ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, now(), COALESCE($17::uuid, gen_random_uuid()) ) 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, stage, properties, revision, last_activity_at ` 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"` Stage pgtype.Int4 `json:"stage"` ID pgtype.UUID `json:"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, arg.Stage, arg.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, &i.Stage, &i.Properties, &i.Revision, &i.LastActivityAt, ) 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, stage, last_activity_at, id ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, now(), COALESCE($19::uuid, gen_random_uuid()) ) 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, stage, properties, revision, last_activity_at ` 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"` Stage pgtype.Int4 `json:"stage"` ID pgtype.UUID `json:"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, arg.Stage, arg.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, &i.Stage, &i.Properties, &i.Revision, &i.LastActivityAt, ) return i, err } const deleteIssue = `-- name: DeleteIssue :exec WITH target AS ( SELECT issue.id FROM issue WHERE issue.id = $1 AND issue.workspace_id = $2 ), cleared_vcs_pr_links AS ( DELETE FROM issue_vcs_pull_request WHERE issue_id IN (SELECT target.id FROM target) ) DELETE FROM issue WHERE issue.id IN (SELECT target.id FROM target) ` 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. // // issue_vcs_pull_request (migration 213) has no FK to issue, so the link rows // are not cascaded away. Sweep them here so they go atomically with the issue. // The mirrored PR rows themselves belong to the connection, not the issue, so // they persist (matching the GitHub link behaviour). // // The sweep MUST route through the same workspace-checked target as the issue // delete: deleting links by bare issue_id would drop another tenant's link rows // when a caller passes a foreign issue_id with its own workspace_id (the issue // itself is correctly untouched, but the links are already gone) — the exact // cross-tenant leak the #1661 guard above exists to prevent. 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, revision = revision + CASE WHEN metadata ? $1::text THEN 1 ELSE 0 END, last_activity_at = CASE WHEN metadata ? $1::text THEN GREATEST(COALESCE(last_activity_at, updated_at), now()) ELSE last_activity_at END, 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, stage, properties, revision, last_activity_at ` 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, &i.Stage, &i.Properties, &i.Revision, &i.LastActivityAt, ) return i, err } const detachDirectChildIssues = `-- name: DetachDirectChildIssues :many UPDATE issue SET parent_issue_id = NULL, stage = NULL, revision = revision + 1, updated_at = now(), last_activity_at = GREATEST(COALESCE(last_activity_at, updated_at), now()) WHERE workspace_id = $1 AND parent_issue_id = $2 AND NOT COALESCE(id = ANY($3::uuid[]), false) 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, stage, properties, revision, last_activity_at ` type DetachDirectChildIssuesParams struct { WorkspaceID pgtype.UUID `json:"workspace_id"` ParentIssueID pgtype.UUID `json:"parent_issue_id"` ExcludedIssueIds []pgtype.UUID `json:"excluded_issue_ids"` } func (q *Queries) DetachDirectChildIssues(ctx context.Context, arg DetachDirectChildIssuesParams) ([]Issue, error) { rows, err := q.db.Query(ctx, detachDirectChildIssues, arg.WorkspaceID, arg.ParentIssueID, arg.ExcludedIssueIds) 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, &i.Stage, &i.Properties, &i.Revision, &i.LastActivityAt, ); err != nil { return nil, err } items = append(items, i) } if err := rows.Err(); err != nil { return nil, err } return items, nil } 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, stage, properties, revision, last_activity_at FROM issue WHERE workspace_id = $1 -- Negate only known terminal keys so an unknown legacy key remains active. AND NOT (status = ANY($2::text[])) AND project_id IS NOT DISTINCT FROM $3::uuid AND parent_issue_id IS NOT DISTINCT FROM $4::uuid AND lower(btrim(regexp_replace(title, '[[:space:]]+', ' ', 'g'))) = $5 ORDER BY created_at ASC LIMIT 1 ` type FindActiveDuplicateIssueParams struct { WorkspaceID pgtype.UUID `json:"workspace_id"` TerminalStatusKeys []string `json:"terminal_status_keys"` 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.TerminalStatusKeys, 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, &i.Stage, &i.Properties, &i.Revision, &i.LastActivityAt, ) return i, err } const findRecentAutopilotDuplicateIssue = `-- name: FindRecentAutopilotDuplicateIssue :one 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.acceptance_criteria, i.context_refs, i.position, i.due_date, i.created_at, i.updated_at, i.number, i.project_id, i.origin_type, i.origin_id, i.first_executed_at, i.start_date, i.metadata, i.stage, i.properties, i.revision, i.last_activity_at FROM issue i WHERE i.workspace_id = $1 -- Negate only known terminal keys so an unknown legacy key remains active. AND NOT (i.status = ANY($3::text[])) AND i.origin_type = 'autopilot' AND i.origin_id = $2 AND i.project_id IS NOT DISTINCT FROM $4::uuid AND lower(btrim(regexp_replace(i.title, '[[:space:]]+', ' ', 'g'))) = $5 AND i.created_at >= $6::timestamptz AND EXISTS ( SELECT 1 FROM autopilot_run r WHERE r.issue_id = i.id AND r.autopilot_id = i.origin_id AND r.status IN ('issue_created', 'running', 'completed') ) ORDER BY i.created_at ASC LIMIT 1 ` type FindRecentAutopilotDuplicateIssueParams struct { WorkspaceID pgtype.UUID `json:"workspace_id"` OriginID pgtype.UUID `json:"origin_id"` TerminalStatusKeys []string `json:"terminal_status_keys"` ProjectID pgtype.UUID `json:"project_id"` NormalizedTitle string `json:"normalized_title"` CreatedAfter pgtype.Timestamptz `json:"created_after"` } func (q *Queries) FindRecentAutopilotDuplicateIssue(ctx context.Context, arg FindRecentAutopilotDuplicateIssueParams) (Issue, error) { row := q.db.QueryRow(ctx, findRecentAutopilotDuplicateIssue, arg.WorkspaceID, arg.OriginID, arg.TerminalStatusKeys, arg.ProjectID, arg.NormalizedTitle, arg.CreatedAfter, ) 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, &i.Stage, &i.Properties, &i.Revision, &i.LastActivityAt, ) 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, stage, properties, revision, last_activity_at 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, &i.Stage, &i.Properties, &i.Revision, &i.LastActivityAt, ) 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, stage, properties, revision, last_activity_at 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, &i.Stage, &i.Properties, &i.Revision, &i.LastActivityAt, ) 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, stage, properties, revision, last_activity_at 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, &i.Stage, &i.Properties, &i.Revision, &i.LastActivityAt, ) return i, err } const getIssueGCStatus = `-- name: GetIssueGCStatus :one SELECT workspace_id, status, updated_at FROM issue WHERE id = $1 ` type GetIssueGCStatusRow struct { WorkspaceID pgtype.UUID `json:"workspace_id"` Status string `json:"status"` UpdatedAt pgtype.Timestamptz `json:"updated_at"` } func (q *Queries) GetIssueGCStatus(ctx context.Context, id pgtype.UUID) (GetIssueGCStatusRow, error) { row := q.db.QueryRow(ctx, getIssueGCStatus, id) var i GetIssueGCStatusRow err := row.Scan(&i.WorkspaceID, &i.Status, &i.UpdatedAt) 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, stage, properties, revision, last_activity_at 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, &i.Stage, &i.Properties, &i.Revision, &i.LastActivityAt, ) 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, stage, properties, revision, last_activity_at FROM issue WHERE parent_issue_id = $1 ORDER BY number ASC ` // Order by number ASC so sub-issues display in stable creation order // (oldest first), matching how a parent's plan reads top-to-bottom. The // position column is computed per-(workspace, status) by NextTopPosition, // not relative to siblings, so ordering by it interleaves children // unpredictably across batches and statuses; number is a per-workspace // monotonic counter and is sibling-stable. 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, &i.Stage, &i.Properties, &i.Revision, &i.LastActivityAt, ); 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, stage, properties, revision, last_activity_at FROM issue WHERE workspace_id = $1 AND parent_issue_id = ANY($2::uuid[]) ORDER BY parent_issue_id, number ASC ` 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. // Within each parent, order by number ASC for the same sibling-stable // creation order as ListChildIssues. 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, &i.Stage, &i.Properties, &i.Revision, &i.LastActivityAt, ); err != nil { return nil, err } items = append(items, i) } if err := rows.Err(); err != nil { return nil, err } return items, nil } const listIssueGCStatuses = `-- name: ListIssueGCStatuses :many SELECT id, status, updated_at FROM issue WHERE workspace_id = $1 AND id = ANY($2::uuid[]) ` type ListIssueGCStatusesParams struct { WorkspaceID pgtype.UUID `json:"workspace_id"` IssueIds []pgtype.UUID `json:"issue_ids"` } type ListIssueGCStatusesRow struct { ID pgtype.UUID `json:"id"` Status string `json:"status"` UpdatedAt pgtype.Timestamptz `json:"updated_at"` } func (q *Queries) ListIssueGCStatuses(ctx context.Context, arg ListIssueGCStatusesParams) ([]ListIssueGCStatusesRow, error) { rows, err := q.db.Query(ctx, listIssueGCStatuses, arg.WorkspaceID, arg.IssueIds) if err != nil { return nil, err } defer rows.Close() items := []ListIssueGCStatusesRow{} for rows.Next() { var i ListIssueGCStatusesRow if err := rows.Scan(&i.ID, &i.Status, &i.UpdatedAt); 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.last_activity_at, i.number, i.project_id, i.metadata, i.stage, i.properties, i.revision 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"` LastActivityAt pgtype.Timestamptz `json:"last_activity_at"` Number int32 `json:"number"` ProjectID pgtype.UUID `json:"project_id"` Metadata []byte `json:"metadata"` Stage pgtype.Int4 `json:"stage"` Properties []byte `json:"properties"` Revision int64 `json:"revision"` } // 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.LastActivityAt, &i.Number, &i.ProjectID, &i.Metadata, &i.Stage, &i.Properties, &i.Revision, ); 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.last_activity_at, i.number, i.project_id, i.metadata, i.stage, i.properties, i.revision FROM issue i WHERE i.workspace_id = $1 -- Negate only known terminal keys so an unknown legacy key remains visible. AND NOT (i.status = ANY($2::text[])) 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::jsonb IS NULL OR i.metadata @> $8::jsonb) -- properties_filter is a jsonb array of groups, each group an array of -- patterns (built by parsePropertiesFilterParam): the issue must match at -- least one pattern from EVERY group (AND of ORs). Three pattern shapes: -- {"__none__": ""} — "no value" marker; -- {"": , ...} — containment; -- {"__op__": "", "def": "", "value": ""} — scalar operator -- (contains / gt / gte / lt / lte / before / after), where the contains -- value arrives already ILIKE-escaped and the op was validated in Go, so -- the per-op branches below only ever see legal values. A contains pattern -- additionally carries "prefilter" when its needle survives jsonb text -- serialization intact; the clause it enables is redundant by construction -- (prefilterableContainsNeedle in property.go) and stays here so both renderings -- of the filter keep matching row for row. The correlated form skips the GIN -- index, which is fine here: open_only is an unpaginated workspace scan. The -- terminal-status predicate intentionally remains a filter rather than a -- positive index narrowing so unknown legacy status keys stay visible. AND ( $9::jsonb IS NULL OR NOT EXISTS ( SELECT 1 FROM jsonb_array_elements($9::jsonb) AS pf(alternatives) WHERE NOT EXISTS ( SELECT 1 FROM jsonb_array_elements(pf.alternatives) AS alt(pattern) WHERE (alt.pattern ? '__none__' AND NOT (i.properties ? (alt.pattern ->> '__none__'))) OR (NOT (alt.pattern ? '__none__') AND NOT (alt.pattern ? '__op__') AND i.properties @> alt.pattern) OR (alt.pattern ? '__op__' AND i.properties ->> (alt.pattern ->> 'def') IS NOT NULL AND ( (alt.pattern ->> '__op__' = 'contains' AND (NOT (alt.pattern ? 'prefilter') OR LOWER(i.properties::text) LIKE LOWER('%' || (alt.pattern ->> 'prefilter') || '%')) AND jsonb_typeof(i.properties -> (alt.pattern ->> 'def')) = 'string' AND (i.properties ->> (alt.pattern ->> 'def')) ILIKE '%' || (alt.pattern ->> 'value') || '%') OR (alt.pattern ->> '__op__' = 'gt' AND CASE WHEN jsonb_typeof(i.properties -> (alt.pattern ->> 'def')) = 'number' THEN (i.properties ->> (alt.pattern ->> 'def'))::numeric END > (alt.pattern ->> 'value')::numeric) OR (alt.pattern ->> '__op__' = 'gte' AND CASE WHEN jsonb_typeof(i.properties -> (alt.pattern ->> 'def')) = 'number' THEN (i.properties ->> (alt.pattern ->> 'def'))::numeric END >= (alt.pattern ->> 'value')::numeric) OR (alt.pattern ->> '__op__' = 'lt' AND CASE WHEN jsonb_typeof(i.properties -> (alt.pattern ->> 'def')) = 'number' THEN (i.properties ->> (alt.pattern ->> 'def'))::numeric END < (alt.pattern ->> 'value')::numeric) OR (alt.pattern ->> '__op__' = 'lte' AND CASE WHEN jsonb_typeof(i.properties -> (alt.pattern ->> 'def')) = 'number' THEN (i.properties ->> (alt.pattern ->> 'def'))::numeric END <= (alt.pattern ->> 'value')::numeric) OR (alt.pattern ->> '__op__' = 'before' AND jsonb_typeof(i.properties -> (alt.pattern ->> 'def')) = 'string' AND i.properties ->> (alt.pattern ->> 'def') < (alt.pattern ->> 'value')) OR (alt.pattern ->> '__op__' = 'after' AND jsonb_typeof(i.properties -> (alt.pattern ->> 'def')) = 'string' AND i.properties ->> (alt.pattern ->> 'def') > (alt.pattern ->> 'value')) )) ) ) ) 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 )) ) ORDER BY i.position ASC, i.created_at DESC ` type ListOpenIssuesParams struct { WorkspaceID pgtype.UUID `json:"workspace_id"` TerminalStatusKeys []string `json:"terminal_status_keys"` 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"` PropertiesFilter []byte `json:"properties_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"` LastActivityAt pgtype.Timestamptz `json:"last_activity_at"` Number int32 `json:"number"` ProjectID pgtype.UUID `json:"project_id"` Metadata []byte `json:"metadata"` Stage pgtype.Int4 `json:"stage"` Properties []byte `json:"properties"` Revision int64 `json:"revision"` } // 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.TerminalStatusKeys, arg.Priority, arg.AssigneeID, arg.AssigneeIds, arg.CreatorID, arg.ProjectID, arg.MetadataFilter, arg.PropertiesFilter, 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.LastActivityAt, &i.Number, &i.ProjectID, &i.Metadata, &i.Stage, &i.Properties, &i.Revision, ); 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 lockIssueForChannelMediaBind = `-- name: LockIssueForChannelMediaBind :one SELECT id FROM issue WHERE id = $1 AND workspace_id = $2 FOR KEY SHARE ` type LockIssueForChannelMediaBindParams struct { ID pgtype.UUID `json:"id"` WorkspaceID pgtype.UUID `json:"workspace_id"` } // Channel media resolves after /issue creation. Hold a key-share lock while // the attachment row is written so a concurrent issue delete cannot land // between the workspace-scoped validation and the attachment insert. func (q *Queries) LockIssueForChannelMediaBind(ctx context.Context, arg LockIssueForChannelMediaBindParams) (pgtype.UUID, error) { row := q.db.QueryRow(ctx, lockIssueForChannelMediaBind, arg.ID, arg.WorkspaceID) var id pgtype.UUID err := row.Scan(&id) return id, err } const lockIssueForDelete = `-- name: LockIssueForDelete :one SELECT id FROM issue WHERE id = $1 AND workspace_id = $2 FOR UPDATE ` type LockIssueForDeleteParams struct { ID pgtype.UUID `json:"id"` WorkspaceID pgtype.UUID `json:"workspace_id"` } // Issue deletion must collect every attachment URL after it has won the same // row-lock race used by channel media binding. FOR UPDATE conflicts with the // binder's FOR KEY SHARE: either bind commits first and its URL is collected, // or delete commits first and the binder leaves its durable intent for cleanup. func (q *Queries) LockIssueForDelete(ctx context.Context, arg LockIssueForDeleteParams) (pgtype.UUID, error) { row := q.db.QueryRow(ctx, lockIssueForDelete, arg.ID, arg.WorkspaceID) var id pgtype.UUID err := row.Scan(&id) return id, err } const lockIssueForDescriptionUpdate = `-- name: LockIssueForDescriptionUpdate :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, stage, properties, revision, last_activity_at FROM issue WHERE id = $1 AND workspace_id = $2 FOR UPDATE ` type LockIssueForDescriptionUpdateParams struct { ID pgtype.UUID `json:"id"` WorkspaceID pgtype.UUID `json:"workspace_id"` } // Serialize field-baseline checks and combined attachment binding on the // owner row. The handler merges channel media that landed after the editor's // submitted base, updates the issue, and binds this request's attachments in // the same transaction while holding this lock. func (q *Queries) LockIssueForDescriptionUpdate(ctx context.Context, arg LockIssueForDescriptionUpdateParams) (Issue, error) { row := q.db.QueryRow(ctx, lockIssueForDescriptionUpdate, 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, &i.Stage, &i.Properties, &i.Revision, &i.LastActivityAt, ) return i, 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 materializeIssueChannelMediaMarkdown = `-- name: MaterializeIssueChannelMediaMarkdown :one UPDATE issue SET description = CASE WHEN $1::text IS NOT NULL AND COALESCE(description, '') = $1::text THEN $2::text WHEN description IS NULL OR description = '' THEN $3 ELSE description || E'\n\n' || $3 END, revision = revision + 1, updated_at = now() WHERE id = $4 AND workspace_id = $5 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, stage, properties, revision, last_activity_at ` type MaterializeIssueChannelMediaMarkdownParams struct { BaseDescription pgtype.Text `json:"base_description"` Description string `json:"description"` Markdown pgtype.Text `json:"markdown"` ID pgtype.UUID `json:"id"` WorkspaceID pgtype.UUID `json:"workspace_id"` } // Detached channel media resolves after /issue creation. When the description // still equals the exact creation-time base, replace its inline placeholders // with the fully composed Markdown so rich-text ordering survives. If a user // edited concurrently (or the adapter has no inline layout), append instead; // preserving user-authored bytes takes precedence over layout fidelity. // This is asynchronous system materialization, not a new user action, so it // intentionally preserves last_activity_at while still advancing revision. func (q *Queries) MaterializeIssueChannelMediaMarkdown(ctx context.Context, arg MaterializeIssueChannelMediaMarkdownParams) (Issue, error) { row := q.db.QueryRow(ctx, materializeIssueChannelMediaMarkdown, arg.BaseDescription, arg.Description, arg.Markdown, 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, &i.Stage, &i.Properties, &i.Revision, &i.LastActivityAt, ) return i, err } const setIssueMetadataKey = `-- name: SetIssueMetadataKey :one UPDATE issue SET metadata = jsonb_set(metadata, ARRAY[$1::text], $2::jsonb), revision = revision + CASE WHEN metadata -> $1::text IS DISTINCT FROM $2::jsonb THEN 1 ELSE 0 END, last_activity_at = CASE WHEN metadata -> $1::text IS DISTINCT FROM $2::jsonb THEN GREATEST(COALESCE(last_activity_at, updated_at), now()) ELSE last_activity_at END, 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, stage, properties, revision, last_activity_at ` 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, &i.Stage, &i.Properties, &i.Revision, &i.LastActivityAt, ) return i, err } const updateIssue = `-- name: UpdateIssue :one WITH candidate AS ( 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.acceptance_criteria, i.context_refs, i.position, i.due_date, i.created_at, i.updated_at, i.number, i.project_id, i.origin_type, i.origin_id, i.first_executed_at, i.start_date, i.metadata, i.stage, i.properties, i.revision, i.last_activity_at, COALESCE($3::text, i.title) AS next_title, COALESCE($4::text, i.description) AS next_description, COALESCE($5::text, i.status) AS next_status, COALESCE($6::text, i.priority) AS next_priority, $7::text AS next_assignee_type, $8::uuid AS next_assignee_id, CASE -- An explicit position wins. Cross-column drag-and-drop sends -- status and position together and means the slot it dropped on. WHEN $9::double precision IS NOT NULL THEN $9::double precision -- position ranks an issue *within* its (workspace, status) -- column, so it stops meaning anything the moment the column -- changes: the value that put the issue on top of Todo lands it -- below every hand-dragged issue in Done. Re-rank to the top of -- the destination, the same policy new issues get from -- NextTopPosition. Keep the two in sync. -- -- Two status changes running at once can read the same MIN and -- claim one slot. Every position-ordered query carries a unique -- final key (created_at DESC, id DESC), so a tie leaves the -- relative order of simultaneous moves arbitrary but never -- unstable across pages. Creation avoids the tie by computing its -- min under the workspace counter lock; a status change holds no -- such lock and is not worth taking one for. WHEN i.status IS DISTINCT FROM COALESCE($5::text, i.status) THEN ( SELECT COALESCE(MIN(target.position), 0) - 1 FROM issue AS target WHERE target.workspace_id = i.workspace_id AND target.status = $5::text ) ELSE i.position END AS next_position, $10::date AS next_start_date, $11::date AS next_due_date, $12::uuid AS next_parent_issue_id, $13::uuid AS next_project_id, $14::integer AS next_stage FROM issue AS i WHERE i.id = $1 AND ($2::bigint IS NULL OR i.revision = $2::bigint) ), changed AS ( SELECT candidate.id, candidate.workspace_id, candidate.title, candidate.description, candidate.status, candidate.priority, candidate.assignee_type, candidate.assignee_id, candidate.creator_type, candidate.creator_id, candidate.parent_issue_id, candidate.acceptance_criteria, candidate.context_refs, candidate.position, candidate.due_date, candidate.created_at, candidate.updated_at, candidate.number, candidate.project_id, candidate.origin_type, candidate.origin_id, candidate.first_executed_at, candidate.start_date, candidate.metadata, candidate.stage, candidate.properties, candidate.revision, candidate.last_activity_at, candidate.next_title, candidate.next_description, candidate.next_status, candidate.next_priority, candidate.next_assignee_type, candidate.next_assignee_id, candidate.next_position, candidate.next_start_date, candidate.next_due_date, candidate.next_parent_issue_id, candidate.next_project_id, candidate.next_stage, ROW( title, description, status, priority, assignee_type, assignee_id, position, start_date, due_date, parent_issue_id, project_id, stage ) IS DISTINCT FROM ROW( next_title, next_description, next_status, next_priority, next_assignee_type, next_assignee_id, next_position, next_start_date, next_due_date, next_parent_issue_id, next_project_id, next_stage ) AS did_change, ROW( title, description, status, priority, assignee_type, assignee_id, start_date, due_date, parent_issue_id, project_id, stage ) IS DISTINCT FROM ROW( next_title, next_description, next_status, next_priority, next_assignee_type, next_assignee_id, next_start_date, next_due_date, next_parent_issue_id, next_project_id, next_stage ) AS did_activity FROM candidate ) UPDATE issue AS i SET title = changed.next_title, description = changed.next_description, status = changed.next_status, priority = changed.next_priority, assignee_type = changed.next_assignee_type, assignee_id = changed.next_assignee_id, position = changed.next_position, start_date = changed.next_start_date, due_date = changed.next_due_date, parent_issue_id = changed.next_parent_issue_id, project_id = changed.next_project_id, stage = changed.next_stage, revision = i.revision + changed.did_change::integer, last_activity_at = CASE WHEN changed.did_activity THEN GREATEST(COALESCE(i.last_activity_at, i.updated_at), now()) ELSE i.last_activity_at END, updated_at = CASE WHEN changed.did_change THEN now() ELSE i.updated_at END FROM changed WHERE i.id = changed.id -- Re-check the precondition on the row version that UPDATE actually locks. -- Under READ COMMITTED, concurrent statements may both populate candidate -- from the same snapshot; EvalPlanQual re-evaluates this target-row predicate -- after waiting for the first writer, leaving the stale writer with 0 rows. AND ($2::bigint IS NULL OR i.revision = $2::bigint) RETURNING 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.acceptance_criteria, i.context_refs, i.position, i.due_date, i.created_at, i.updated_at, i.number, i.project_id, i.origin_type, i.origin_id, i.first_executed_at, i.start_date, i.metadata, i.stage, i.properties, i.revision, i.last_activity_at ` type UpdateIssueParams struct { ID pgtype.UUID `json:"id"` ExpectedRevision pgtype.Int8 `json:"expected_revision"` 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"` Stage pgtype.Int4 `json:"stage"` } func (q *Queries) UpdateIssue(ctx context.Context, arg UpdateIssueParams) (Issue, error) { row := q.db.QueryRow(ctx, updateIssue, arg.ID, arg.ExpectedRevision, arg.Title, arg.Description, arg.Status, arg.Priority, arg.AssigneeType, arg.AssigneeID, arg.Position, arg.StartDate, arg.DueDate, arg.ParentIssueID, arg.ProjectID, arg.Stage, ) 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, &i.Stage, &i.Properties, &i.Revision, &i.LastActivityAt, ) return i, err } const updateIssueStatus = `-- name: UpdateIssueStatus :one UPDATE issue AS i SET status = $2, position = CASE WHEN i.status IS DISTINCT FROM $2 THEN ( SELECT COALESCE(MIN(target.position), 0) - 1 FROM issue AS target WHERE target.workspace_id = i.workspace_id AND target.status = $2 ) ELSE i.position END, revision = i.revision + CASE WHEN i.status IS DISTINCT FROM $2 THEN 1 ELSE 0 END, last_activity_at = CASE WHEN i.status IS DISTINCT FROM $2 THEN GREATEST(COALESCE(i.last_activity_at, i.updated_at), now()) ELSE i.last_activity_at END, updated_at = now() WHERE i.id = $1 AND i.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, stage, properties, revision, last_activity_at ` 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. // Repositioning lives here rather than in the callers (GitHub sync, agent task // completion) so a status write cannot land without one: an issue carrying its // old column's rank into a new column is the bug this guards against. See the // next_position CASE in UpdateIssue for the policy. 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, &i.Stage, &i.Properties, &i.Revision, &i.LastActivityAt, ) return i, err }