Support delete and archive for decisions

What does this MR do and why?

Adds state connection and filter to work item decisions

Adds delete and archive operations for decisions

Related to #627780 (closed)

Changelog: added

Note: Closely related to !254958 (merged). If it gets merged first, there may be conflicts in this MR that may require a rebase with master.

Database review notes

Files relevant for DB review

File Why
db/migrate/20260914000001_add_archived_at_to_work_item_decisions.rb Adds nullable archived_at timestamptz, no default, no index
db/schema_migrations/20260914000001 Checksum for the migration
db/structure.sql One-line diff: the new column on work_item_decisions
ee/app/models/work_items/decision.rb New scopes active, resolved, archived; with_state / │ │ │ with_states build the filtered relations
ee/app/models/work_items/widgets/decision_log.rb Base relation the connection filters (unchanged, for │ │ │ context)
ee/app/graphql/types/work_items/widgets/decision_log_type.rb decisions(states:) argument applies the scopes before │ │ │ keyset pagination
ee/app/services/work_items/decisions/archive_service.rb New UPDATE via update_columns
ee/app/services/work_items/decisions/delete_service.rb New DELETE via destroy!, cascading to options

SQL queries

Note: values are local sample values used to generate db query plans using local GDK (this is a new feature and no data exists in prod yet).

~5k decisions and ~15.5k options were seeded for query plan generation.

Archive

Location: archive_service.rb:19

UPDATE "work_item_decisions" SET "archived_at" = '2026-09-15 03:16:16.332077', "updated_at" = '2026-09-15 03:16:16.332077' WHERE "work_item_decisions"."id" = 16380

Query plan: https://explain.depesz.com/s/GgX5

DELETE by primary key; FK cascade deletes options

Location: delete_service.rb:16

-- Delete
DELETE FROM "work_item_decisions" WHERE "work_item_decisions"."id" = 16364

-- FK cascade
DELETE FROM work_item_decision_options WHERE work_item_decision_id = 12573

Query plan (delete): https://explain.depesz.com/s/PczN9

Query plan (FK cascade): https://explain.depesz.com/s/4R5N

Filtered connection

Keyset pagination adds ORDER BY id DESC LIMIT 101 to the widget's base relation. One variant per state, plus the OR form for several states. Work item 4 held 9 open seeded rows; work item 5 held 7 resolved and 2 archived.

Location: decision_log_type.rb:33 with decision.rb:31-33, 64

-- states: [ACTIVE]

SELECT "work_item_decisions".* FROM "work_item_decisions" WHERE "work_item_decisions"."work_item_id" = 4 AND "work_item_decisions"."resolved_at" IS NULL ORDER BY "work_item_decisions"."id" DESC LIMIT 101

Query plan: https://explain.depesz.com/s/Og8Y

-- states: [ACTIVE, RESOLVED]
SELECT "work_item_decisions".* FROM "work_item_decisions" WHERE "work_item_decisions"."work_item_id" = 5 AND ("work_item_decisions"."resolved_at" IS NULL OR "work_item_decisions"."resolved_at" IS NOT NULL AND "work_item_decisions"."archived_at" IS NULL) ORDER BY "work_item_decisions"."id" DESC LIMIT 101

Query plan: https://explain.depesz.com/s/7dGE

FE integration notes

Note: values are sample values

Archiving a resolved decision

Mutation: workItemDecisionArchive

Input type: WorkItemDecisionArchiveInput.

       Field       │         Type         │                                    Notes                                    │
├──────────────────┼──────────────────────┼─────────────────────────────────────────────────────────────────────────────┤
│ id               │ WorkItemsDecisionID! │ Global ID of the decision, for example gid://gitlab/WorkItems::Decision/123 │
├──────────────────┼──────────────────────┼─────────────────────────────────────────────────────────────────────────────┤
│ clientMutationId │ String               │ Optional                                                                    │
└──────────────────┴──────────────────────┴─────────────────────────────────────────────────────────────────────────────┘

mutation workItemDecisionArchive($input: WorkItemDecisionArchiveInput!) {
  workItemDecisionArchive(input: $input) {
    decision {
      id
      ...
      resolvedAt
    }
    errors
  }
}

{
  "input": {
    "id": "gid://gitlab/WorkItems::Decision/123"
  }
}

Hard-deleting an open decision

Mutation: workItemDecisionDelete

Input type: WorkItemDecisionDeleteInput.

┌──────────────────┬──────────────────────┬─────────────────────────────────────────────────────────────────────────────┐
│      Field       │         Type         │                                    Notes                                    │
├──────────────────┼──────────────────────┼─────────────────────────────────────────────────────────────────────────────┤
│ id               │ WorkItemsDecisionID! │ Global ID of the decision, for example gid://gitlab/WorkItems::Decision/123 │
├──────────────────┼──────────────────────┼─────────────────────────────────────────────────────────────────────────────┤
│ clientMutationId │ String               │ Optional                                                                    │
└──────────────────┴──────────────────────┴─────────────────────────────────────────────────────────────────────────────┘

Payload: decision (WorkItemDecision, nullable), errors ([String!]!), clientMutationId.

mutation workItemDecisionDelete($input: WorkItemDecisionDeleteInput!) {
  workItemDecisionDelete(input: $input) {
    decision {
      id
    }
    errors
  }
}

{
  "input": {
    "id": "gid://gitlab/WorkItems::Decision/123"
  }
}

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

Related to #627780 (closed)

Edited by Richard Chan

Merge request reports

Loading
Loading