Skip to content

fix(storage): retain unfinished ramp execution history - #904

Merged
cristim merged 1 commit into
mainfrom
fix/631-retention-ledger-db
Oct 16, 2026
Merged

cristim merged 1 commit into
mainfrom
fix/631-retention-ledger-db

Conversation

@cristim

@cristim cristim commented Oct 15, 2026

Copy link
Copy Markdown
Member

What and why

Adds migration 000124_ramp_ledger_retention so database DELETE paths (including cascades) cannot remove ramp execution rows that are still held by an unfinished ramp. It also tracks monotonic ledger provenance so the runtime can later tell whether a ramp history is verifiable. Without it, deleting an account or plan can silently erase the history that ramp sizing depends on.

The runtime enforcement is in the stacked follow-up PR.

Stack position

1 of 2. Base: main. Next: the runtime PR (base fix/631-retention-ledger-db).

Verification (local, macOS)

  • go build ./... and go test -race ./internal/config/... ./internal/purchase/... pass.
  • The new store_postgres_retention_integration_test.go runs against a real local Postgres with all migrations applied (real DB, not mocks).
  • CI results are tracked on this PR.

Deploy note

Retention needs an old-Lambda drain window: Lambda versions that predate the runtime PR do not write the new provenance, so let them drain before relying on retention.

Refs #631

🤖 Generated with Claude Code

Protect held execution rows through every database DELETE path.
Track monotonic ledger provenance for runtime enforcement in a
follow-up change.
@cristim cristim added urgency/this-sprint Within the current sprint triaged Item has been triaged priority/p1 Next up; this sprint severity/high Significant harm impact/many Affects most users effort/xl Multi-week / refactor type/feat New capability labels Oct 15, 2026
@coderabbitai

coderabbitai Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

  • Run on-demand review

This review includes 3 billable files and costs up to $0.75.

  • Ask an admin to make reviews automatic

Open in CodeRabbit

Reviews can continue after your included limit without a manual trigger. An admin must approve usage-based billing.

Or wait 54 minutes for your next included review.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available. Your 63 included PR review attempts over the past 7 days set your current allowance at 1 review per hour.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Repository: LeanerCloud/cloud-commitments-platform/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Essentials
  • Run ID: 877a69f9-e185-4017-bcf2-e0dad30626a7
📥 Commits

Reviewing files that changed from the base of the PR and between e003025 and 17a3377.

📒 Files selected for processing: 3
  • internal/config/store_postgres_retention_integration_test.go
  • internal/database/postgres/migrations/000124_ramp_ledger_retention.down.sql
  • internal/database/postgres/migrations/000124_ramp_ledger_retention.up.sql
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

@cristim

cristim commented Oct 15, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Review rate limited.

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 commented Oct 15, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Review rate limited.

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 left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Independent review of 17a3377: no actionable findings

Base is main (not stacked below this PR); #905 is stacked on it. origin/main is now d6ffd5d (#892, #899 merged). git merge-tree of origin/main with this SHA is clean, and neither merge touched migrations, so the verdict does not depend on a rebase.

Notes (all NON-BLOCKING)

  1. Merge order vs #896 (nit, but must be settled before merge). 000124_ramp_ledger_retention is the next free number on main (max 000122). #896 (open, draft) claims 000123. golang-migrate Up starts after the recorded version, so a database that reaches 124 first will never apply a later-merged 123. Merge #896 first, or renumber whichever lands second. Main has no gap guard.
  2. Deploy note is imprecise (nit, PR body). The DELETE guard and the legacy-default marker take effect for every writer the moment the migration runs, old Lambdas included: an old cleanup DELETE on a held row is silently skipped (fewer rows affected, no error), old INSERTs normalize to legacy, and an old baseline write degrades new to legacy. Nothing in this PR reads the marker, so no drain is needed for this PR alone. The drain window matters for #905 (runtime refuses untrusted legacy plans; old attachers ignore the marker), and the note should say that. The reverse direction is the real hazard: #905 code against a pre-migration schema fails on the missing column, and a rollback down drops the column while #905 is deployed. Down then up is lossy but fail-safe (everything returns to legacy; covered by TestRampRetention_DownUpKeepsExistingLineageUntrusted).
  3. guard_ramp_ledger_state runs on every purchase_plans write and casts JSON fields (::INT, ::BOOLEAN) with no WHEN clause. Probed with array/scalar services, string term, float total_steps, string enabled, null schedule: all inserts and updates succeed on legacy rows (the legacy branch short-circuits). Malformed JSON could only error on new/trusted rows, which only the Go runtime writes. No action.

Money-path check: can retention delete history still needed to avoid a double purchase?

  • Held rows of any plan whose total_steps > 1 AND current_step < total_steps are skipped by the BEFORE DELETE trigger (up.sql retain_ramp_ledger_execution), for every writer: CleanupOldExecutions, the cleanup-lambda statement, raw DELETE. Status classification mirrors holdsCapacity (unknown statuses count as held); parity test passes for canceled/cancelled/expired/failed with and without no_submission/future status. Empty, {}, null and [] recommendation payloads are retained.
  • Plan delete detaches rows via the existing ON DELETE SET NULL (migration 33), so it still succeeds and the plan's own ledger is moot. Account delete is already RESTRICT (migration 53). Detached, immediate and finished-ramp history still expires.
  • Ledger trust cannot be re-armed through SQL: legacy never promotes, trusted never returns to new, baseline replace/clear, progress rewind, leaving the unfinished schedule, and shape changes degrade to legacy, except the narrow fresh-edit exception (new, step 0, baseline NULL, no held execution). Forged ramp_ledger_state='trusted' on a legacy row is ignored.
  • Concurrency: the DELETE takes no parent lock (no lock-order reversal); an overlapping protected-exit sees the committed shape and over-retains. TestRampRetention_DeleteOverlapsProtectedExitWithoutParentLock covers it.
  • Residual, by design and outside this PR: old attachers still ignore the marker until #905 lands; this PR only guarantees the history is not lost meanwhile.

What I attacked

  • Ran go test -tags integration -run TestRampRetention ./internal/config/ on the exact SHA against Postgres containers: 22 passed.
  • Mutation probes on 000124_ramp_ledger_retention.up.sql (each reverted): < to <= in ramp_ledger_protected; dropped the failed AND no_submission exclusion in ramp_ledger_holds; trigger RETURN NULL to RETURN OLD; dropped the baseline-replacement degradation. All four made the suite fail.
  • Fail-before: without the migration the tests cannot run (functions and column absent); the original loss (completed row deleted at 31 days, ledger drops to 0) is the recorded RED in the plan, not re-run here.
  • Probed trigger robustness against malformed JSON (note 3). Down/up cycle exercised by the test itself.
  • Size: +305/-0, 3 files, test file 212 lines.

Gate status (origin/main Review gate)

  • Independent review at this SHA: this comment.
  • Findings resolved: none open. No unresolved threads. CodeRabbit shows "review rate limited" on two pings and has not reviewed; it is optional when exact-revision verification plus independent review cover the SHA.
  • CI on 17a3377: gh pr checks and gh run list show every check success, including Unit, Integration, E2E, Lint, pre-commit, Security, and CI Success.
  • Labels: match #631 (priority/p1, severity/high, urgency/this-sprint, impact/many, effort/xl, type/feat, triaged).
  • Local real-path evidence: PR body claims a local run against a real Postgres with all migrations applied; I reproduced it above. The body does not record the fail-before result or the mutation probes; this comment adds the probes.
  • Verdict and reviewed SHA: recorded here. A new commit or a non-empty range-diff on rebase restarts the gate.

@cristim

cristim commented Oct 16, 2026

Copy link
Copy Markdown
Member Author

Gate record for 17a3377 (origin/main CLAUDE.md "Review gate"):

Merging at 17a3377 with --match-head-commit. #905 stays open with changes requested.

@cristim
cristim merged commit 94a058c into main Oct 16, 2026
25 checks passed
@cristim
cristim deleted the fix/631-retention-ledger-db branch October 16, 2026 02:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/xl Multi-week / refactor impact/many Affects most users priority/p1 Next up; this sprint severity/high Significant harm triaged Item has been triaged type/feat New capability urgency/this-sprint Within the current sprint

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant