You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
feat(plans): surface legacy no-account plans as 'Unassigned' (backfill not feasible) #973
Follow-up to #705. The Plans page Account global filter (#715) INNER-JOINs plan_accounts, and #743 made target_accounts required at create time. Plans created before#743 with zeroplan_accounts rows ("universal" / legacy no-account plans) are therefore invisible whenever an account filter is active.
The investigated remedy was Option 1: backfill -- associate each legacy no-account plan with its recoverable account(s). After a schema + data-model investigation, backfill is not safely possible, so this issue tracks the recommended alternative instead: surface legacy no-account plans under an "Unassigned" pseudo-account in the filter.
Why backfill is not feasible
A deterministic, recoverable account source for these plans does not exist on any migrated database:
The plan->account link is severed on delete.purchase_executions.plan_id and purchase_history.plan_id are both ON DELETE SET NULL. Even though those child tables carry a cloud_account_id (added in 000011), once 000060 deletes the plan their plan_id becomes NULL, so an orphaned execution/history row can no longer be attributed back to any specific plan. (a) is therefore impossible.
No other plan->account reference exists.purchase_plans has no account column in any migration (000001 ... 000064); plan_accounts is the sole linkage. The plan's services JSONB has no account. (b) is impossible.
Single-account fan-out is unsafe. Assigning every legacy plan to "the one cloud account" only holds in single-account deployments and would silently mis-scope plans in multi-account deployments -- the exact concern that drove 000060 to delete rather than fan-out (see ops(plans): clean up existing universal plans (DB rows with no plan_accounts entry) #742). Guessing associations is explicitly out of scope.
Inserting a backfill migration before 000060 is also impossible: 000060 is already applied on deployed DBs, and golang-migrate does not permit out-of-order versions.
Recommended alternative: "Unassigned" surfacing
Rather than guessing associations, make legacy/unscoped plans discoverable without an account chip selection:
Backend getPlans(): when an account filter is active, return plans matching the selected accounts via the plan_accounts join plus plans that have zero plan_accounts rows (LEFT JOIN + pa.account_id IS NULL), grouped under a synthetic "Unassigned" bucket.
Frontend Plans page: show an "Unassigned" group/badge so operators can find and re-scope these plans manually.
Note: on databases already migrated past 000060 there are no zero-account plans left, so this primarily protects DBs that predate the upgrade and any future regressions that reintroduce unscoped plans. It is the safe, non-destructive complement to the 000060 cleanup.
Summary
Follow-up to #705. The Plans page Account global filter (#715) INNER-JOINs
plan_accounts, and #743 madetarget_accountsrequired at create time. Plans created before #743 with zeroplan_accountsrows ("universal" / legacy no-account plans) are therefore invisible whenever an account filter is active.The investigated remedy was Option 1: backfill -- associate each legacy no-account plan with its recoverable account(s). After a schema + data-model investigation, backfill is not safely possible, so this issue tracks the recommended alternative instead: surface legacy no-account plans under an "Unassigned" pseudo-account in the filter.
Why backfill is not feasible
A deterministic, recoverable account source for these plans does not exist on any migrated database:
000060_cleanup_universal_plans(shipped in fix(db): duplicate migration version 000057 on base (cleanup_universal_plans vs drop_user_role_to_groups) #970/fix(db): renumber duplicate migration 000057_cleanup_universal_plans to 000060 (closes #970) #971) runsDELETE FROM purchase_plans WHERE NOT EXISTS (SELECT 1 FROM plan_accounts ...). Every legacy no-account plan is permanently removed by the time any later migration could run, and its.down.sqlis a documented no-op (data cannot be reconstructed from SQL). A new backfill migration (next free slot000065) would match zero rows.purchase_executions.plan_idandpurchase_history.plan_idare bothON DELETE SET NULL. Even though those child tables carry acloud_account_id(added in000011), once 000060 deletes the plan theirplan_idbecomes NULL, so an orphaned execution/history row can no longer be attributed back to any specific plan. (a) is therefore impossible.purchase_planshas no account column in any migration (000001...000064);plan_accountsis the sole linkage. The plan'sservicesJSONB has no account. (b) is impossible.Inserting a backfill migration before 000060 is also impossible: 000060 is already applied on deployed DBs, and golang-migrate does not permit out-of-order versions.
Recommended alternative: "Unassigned" surfacing
Rather than guessing associations, make legacy/unscoped plans discoverable without an account chip selection:
getPlans(): when an account filter is active, return plans matching the selected accounts via theplan_accountsjoin plus plans that have zeroplan_accountsrows (LEFT JOIN +pa.account_id IS NULL), grouped under a synthetic "Unassigned" bucket.Note: on databases already migrated past 000060 there are no zero-account plans left, so this primarily protects DBs that predate the upgrade and any future regressions that reintroduce unscoped plans. It is the safe, non-destructive complement to the 000060 cleanup.
References