Files
multica/server/pkg/db/generated/inbox.sql.go
T
446080bd57 MUL-6243 feat(issue-status): per-workspace custom issue statuses over 7 canonical categories (#7065)
* feat(issue-status): per-workspace custom issue statuses over 7 canonical categories (MUL-6243)

Co-authored-by: multica-agent <[email protected]>

* fix(issue-status): address review — normalize all status consumers, close archive race, gate rollout, split PK index

Review round 1 (MUL-6243 / PR #7065):

1. Behavior normalization was incomplete. Adds issue_effective_status() as a
   SQL mirror of issuestatus.Effective so the six SQL consumers the Go resolver
   cannot reach (duplicate guard, open-issue list, sub-issue progress, project
   stats, completed-inbox archive, open_only) resolve categories too. Wires the
   resolver into the task-failure sweeper and the stage-barrier helpers.

2. Archive TOCTOU. Archive now runs its in-use census and the archive itself in
   one transaction under an exclusive catalog lock; writes that target a custom
   status take the shared side. Built-in writes stay lock-free — a built-in can
   never be archived.

3. Rollout safety. Creating a custom status is gated behind the default-off
   CustomIssueStatuses flag, so a value old pods cannot interpret cannot exist
   until the fleet is updated. Readers ship ungated and are safe.

4. Primary key index. Follows the repo convention (327-329): table without a
   PK, unique index built CONCURRENTLY in its own migration, attached via
   PRIMARY KEY USING INDEX, and registered for invalid-index cleanup.

5. Status normalization vs stored value. Write paths now persist the canonical
   key from Resolve, so 'HUMAN_REVIEW' no longer validates and then fails the
   column format constraint as a 500.

Also fixes the two CLI tests failing in CI: they asserted the pre-MUL-6243
contract that an unknown-but-well-formed status is rejected locally.

Co-authored-by: multica-agent <[email protected]>

* fix(issue-status): close the archive race in both orderings; resolve categories in batch barrier and CLI progress

Review round 2 (MUL-6243 / PR #7065):

1. The archive race was only half closed. Handlers resolved the status BEFORE
   opening a transaction, so an archive could commit in that window and the
   write would still land on an archived key. assertIssueStatusStillActive now
   takes the shared catalog lock and RE-RESOLVES inside the caller's
   transaction, which is what actually makes the status provably active at
   write time. Applied to create, update, batch, and both description-merge
   branches, which previously bypassed the guard entirely. A losing writer gets
   409 rather than a silent partial update.

2. The batch stage-barrier prefilter still compared literals, so a batch that
   moved the last child onto a custom done status left childDoneCompleted empty
   and skipped the parent notification. It now resolves categories first.

3. multica issue children --output json counted only literal done/cancelled.
   ListChildIssues now emits status_category and the CLI reads it, falling back
   to status for older backends.

TestArchiveCommitsInsideTheWriteRaceWindow interleaves the two operations so the
writer validates against an active status and the archive commits before the
write. Verified it fails without the re-resolve (issue stranded on the archived
status) and passes with it.

Co-authored-by: multica-agent <[email protected]>

* refactor(issue-status): address review nits — honest race-test naming, one catalog read per list

Review round 3 nits (MUL-6243 / PR #7065):

1. TestArchiveRaceBothOrderings claimed more than it proved. Its 'archive wins'
   cases archived first and only then sent the request, which the pre-flight
   check alone rejects. Split into TestWritesRejectAnArchivedStatus (the
   sequential case, named for what it is) and TestArchiveRefusesAfterACommitted
   Write, and extended TestArchiveCommitsInsideTheWriteRaceWindow — the only
   test that truly interleaves archive between pre-flight and write — from
   create alone to all five write endpoints. Re-verified by deleting the
   in-transaction re-resolve: 4 of 5 fail with issues stranded on the archived
   status (create is guarded in the service layer and checked separately).

2. ListChildIssues / ListChildrenByParents issued one category lookup per
   custom-status child. issuestatus.Resolver now amortizes that to a single
   catalog read per request, lazily on the first custom key, so built-in
   statuses still cost zero queries however long the list is.

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-17 18:32:02 +08:00

660 lines
20 KiB
Go

// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: inbox.sql
package db
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const archiveAllInbox = `-- name: ArchiveAllInbox :execrows
UPDATE inbox_item SET archived = true
WHERE workspace_id = $1 AND recipient_type = 'member' AND recipient_id = $2 AND archived = false
`
type ArchiveAllInboxParams struct {
WorkspaceID pgtype.UUID `json:"workspace_id"`
RecipientID pgtype.UUID `json:"recipient_id"`
}
func (q *Queries) ArchiveAllInbox(ctx context.Context, arg ArchiveAllInboxParams) (int64, error) {
result, err := q.db.Exec(ctx, archiveAllInbox, arg.WorkspaceID, arg.RecipientID)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
const archiveAllReadInbox = `-- name: ArchiveAllReadInbox :execrows
UPDATE inbox_item SET archived = true
WHERE workspace_id = $1 AND recipient_type = 'member' AND recipient_id = $2 AND read = true AND archived = false
`
type ArchiveAllReadInboxParams struct {
WorkspaceID pgtype.UUID `json:"workspace_id"`
RecipientID pgtype.UUID `json:"recipient_id"`
}
func (q *Queries) ArchiveAllReadInbox(ctx context.Context, arg ArchiveAllReadInboxParams) (int64, error) {
result, err := q.db.Exec(ctx, archiveAllReadInbox, arg.WorkspaceID, arg.RecipientID)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
const archiveCompletedInbox = `-- name: ArchiveCompletedInbox :execrows
UPDATE inbox_item i SET archived = true
WHERE i.workspace_id = $1 AND i.recipient_type = 'member' AND i.recipient_id = $2 AND i.archived = false
AND i.issue_id IN (
SELECT id FROM issue
WHERE workspace_id = $1
AND issue_effective_status(workspace_id, status) IN ('done', 'cancelled')
)
`
type ArchiveCompletedInboxParams struct {
WorkspaceID pgtype.UUID `json:"workspace_id"`
RecipientID pgtype.UUID `json:"recipient_id"`
}
func (q *Queries) ArchiveCompletedInbox(ctx context.Context, arg ArchiveCompletedInboxParams) (int64, error) {
result, err := q.db.Exec(ctx, archiveCompletedInbox, arg.WorkspaceID, arg.RecipientID)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
const archiveInboxByIssue = `-- name: ArchiveInboxByIssue :execrows
UPDATE inbox_item SET archived = true
WHERE workspace_id = $1 AND recipient_type = $2 AND recipient_id = $3 AND issue_id = $4 AND archived = false
`
type ArchiveInboxByIssueParams struct {
WorkspaceID pgtype.UUID `json:"workspace_id"`
RecipientType string `json:"recipient_type"`
RecipientID pgtype.UUID `json:"recipient_id"`
IssueID pgtype.UUID `json:"issue_id"`
}
func (q *Queries) ArchiveInboxByIssue(ctx context.Context, arg ArchiveInboxByIssueParams) (int64, error) {
result, err := q.db.Exec(ctx, archiveInboxByIssue,
arg.WorkspaceID,
arg.RecipientType,
arg.RecipientID,
arg.IssueID,
)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
const archiveInboxByIssueAndType = `-- name: ArchiveInboxByIssueAndType :many
UPDATE inbox_item SET archived = true
WHERE workspace_id = $1 AND issue_id = $2 AND type = $3 AND archived = false
RETURNING recipient_type, recipient_id
`
type ArchiveInboxByIssueAndTypeParams struct {
WorkspaceID pgtype.UUID `json:"workspace_id"`
IssueID pgtype.UUID `json:"issue_id"`
Type string `json:"type"`
}
type ArchiveInboxByIssueAndTypeRow struct {
RecipientType string `json:"recipient_type"`
RecipientID pgtype.UUID `json:"recipient_id"`
}
func (q *Queries) ArchiveInboxByIssueAndType(ctx context.Context, arg ArchiveInboxByIssueAndTypeParams) ([]ArchiveInboxByIssueAndTypeRow, error) {
rows, err := q.db.Query(ctx, archiveInboxByIssueAndType, arg.WorkspaceID, arg.IssueID, arg.Type)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ArchiveInboxByIssueAndTypeRow{}
for rows.Next() {
var i ArchiveInboxByIssueAndTypeRow
if err := rows.Scan(&i.RecipientType, &i.RecipientID); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const archiveInboxItem = `-- name: ArchiveInboxItem :one
UPDATE inbox_item SET archived = true
WHERE id = $1
RETURNING id, workspace_id, recipient_type, recipient_id, type, severity, issue_id, title, body, read, archived, created_at, actor_type, actor_id, details
`
func (q *Queries) ArchiveInboxItem(ctx context.Context, id pgtype.UUID) (InboxItem, error) {
row := q.db.QueryRow(ctx, archiveInboxItem, id)
var i InboxItem
err := row.Scan(
&i.ID,
&i.WorkspaceID,
&i.RecipientType,
&i.RecipientID,
&i.Type,
&i.Severity,
&i.IssueID,
&i.Title,
&i.Body,
&i.Read,
&i.Archived,
&i.CreatedAt,
&i.ActorType,
&i.ActorID,
&i.Details,
)
return i, err
}
const countUnreadInbox = `-- name: CountUnreadInbox :one
SELECT count(*) FROM inbox_item
WHERE workspace_id = $1 AND recipient_type = $2 AND recipient_id = $3 AND read = false AND archived = false
`
type CountUnreadInboxParams struct {
WorkspaceID pgtype.UUID `json:"workspace_id"`
RecipientType string `json:"recipient_type"`
RecipientID pgtype.UUID `json:"recipient_id"`
}
func (q *Queries) CountUnreadInbox(ctx context.Context, arg CountUnreadInboxParams) (int64, error) {
row := q.db.QueryRow(ctx, countUnreadInbox, arg.WorkspaceID, arg.RecipientType, arg.RecipientID)
var count int64
err := row.Scan(&count)
return count, err
}
const countUnreadInboxByWorkspace = `-- name: CountUnreadInboxByWorkspace :many
SELECT newest.workspace_id, count(*) AS count
FROM (
SELECT DISTINCT ON (i.workspace_id, COALESCE(i.issue_id, i.id))
i.workspace_id, i.read
FROM inbox_item i
JOIN member m ON m.workspace_id = i.workspace_id AND m.user_id = i.recipient_id
WHERE i.recipient_type = 'member'
AND i.recipient_id = $1
AND i.archived = false
ORDER BY i.workspace_id, COALESCE(i.issue_id, i.id), i.created_at DESC
) newest
WHERE newest.read = false
GROUP BY newest.workspace_id
`
type CountUnreadInboxByWorkspaceRow struct {
WorkspaceID pgtype.UUID `json:"workspace_id"`
Count int64 `json:"count"`
}
// Per-workspace unread inbox counts for a recipient member, matching the
// inbox UI's deduplicated view: notifications are grouped per issue
// (Linear-style, one row per issue) and an issue counts as unread only when
// its NEWEST non-archived item is unread. Opening an issue marks just that
// newest item read, so counting raw unread rows would keep older siblings
// alive and light the switcher dot for a workspace whose inbox the user sees
// as empty (MUL-3695). Items without an issue group on their own id. The
// member join keeps counts scoped to workspaces the user still belongs to,
// so a stale item left behind in a workspace the user has since left cannot
// light the dot.
func (q *Queries) CountUnreadInboxByWorkspace(ctx context.Context, recipientID pgtype.UUID) ([]CountUnreadInboxByWorkspaceRow, error) {
rows, err := q.db.Query(ctx, countUnreadInboxByWorkspace, recipientID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []CountUnreadInboxByWorkspaceRow{}
for rows.Next() {
var i CountUnreadInboxByWorkspaceRow
if err := rows.Scan(&i.WorkspaceID, &i.Count); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const createInboxItem = `-- name: CreateInboxItem :one
INSERT INTO inbox_item (
workspace_id, recipient_type, recipient_id,
type, severity, issue_id, title, body,
actor_type, actor_id, details
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
RETURNING id, workspace_id, recipient_type, recipient_id, type, severity, issue_id, title, body, read, archived, created_at, actor_type, actor_id, details
`
type CreateInboxItemParams struct {
WorkspaceID pgtype.UUID `json:"workspace_id"`
RecipientType string `json:"recipient_type"`
RecipientID pgtype.UUID `json:"recipient_id"`
Type string `json:"type"`
Severity string `json:"severity"`
IssueID pgtype.UUID `json:"issue_id"`
Title string `json:"title"`
Body pgtype.Text `json:"body"`
ActorType pgtype.Text `json:"actor_type"`
ActorID pgtype.UUID `json:"actor_id"`
Details []byte `json:"details"`
}
func (q *Queries) CreateInboxItem(ctx context.Context, arg CreateInboxItemParams) (InboxItem, error) {
row := q.db.QueryRow(ctx, createInboxItem,
arg.WorkspaceID,
arg.RecipientType,
arg.RecipientID,
arg.Type,
arg.Severity,
arg.IssueID,
arg.Title,
arg.Body,
arg.ActorType,
arg.ActorID,
arg.Details,
)
var i InboxItem
err := row.Scan(
&i.ID,
&i.WorkspaceID,
&i.RecipientType,
&i.RecipientID,
&i.Type,
&i.Severity,
&i.IssueID,
&i.Title,
&i.Body,
&i.Read,
&i.Archived,
&i.CreatedAt,
&i.ActorType,
&i.ActorID,
&i.Details,
)
return i, err
}
const getInboxItem = `-- name: GetInboxItem :one
SELECT id, workspace_id, recipient_type, recipient_id, type, severity, issue_id, title, body, read, archived, created_at, actor_type, actor_id, details FROM inbox_item
WHERE id = $1
`
func (q *Queries) GetInboxItem(ctx context.Context, id pgtype.UUID) (InboxItem, error) {
row := q.db.QueryRow(ctx, getInboxItem, id)
var i InboxItem
err := row.Scan(
&i.ID,
&i.WorkspaceID,
&i.RecipientType,
&i.RecipientID,
&i.Type,
&i.Severity,
&i.IssueID,
&i.Title,
&i.Body,
&i.Read,
&i.Archived,
&i.CreatedAt,
&i.ActorType,
&i.ActorID,
&i.Details,
)
return i, err
}
const getInboxItemInWorkspace = `-- name: GetInboxItemInWorkspace :one
SELECT id, workspace_id, recipient_type, recipient_id, type, severity, issue_id, title, body, read, archived, created_at, actor_type, actor_id, details FROM inbox_item
WHERE id = $1 AND workspace_id = $2
`
type GetInboxItemInWorkspaceParams struct {
ID pgtype.UUID `json:"id"`
WorkspaceID pgtype.UUID `json:"workspace_id"`
}
func (q *Queries) GetInboxItemInWorkspace(ctx context.Context, arg GetInboxItemInWorkspaceParams) (InboxItem, error) {
row := q.db.QueryRow(ctx, getInboxItemInWorkspace, arg.ID, arg.WorkspaceID)
var i InboxItem
err := row.Scan(
&i.ID,
&i.WorkspaceID,
&i.RecipientType,
&i.RecipientID,
&i.Type,
&i.Severity,
&i.IssueID,
&i.Title,
&i.Body,
&i.Read,
&i.Archived,
&i.CreatedAt,
&i.ActorType,
&i.ActorID,
&i.Details,
)
return i, err
}
const listArchivedInboxItems = `-- name: ListArchivedInboxItems :many
SELECT i.id, i.workspace_id, i.recipient_type, i.recipient_id, i.type, i.severity, i.issue_id, i.title, i.body, i.read, i.archived, i.created_at, i.actor_type, i.actor_id, i.details,
iss.status as issue_status
FROM inbox_item i
LEFT JOIN issue iss ON iss.id = i.issue_id
WHERE i.workspace_id = $1 AND i.recipient_type = $2 AND i.recipient_id = $3 AND i.archived = true
AND (i.issue_id IS NULL OR NOT EXISTS (
SELECT 1
FROM inbox_item active
WHERE active.workspace_id = i.workspace_id
AND active.recipient_type = i.recipient_type
AND active.recipient_id = i.recipient_id
AND active.issue_id = i.issue_id
AND active.archived = false
))
ORDER BY i.created_at DESC
LIMIT 200
`
type ListArchivedInboxItemsParams struct {
WorkspaceID pgtype.UUID `json:"workspace_id"`
RecipientType string `json:"recipient_type"`
RecipientID pgtype.UUID `json:"recipient_id"`
}
type ListArchivedInboxItemsRow struct {
ID pgtype.UUID `json:"id"`
WorkspaceID pgtype.UUID `json:"workspace_id"`
RecipientType string `json:"recipient_type"`
RecipientID pgtype.UUID `json:"recipient_id"`
Type string `json:"type"`
Severity string `json:"severity"`
IssueID pgtype.UUID `json:"issue_id"`
Title string `json:"title"`
Body pgtype.Text `json:"body"`
Read bool `json:"read"`
Archived bool `json:"archived"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
ActorType pgtype.Text `json:"actor_type"`
ActorID pgtype.UUID `json:"actor_id"`
Details []byte `json:"details"`
IssueStatus pgtype.Text `json:"issue_status"`
}
// Archived counterpart of ListInboxItems, backing the inbox's "Archived"
// sub-view (MUL-3736).
//
// An issue whose group still has an active row is excluded: archiving is
// issue-level, so a NEW notification on an already-archived issue leaves the
// old archived rows in place alongside the fresh active one. The issue belongs
// in the main inbox at that point, and the two lists must stay mutually
// exclusive per issue group — otherwise the same issue renders in both. The
// exclusion lives here rather than in the client so neither list depends on
// the other's cache being loaded. Items without an issue_id group on their own
// id and can never have an active sibling, hence the IS NULL short-circuit.
//
// LIMIT bounds the response while the archive grows without end (v1 ships no
// pagination). Rows are newest-first, so truncation drops the OLDEST rows and
// can never hide a group's newest row — the one the deduplicated UI renders.
func (q *Queries) ListArchivedInboxItems(ctx context.Context, arg ListArchivedInboxItemsParams) ([]ListArchivedInboxItemsRow, error) {
rows, err := q.db.Query(ctx, listArchivedInboxItems, arg.WorkspaceID, arg.RecipientType, arg.RecipientID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListArchivedInboxItemsRow{}
for rows.Next() {
var i ListArchivedInboxItemsRow
if err := rows.Scan(
&i.ID,
&i.WorkspaceID,
&i.RecipientType,
&i.RecipientID,
&i.Type,
&i.Severity,
&i.IssueID,
&i.Title,
&i.Body,
&i.Read,
&i.Archived,
&i.CreatedAt,
&i.ActorType,
&i.ActorID,
&i.Details,
&i.IssueStatus,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listInboxItems = `-- name: ListInboxItems :many
SELECT i.id, i.workspace_id, i.recipient_type, i.recipient_id, i.type, i.severity, i.issue_id, i.title, i.body, i.read, i.archived, i.created_at, i.actor_type, i.actor_id, i.details,
iss.status as issue_status
FROM inbox_item i
LEFT JOIN issue iss ON iss.id = i.issue_id
WHERE i.workspace_id = $1 AND i.recipient_type = $2 AND i.recipient_id = $3 AND i.archived = false
ORDER BY i.created_at DESC
`
type ListInboxItemsParams struct {
WorkspaceID pgtype.UUID `json:"workspace_id"`
RecipientType string `json:"recipient_type"`
RecipientID pgtype.UUID `json:"recipient_id"`
}
type ListInboxItemsRow struct {
ID pgtype.UUID `json:"id"`
WorkspaceID pgtype.UUID `json:"workspace_id"`
RecipientType string `json:"recipient_type"`
RecipientID pgtype.UUID `json:"recipient_id"`
Type string `json:"type"`
Severity string `json:"severity"`
IssueID pgtype.UUID `json:"issue_id"`
Title string `json:"title"`
Body pgtype.Text `json:"body"`
Read bool `json:"read"`
Archived bool `json:"archived"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
ActorType pgtype.Text `json:"actor_type"`
ActorID pgtype.UUID `json:"actor_id"`
Details []byte `json:"details"`
IssueStatus pgtype.Text `json:"issue_status"`
}
func (q *Queries) ListInboxItems(ctx context.Context, arg ListInboxItemsParams) ([]ListInboxItemsRow, error) {
rows, err := q.db.Query(ctx, listInboxItems, arg.WorkspaceID, arg.RecipientType, arg.RecipientID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListInboxItemsRow{}
for rows.Next() {
var i ListInboxItemsRow
if err := rows.Scan(
&i.ID,
&i.WorkspaceID,
&i.RecipientType,
&i.RecipientID,
&i.Type,
&i.Severity,
&i.IssueID,
&i.Title,
&i.Body,
&i.Read,
&i.Archived,
&i.CreatedAt,
&i.ActorType,
&i.ActorID,
&i.Details,
&i.IssueStatus,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const markAllInboxRead = `-- name: MarkAllInboxRead :execrows
UPDATE inbox_item SET read = true
WHERE workspace_id = $1 AND recipient_type = 'member' AND recipient_id = $2 AND archived = false AND read = false
`
type MarkAllInboxReadParams struct {
WorkspaceID pgtype.UUID `json:"workspace_id"`
RecipientID pgtype.UUID `json:"recipient_id"`
}
func (q *Queries) MarkAllInboxRead(ctx context.Context, arg MarkAllInboxReadParams) (int64, error) {
result, err := q.db.Exec(ctx, markAllInboxRead, arg.WorkspaceID, arg.RecipientID)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
const markInboxRead = `-- name: MarkInboxRead :one
UPDATE inbox_item SET read = true
WHERE id = $1
RETURNING id, workspace_id, recipient_type, recipient_id, type, severity, issue_id, title, body, read, archived, created_at, actor_type, actor_id, details
`
func (q *Queries) MarkInboxRead(ctx context.Context, id pgtype.UUID) (InboxItem, error) {
row := q.db.QueryRow(ctx, markInboxRead, id)
var i InboxItem
err := row.Scan(
&i.ID,
&i.WorkspaceID,
&i.RecipientType,
&i.RecipientID,
&i.Type,
&i.Severity,
&i.IssueID,
&i.Title,
&i.Body,
&i.Read,
&i.Archived,
&i.CreatedAt,
&i.ActorType,
&i.ActorID,
&i.Details,
)
return i, err
}
const markInboxUnread = `-- name: MarkInboxUnread :one
UPDATE inbox_item SET read = false
WHERE id = $1
RETURNING id, workspace_id, recipient_type, recipient_id, type, severity, issue_id, title, body, read, archived, created_at, actor_type, actor_id, details
`
// Exact inverse of MarkInboxRead, and item-level for the same reason it is:
// the inbox renders one row per issue carrying that group's NEWEST item, and
// the group's read state is that item's read state. Flipping the whole group
// unread would resurrect older siblings the user already dealt with and
// inflate CountUnreadInbox (which counts raw rows), while changing nothing the
// UI shows.
func (q *Queries) MarkInboxUnread(ctx context.Context, id pgtype.UUID) (InboxItem, error) {
row := q.db.QueryRow(ctx, markInboxUnread, id)
var i InboxItem
err := row.Scan(
&i.ID,
&i.WorkspaceID,
&i.RecipientType,
&i.RecipientID,
&i.Type,
&i.Severity,
&i.IssueID,
&i.Title,
&i.Body,
&i.Read,
&i.Archived,
&i.CreatedAt,
&i.ActorType,
&i.ActorID,
&i.Details,
)
return i, err
}
const unarchiveInboxByIssue = `-- name: UnarchiveInboxByIssue :execrows
UPDATE inbox_item SET archived = false
WHERE workspace_id = $1 AND recipient_type = $2 AND recipient_id = $3 AND issue_id = $4 AND archived = true
`
type UnarchiveInboxByIssueParams struct {
WorkspaceID pgtype.UUID `json:"workspace_id"`
RecipientType string `json:"recipient_type"`
RecipientID pgtype.UUID `json:"recipient_id"`
IssueID pgtype.UUID `json:"issue_id"`
}
// Issue-level restore, mirroring ArchiveInboxByIssue: archiving one item
// archives every sibling for the same issue, so unarchiving must bring the
// whole group back. Leaves `read` untouched for the same reason as above.
func (q *Queries) UnarchiveInboxByIssue(ctx context.Context, arg UnarchiveInboxByIssueParams) (int64, error) {
result, err := q.db.Exec(ctx, unarchiveInboxByIssue,
arg.WorkspaceID,
arg.RecipientType,
arg.RecipientID,
arg.IssueID,
)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
const unarchiveInboxItem = `-- name: UnarchiveInboxItem :one
UPDATE inbox_item SET archived = false
WHERE id = $1
RETURNING id, workspace_id, recipient_type, recipient_id, type, severity, issue_id, title, body, read, archived, created_at, actor_type, actor_id, details
`
// Deliberately does not touch `read`: unarchiving restores an item to the main
// inbox in the exact read/unread state it was archived in, so restoring an
// unread item legitimately raises the unread badge again (MUL-3736).
func (q *Queries) UnarchiveInboxItem(ctx context.Context, id pgtype.UUID) (InboxItem, error) {
row := q.db.QueryRow(ctx, unarchiveInboxItem, id)
var i InboxItem
err := row.Scan(
&i.ID,
&i.WorkspaceID,
&i.RecipientType,
&i.RecipientID,
&i.Type,
&i.Severity,
&i.IssueID,
&i.Title,
&i.Body,
&i.Read,
&i.Archived,
&i.CreatedAt,
&i.ActorType,
&i.ActorID,
&i.Details,
)
return i, err
}