2026-10-21-state-columns
title: Declare lifecycle states and use guarded transitions affects: dartway_orm: "0.23.0" dartway_core_server: "0.23.0" dartway_cli: "0.26.0"
Who is affected
Projects adopting state columns for lifecycle status fields. Existing unannotated columns keep their ordinary repository behavior; review lifecycle statuses when updating the server toolkit.
What to change
- Declare a const
DwStateTransitions<YourStatus>beside the enum withinitialandmoves. Annotate the row's non-null enum field with@DwStateColumn(yourTransitions)and regenerate. - Replace
update(row.copyWith(status: next))and bulk state assignments withtransition(id, column: (t) => t.status, to: next)ortransitionWhere. Translate null or unchanged requested ids into the feature's refusal. Publish only returned rows. - Insert only initial states. Replace upserts on that table with insert/tryInsert plus transitions.
Ordinary updates must carry the current stored state; they cannot reset a concurrently changed one.
Catch
DwStaleStatefor an ordinary edit that races a transition and translate it into your feature's conflict, or leave it to the server'sdw.conflictresponse (HTTP 409, no incident). Re-read before deciding whether to retry. Keepon DwRowNotFoundfor missing rows, including state tables. Generated stored-rowcopyWith(status: next)throwsDwStateWriteExceptionwhen the value changes;status: row.statusis allowed and passes the checker. - Remove raw SQL state writes in
lib/. A migration backfill for stranded values usesm.backfill.
The annotation does not alter the SQL schema; no schema migration is needed for the annotation alone. Do not rewrite an applied migration.
How to check
Run generation and dartway check, then the feature's acceptance suite on a real Postgres. Cover
forbidden initial states and moves, repeated terminal moves, and competing transitions where only
one is legal after the first completes. Also verify an ordinary edit racing a transition throws
DwStaleState without overwriting fields, a deleted row throws DwRowNotFound, and an intentional
state change through update(row.copyWith(...)) remains a programming error.