improvement(tables): drain rows in batches before hard-deleting archived tables#4894
improvement(tables): drain rows in batches before hard-deleting archived tables#4894TheodoreSpeaks wants to merge 6 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
PR SummaryMedium Risk Overview A new If the budget runs out, a table errors mid-drain, or the guarded definition delete no-ops, cleanup defers that table to a later cron run instead of forcing a large cascade. Reviewed by Cursor Bugbot for commit e5d3eb8. Bugbot is set up for automated code reviews on this repo. Configure here. |
|
@greptile review |
Greptile SummaryThis PR replaces the single-statement ON DELETE CASCADE path for archived
Confidence Score: 5/5Safe to merge. The drain-before-delete flow is logically sound, the per-batch guard and guarded definition delete form two independent layers of protection against the restore race, and all critical edge cases are covered by tests. The three previously-flagged issues (unsafe error return, off-by-one on exact-divisor budget, stale archive eligibility after restore) are all addressed in this revision. Error handling correctly returns fullyDrained: false. The existence probe after budget exhaustion disambiguates the divisible case. The per-batch EXISTS guard plus the conditional RETURNING delete on the definition form a solid two-layer guard against concurrent restores. No remaining logic gaps or unsafe paths identified. No files require special attention. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A([cleanupArchivedUserTables]) --> B[SELECT archived+expired table IDs]
B --> C{doomedTables empty?}
C -- Yes --> Z([return 0])
C -- No --> D[rowBudget = 1_000_000]
D --> E{For each tableId\nrowBudget > 0?}
E -- No --> LOG
E -- Yes --> F[Build stillArchived EXISTS guard]
F --> G[drainRowsByColumn\nwith guard + budget]
G --> H{batch DELETE\nsuccess?}
H -- Error --> I[return fullyDrained: false\ndeleted so far]
H -- OK --> J{batchDeleted.length\n< limit?}
J -- Yes short batch --> K[return fullyDrained: true]
J -- No full batch --> L{remaining == 0?}
L -- No --> H
L -- Yes budget hit --> M[Existence probe\nSELECT 1 WHERE matchCol = val]
M --> N{leftover row?}
N -- No --> O[fullyDrained: true]
N -- Yes --> P[fullyDrained: false]
I --> Q{drain.fullyDrained?}
K --> Q
O --> Q
P --> Q
Q -- true --> R["db.delete(userTableDefinitions)\nWHERE id=? AND archivedAt IS NOT NULL\nRETURNING id"]
R --> S{deletedDef.length == 1?}
S -- Yes --> T[definitionsDeleted++]
S -- No restored table no-op --> E
T --> E
Q -- false --> U{rowBudget <= 0?}
U -- Yes --> LOG
U -- No error continue --> E
LOG([log + return definitionsDeleted])
Reviews (6): Last reviewed commit: "fix(tables): guard drain and definition ..." | Re-trigger Greptile |
Greptile SummaryThis PR replaces a single-statement hard-delete of archived
Confidence Score: 3/5The drain logic is architecturally correct, but a transient database error during a batch delete causes the definition to be hard-deleted with rows still present, firing the same large cascade the PR was designed to prevent. The error path in apps/sim/lib/cleanup/batch-delete.ts — the Important Files Changed
Sequence DiagramsequenceDiagram
participant Cron as Cron Job
participant CSD as cleanupArchivedUserTables
participant DRC as drainRowsByColumn
participant DB as Postgres
Cron->>CSD: runCleanupSoftDeletes(payload)
CSD->>DB: "SELECT id FROM userTableDefinitions WHERE archivedAt < retentionDate"
DB-->>CSD: [tbl-1, tbl-2, ...]
loop for each archived table
CSD->>DRC: "drainRowsByColumn({ tableId, rowBudget })"
loop "while remaining > 0"
DRC->>DB: DELETE FROM userTableRows WHERE id IN (SELECT id ... LIMIT batch) RETURNING id
DB-->>DRC: deleted rows (cascades table_row_executions)
alt short batch — set exhausted
DRC-->>CSD: "{ deleted, budgetExhausted: false }"
else budget hit
DRC-->>CSD: "{ deleted, budgetExhausted: true }"
else DB error
DRC-->>CSD: "{ deleted, budgetExhausted: false } ⚠️"
end
end
alt "budgetExhausted = false"
CSD->>DB: "DELETE FROM userTableDefinitions WHERE id = tableId"
else "budgetExhausted = true"
CSD-->>Cron: defer definition to next run
end
end
CSD-->>Cron: definitionsDeleted count
Reviews (1): Last reviewed commit: "improvement(tables): drain rows in batch..." | Re-trigger Greptile |
|
Good catch — both Bugbot and Greptile flagged the same real bug. Fixed in d660542.
Re the P2 exact-budget edge: when a table's row count equals the budget exactly, it now defers by one extra cron run (safe, harmless) rather than risk an unsafe delete — accepting that over an extra COUNT probe. @greptile review |
|
Addressed the Bugbot Medium ("Failed drain aborts later tables") in 4ad8c0b. The loop no longer @greptile review |
|
Addressed Bugbot's Medium ("Budget exhaustion misreports empty drain") in ed095a3. When the per-run budget is spent on a full final batch, Note: the two Greptile P1 comments still referencing @greptile review |
|
Fixed the CI build failure in b26cbda. Root cause: an earlier Bugbot now passes; the remaining Greptile comments reference the old |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit b26cbda. Configure here.
|
Addressed Bugbot's High ("Stale archive eligibility after restore") in e5d3eb8. The snapshot race is now closed atomically:
Added the optional Test and Build is green; the remaining Greptile comments reference the old @greptile review |
|
✅ CI is green on All review findings addressed across the iterations:
The two remaining Greptile threads reference Ready for human review. |

Summary
user_table_rowsin bounded 5k-row batches (each its own transaction) before hard-deleting an archived table definition, so the FK cascade fires on an empty/tiny set instead of millions of rows in one statementuserTableDefinitionsfrom the genericCLEANUP_TARGETSsweep; it now goes through a dedicated drain-then-delete path incleanup-soft-deletestable_row_executionsride along via their cascadingrowIdFK, so they're bounded per batch toodrainRowsByColumnprimitive inbatch-delete.tsType of Change
Testing
drainRowsByColumn(4 cases) + integration tests for drain-before-delete ordering and budget-exhaustion deferral (10 tests total, passing)bun run lintclean,bun run check:api-validation:strictpassedChecklist