Repository navigation
fix(db): cancel attribution lands in a write-only column while canceled_by stays NULL #119
Description
Reviewed commit: be11bdcb5. Note: origin/main moved to 3e9660d06 during the review; re-verify against current main before changing code, since a finding may have been fixed or moved.
Two defects compound on a single UPDATE: the cancel actor is stamped into a column nothing reads, and the readable column is never written.
Where
internal/config/store_postgres.go:997-1011-TransitionExecutionStatus(actor written totransitioned_byat:1001;RETURNINGlist at:1006)- Caller:
internal/api/handler_purchases.go:445(cancelOrRecoverExecution), reached fromhandleCancelathandler_purchases.go:420 - Column contract:
internal/database/postgres/migrations/000089_rename_cancelled_to_canceled.up.sqland000077_audit_actor_stamps.up.sql - The setter that is not on this path:
SetCancelledBy(store_postgres.go:1048), called only from the revoke flow athandler_purchases.go:1468 - Sibling readers that do it right:
store_postgres.go:1194,:1235,:1260,:1302,:1323,:1347,:1377,:1404,:1608
What
-
The audit stamp is write-only.
TransitionExecutionStatusstamps the actor intotransitioned_by, the column migration 000077 added as the audit actor stamp.git grep transitioned_byover all non-test Go onorigin/mainshows it appears only in three UPDATE statements (store_postgres.go:1001,store_postgres.go:2505,store_postgres_registrations.go:184) and in comments. No SELECT list in the repository projects it, andPurchaseExecutionhas no field for it. The value is written and then unreachable. -
The RETURNING clause breaks the expand-contract invariant. The same UPDATE's
RETURNINGlist projects the raw legacycancelled_by, while all nine sibling readers projectCOALESCE(canceled_by, cancelled_by). Migration 000089 states the invariant explicitly:every read projects COALESCE(canceled_by, cancelled_by), so a row written by EITHER old or new code at ANY point in the deploy window reads correctly.
This RETURNING clause is the one place in the repository that breaks it.
Failure scenario
A user cancels a pending purchase from the UI. handleCancel resolves the session user and calls cancelOrRecoverExecution -> TransitionExecutionStatus(..., "canceled", actor).
The row ends with status = 'canceled', transitioned_by = <user uuid>, and both canceled_by and cancelled_by still NULL. The ordinary cancel path never calls SetCancelledBy; that setter is invoked only from the revoke flow.
Every subsequent read projects COALESCE(canceled_by, cancelled_by) and gets NULL, so the History view shows a canceled execution with no accountable party, while the actor that was recorded sits in transitioned_by where no query can reach it. For a money-mutation action that is a complete loss of attribution.
Separately, when canceled_by is already set (for example by CancelExecutionAtomic, store_postgres.go:1081, which writes canceled_by only), a later SavePurchaseExecutionTx (store_postgres.go:924, cancelled_by = $17) writes only the legacy column. The COALESCE prefers the already-set canceled_by, so that update is silently invisible.
Fix direction
- Have
TransitionExecutionStatuswritecanceled_by(andcancelled_byfor the deploy window) whentoStatusis a cancel state. - Change its
RETURNINGto the sameCOALESCE(canceled_by, cancelled_by)the other readers use. - Either project
transitioned_byintoPurchaseExecutionso the audit stamp is reachable, or drop the write. A column written by three statements and read by none is worse than no column, because it looks like the audit trail exists.
Related
- LeanerCloud/cloud-commitments-cli#1278 (contract migration: drop
'cancelled'from CHECK constraints and drop thecancelled_bycolumn) is the planned follow-on to the fix(db): rename status value cancelled->canceled (expand-contract, US spelling) cloud-commitments-cli#1277 expand. It does not cover this defect, and the interaction matters in both directions: LeanerCloud/cloud-commitments-cli#1278's step 1 drainscancelled_byintocanceled_by, which cannot repair rows where neither column was ever written; and once LeanerCloud/cloud-commitments-cli#1278 dropscancelled_by, thisRETURNINGclause stops compiling against the schema. Fix this first, or fold it into the same change. - fix(db): rename status value cancelled->canceled (expand-contract, US spelling) cloud-commitments-cli#1277 is the expand half that established the COALESCE-on-every-read invariant this clause violates.
Activity