Skip to content

ops(plans): migration to clean up universal plans (closes #742)#802

Merged
cristim merged 1 commit into
feat/multicloud-web-frontendfrom
fix/742-wave3
Jun 5, 2026
Merged

ops(plans): migration to clean up universal plans (closes #742)#802
cristim merged 1 commit into
feat/multicloud-web-frontendfrom
fix/742-wave3

Conversation

@cristim
Copy link
Copy Markdown
Member

@cristim cristim commented May 28, 2026

Summary

  • Shape chosen: A (SQL migration) -- one-shot data cleanup is exactly what migrations are for; a runtime sweep would add code that lives forever for a problem that existed once.
  • Migration number: 000057 -- the highest existing migration on origin/feat/multicloud-web-frontend was 000056; no other open PRs carry a migration, so 000057 is uncontested.
  • Cleanup semantic: DELETE. The three options from issue ops(plans): clean up existing universal plans (DB rows with no plan_accounts entry) #742 are (a) delete, (b) fan-out to all matching accounts, (c) manual review. A migration can only act uniformly; fan-out silently attaches every account to plans the operator may never have intended to run, and manual review cannot be encoded in SQL. Deletion is the safe default for rows that predate account-scoping enforcement and have no explicit account assignment.

Migration shape

DELETE FROM purchase_plans
WHERE NOT EXISTS (
    SELECT 1 FROM plan_accounts pa WHERE pa.plan_id = purchase_plans.id
);

Referential safety: purchase_executions.plan_id is ON DELETE SET NULL (migration 000033); purchase_history.plan_id is ON DELETE SET NULL (initial schema); plan_accounts.plan_id is ON DELETE CASCADE (migration 000011). No orphaned child rows are created.

Down migration

The .down.sql is an intentional no-op (SELECT 1). Deleted rows cannot be recovered from SQL alone; operators must restore from a backup taken before migration 000057 ran. The file documents this explicitly rather than being silently empty.

Test scope

TestMigration_CleanupUniversalPlans (integration tag) covers:

  • 2 universal plans (no plan_accounts rows) -- asserted deleted after migration
  • 1 scoped plan (has a plan_accounts row) -- asserted to survive
  • FK SET NULL on child rows -- execution and history rows linked to a deleted plan have plan_id NULLed
  • Idempotency -- re-running when no universal plans exist is a no-op; scoped plan count stays at 1

Manual verification after merge

-- Should return zero rows once migration runs on the live DB:
SELECT pp.id, pp.name, pp.created_at
FROM purchase_plans pp
LEFT JOIN plan_accounts pa ON pa.plan_id = pp.id
WHERE pa.plan_id IS NULL;

Summary by CodeRabbit

  • Chores
    • Added database migration to remove orphaned purchase plan records that lack associated plan accounts
    • Migration properly handles all related record references during cleanup
    • Implemented integration tests validating migration behavior across multiple scenarios, including rollback safety and idempotency verification

Add migration 000057 that deletes purchase_plans rows with no
plan_accounts entry (universal plans created before #743's API guard).
Linked executions and history rows have their plan_id NULLed via the
existing ON DELETE SET NULL FK constraints; the .down.sql is a no-op
because deleted rows cannot be reconstructed from SQL alone.

Includes an integration test covering 2 universal plans deleted, 1
scoped plan preserved, FK SET NULL on child rows, and idempotency.
@cristim cristim added triaged Item has been triaged priority/p2 Backlog-worthy severity/medium Moderate harm urgency/this-sprint Within the current sprint impact/few Limited audience effort/m Days type/chore Maintenance / non-user-visible labels May 28, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 28, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This PR introduces database migration 000057 that permanently deletes orphaned purchase_plans rows lacking plan_accounts entries. The up migration includes documented assumptions about referential integrity, while the down migration is intentionally a no-op. An integration test validates deletion, referential nulling behavior, and re-run idempotency.

Changes

Universal Plans Cleanup Migration

Layer / File(s) Summary
Migration implementation
internal/database/postgres/migrations/000057_cleanup_universal_plans.up.sql, internal/database/postgres/migrations/000057_cleanup_universal_plans.down.sql
Up migration deletes purchase_plans rows without matching plan_accounts, documents referential behavior expectations (dependent purchase_executions and purchase_history use ON DELETE SET NULL), and confirms deletion is idempotent. Down migration is a documented no-op acknowledging deleted data cannot be restored.
Integration test with fixtures and validation
internal/database/postgres/migrations/000057_cleanup_universal_plans_test.go
Test boots Postgres container, establishes pre-migration baseline with two universal and one scoped purchase_plans, plus purchase_executions and purchase_history rows referencing a universal plan. After executing migration 000057, asserts both universal plans are deleted, the scoped plan remains, and dependent rows have plan_id set to NULL. Finally verifies idempotency by re-running migrations from the same baseline.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🐰 A cleanup hop through the database lands,
Orphaned plans swept away by careful hands,
Down migrations just rest and smile,
Tests confirm it's idempotent all the while!
✨ Clean data, no mess, mile after mile.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically summarizes the main change: a migration to clean up universal plans, with a direct reference to the closed issue.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/742-wave3

Comment @coderabbitai help to get the list of available commands and usage tips.

@cristim
Copy link
Copy Markdown
Member Author

cristim commented May 28, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 28, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

cristim added a commit that referenced this pull request May 28, 2026
PR #802 (issue #742) also claimed migration 000057 -- the two PRs were
dispatched in parallel and each independently verified "000057 is free".
Whichever lands second would trip the pre-commit migration-collision
hook. Per project memory `project_migration_number_collisions.md`,
renumber the lower-priority PR via `git mv` to the next free slot.

- 000057_purchase_executions_direct_execute_audit.up.sql -> 000058_*
- 000057_purchase_executions_direct_execute_audit.down.sql -> 000058_*
- internal/config/types.go: three "Migration 000057" comment refs ->
  "Migration 000058"

Verified 000058 is free on origin/feat/multicloud-web-frontend and no
other open PR introduces a clashing migration.
cristim added a commit that referenced this pull request May 28, 2026
Three PRs were dispatched in parallel and each independently picked the
same "next free" migration number 000057. PR #802 (universal-plans
cleanup) took 000057, PR #803 (execute-permissions) renumbered to 000058
in a follow-up, and this PR (#804, revocation) now takes 000059.

- 000057_purchase_history_revocation.up.sql -> 000059_*
- 000057_purchase_history_revocation.down.sql -> 000059_*
- internal/config/store_postgres.go: comment ref "migration 000057"
  -> "migration 000059"
- migration file headers updated

Verified 000059 is free on origin/feat/multicloud-web-frontend and no
other open PR introduces a clashing migration.

Per `project_migration_number_collisions.md`.
@cristim
Copy link
Copy Markdown
Member Author

cristim commented May 30, 2026

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 30, 2026

✅ Actions performed

Full review triggered.

cristim added a commit that referenced this pull request Jun 3, 2026
Three PRs were dispatched in parallel and each independently picked the
same "next free" migration number 000057. PR #802 (universal-plans
cleanup) took 000057, PR #803 (execute-permissions) renumbered to 000058
in a follow-up, and this PR (#804, revocation) now takes 000059.

- 000057_purchase_history_revocation.up.sql -> 000059_*
- 000057_purchase_history_revocation.down.sql -> 000059_*
- internal/config/store_postgres.go: comment ref "migration 000057"
  -> "migration 000059"
- migration file headers updated

Verified 000059 is free on origin/feat/multicloud-web-frontend and no
other open PR introduces a clashing migration.

Per `project_migration_number_collisions.md`.
cristim added a commit that referenced this pull request Jun 3, 2026
…oses #289) (#803)

* feat(api): execute-any/own permission gate for direct purchase execute (closes #289)

Add two new RBAC verbs (execute-any:purchases, execute-own:purchases) that
let privileged users bypass the approval email and execute purchases
immediately from the dashboard.

- auth/types.go: ActionExecuteOwn + ActionExecuteAny constants (not in
  DefaultUserPermissions -- opt-in only)
- config/types.go: three nullable audit fields on PurchaseExecution
  (executed_by_user_id, executed_at, pre_approval_skip_reason)
- migration 000057: ALTER TABLE purchase_executions ADD COLUMN for the
  three audit fields + partial index on direct-execute rows
- store_postgres.go: extend INSERT, all SELECT RETURNING, and scanExecutionRows
  to carry the three new columns
- handler_purchases.go: authorizeSessionExecuteDirect (fail-closed, nil-safe),
  directExecutePurchase (stamps audit fields, delegates to ApproveAndExecute),
  and execute_mode="direct" branch in executePurchase
- handler_purchases_test.go: 4 new scenarios (no-permission 403,
  execute-any admin 200, execute-own owner 200, execute-own non-owner 403)
- store_postgres_pgxmock_test.go: updated column/row fixtures for new columns

* feat(recs): execute-mode toggle in purchase modal for privileged sessions

Show a radio-group toggle ("Send for Approval" / "Execute Now") in the
purchase modal when the session holds execute-any:purchases or
execute-own:purchases; plain approval note otherwise.

- permissions.ts: add 'execute-own' and 'execute-any' to Action union type
- api/purchases.ts: accept optional executeMode param, send execute_mode
  in POST body
- api/types.ts: add direct_execute?: boolean to PurchaseResult
- recommendations.ts: currentExecuteMode state, getExecuteMode /
  clearExecuteMode exports, conditional toggle rendering with warning
  callout in openPurchaseModal
- app.ts: read getExecuteMode() in handleExecutePurchase, confirmation
  dialog copy + toast vary by mode, clearExecuteMode on success
- __tests__/execute-mode-toggle.test.ts: 6 tests covering toggle
  visibility by role, default state, radio-change state, and button
  label updates

* fix(migrations): renumber direct-execute-audit 000057 -> 000058

PR #802 (issue #742) also claimed migration 000057 -- the two PRs were
dispatched in parallel and each independently verified "000057 is free".
Whichever lands second would trip the pre-commit migration-collision
hook. Per project memory `project_migration_number_collisions.md`,
renumber the lower-priority PR via `git mv` to the next free slot.

- 000057_purchase_executions_direct_execute_audit.up.sql -> 000058_*
- 000057_purchase_executions_direct_execute_audit.down.sql -> 000058_*
- internal/config/types.go: three "Migration 000057" comment refs ->
  "Migration 000058"

Verified 000058 is free on origin/feat/multicloud-web-frontend and no
other open PR introduces a clashing migration.

* fix(ci): extract buildApprovalPendingResponse to reduce executePurchase complexity on PR #803
cristim added a commit that referenced this pull request Jun 4, 2026
Three PRs were dispatched in parallel and each independently picked the
same "next free" migration number 000057. PR #802 (universal-plans
cleanup) took 000057, PR #803 (execute-permissions) renumbered to 000058
in a follow-up, and this PR (#804, revocation) now takes 000059.

- 000057_purchase_history_revocation.up.sql -> 000059_*
- 000057_purchase_history_revocation.down.sql -> 000059_*
- internal/config/store_postgres.go: comment ref "migration 000057"
  -> "migration 000059"
- migration file headers updated

Verified 000059 is free on origin/feat/multicloud-web-frontend and no
other open PR introduces a clashing migration.

Per `project_migration_number_collisions.md`.
cristim added a commit that referenced this pull request Jun 4, 2026
Three PRs were dispatched in parallel and each independently picked the
same "next free" migration number 000057. PR #802 (universal-plans
cleanup) took 000057, PR #803 (execute-permissions) renumbered to 000058
in a follow-up, and this PR (#804, revocation) now takes 000059.

- 000057_purchase_history_revocation.up.sql -> 000059_*
- 000057_purchase_history_revocation.down.sql -> 000059_*
- internal/config/store_postgres.go: comment ref "migration 000057"
  -> "migration 000059"
- migration file headers updated

Verified 000059 is free on origin/feat/multicloud-web-frontend and no
other open PR introduces a clashing migration.

Per `project_migration_number_collisions.md`.
@cristim
Copy link
Copy Markdown
Member Author

cristim commented Jun 4, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 4, 2026

Rate Limit Exceeded

@cristim have exceeded the limit for the number of chat messages per hour. Please wait 38 minutes and 41 seconds before sending another message.

@cristim
Copy link
Copy Markdown
Member Author

cristim commented Jun 5, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 5, 2026

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@internal/database/postgres/migrations/000057_cleanup_universal_plans_test.go`:
- Around line 152-153: Update the misleading comment in
000057_cleanup_universal_plans_test.go: replace or remove the line claiming
"Re-seed only the scoped plan to simulate a clean DB at version 56" since no
re-seeding occurs; instead state that after the rollback the scoped plan remains
(rollback changes schema version, not data) and that the idempotency test
verifies running migration 000057 is a no-op when universal plans are absent.
Reference the rollback call and the migration ID 000057 in the revised comment
so future maintainers understand intent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 29745840-beac-4b0c-8cf0-93dd073d0320

📥 Commits

Reviewing files that changed from the base of the PR and between 4956d66 and e08d7e4.

📒 Files selected for processing (3)
  • internal/database/postgres/migrations/000057_cleanup_universal_plans.down.sql
  • internal/database/postgres/migrations/000057_cleanup_universal_plans.up.sql
  • internal/database/postgres/migrations/000057_cleanup_universal_plans_test.go

Comment on lines +152 to +153
// Re-seed only the scoped plan to simulate a clean DB at version 56.
// The universal plans are intentionally absent (already cleaned up).
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Clarify or remove misleading comment.

The comment states "Re-seed only the scoped plan to simulate a clean DB at version 56", but no re-seeding occurs. After the rollback on line 151, the scoped plan is already present in the database (rollback changes the schema version, not data). The idempotency test works correctly—it verifies that re-running migration 000057 when no universal plans exist is a no-op—but the comment may confuse future maintainers.

📝 Suggested fix to clarify intent
-	// Re-seed only the scoped plan to simulate a clean DB at version 56.
-	// The universal plans are intentionally absent (already cleaned up).
+	// Data remains unchanged: scoped plan still present, universal plans already deleted.
+	// Re-running 000057 should be a no-op (no universal plans to delete).
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Re-seed only the scoped plan to simulate a clean DB at version 56.
// The universal plans are intentionally absent (already cleaned up).
// Data remains unchanged: scoped plan still present, universal plans already deleted.
// Re-running 000057 should be a no-op (no universal plans to delete).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/database/postgres/migrations/000057_cleanup_universal_plans_test.go`
around lines 152 - 153, Update the misleading comment in
000057_cleanup_universal_plans_test.go: replace or remove the line claiming
"Re-seed only the scoped plan to simulate a clean DB at version 56" since no
re-seeding occurs; instead state that after the rollback the scoped plan remains
(rollback changes schema version, not data) and that the idempotency test
verifies running migration 000057 is a no-op when universal plans are absent.
Reference the rollback call and the migration ID 000057 in the revised comment
so future maintainers understand intent.

cristim added a commit that referenced this pull request Jun 5, 2026
Three PRs were dispatched in parallel and each independently picked the
same "next free" migration number 000057. PR #802 (universal-plans
cleanup) took 000057, PR #803 (execute-permissions) renumbered to 000058
in a follow-up, and this PR (#804, revocation) now takes 000059.

- 000057_purchase_history_revocation.up.sql -> 000059_*
- 000057_purchase_history_revocation.down.sql -> 000059_*
- internal/config/store_postgres.go: comment ref "migration 000057"
  -> "migration 000059"
- migration file headers updated

Verified 000059 is free on origin/feat/multicloud-web-frontend and no
other open PR introduces a clashing migration.

Per `project_migration_number_collisions.md`.
@cristim
Copy link
Copy Markdown
Member Author

cristim commented Jun 5, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 5, 2026

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim cristim merged commit dd16b94 into feat/multicloud-web-frontend Jun 5, 2026
4 checks passed
@cristim cristim deleted the fix/742-wave3 branch June 5, 2026 09:02
cristim added a commit that referenced this pull request Jun 5, 2026
…to 000060 (closes #970) (#971)

Two migrations shared version 000057: drop_user_role_to_groups (applied on
live DBs) and cleanup_universal_plans (from #802, not yet applied). golang-
migrate requires unique version numbers. Renumber cleanup_universal_plans to
000060 (a free slot between 000059 and 000063), and update the version
references in its SQL comments and test file.
cristim added a commit that referenced this pull request Jun 5, 2026
Three PRs were dispatched in parallel and each independently picked the
same "next free" migration number 000057. PR #802 (universal-plans
cleanup) took 000057, PR #803 (execute-permissions) renumbered to 000058
in a follow-up, and this PR (#804, revocation) now takes 000059.

- 000057_purchase_history_revocation.up.sql -> 000059_*
- 000057_purchase_history_revocation.down.sql -> 000059_*
- internal/config/store_postgres.go: comment ref "migration 000057"
  -> "migration 000059"
- migration file headers updated

Verified 000059 is free on origin/feat/multicloud-web-frontend and no
other open PR introduces a clashing migration.

Per `project_migration_number_collisions.md`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/m Days impact/few Limited audience priority/p2 Backlog-worthy severity/medium Moderate harm triaged Item has been triaged type/chore Maintenance / non-user-visible urgency/this-sprint Within the current sprint

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant