Skip to content

Commit 4ad8c0b

Browse files
fix(tables): skip an errored table mid-drain instead of aborting the whole run
1 parent d660542 commit 4ad8c0b

2 files changed

Lines changed: 30 additions & 10 deletions

File tree

apps/sim/background/cleanup-soft-deletes.test.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -177,15 +177,16 @@ describe('cleanup soft deletes — archived user tables', () => {
177177
vi.clearAllMocks()
178178
mockIsUsingCloudStorage.mockReturnValue(true)
179179
mockLimit.mockResolvedValue([])
180-
// selectRowsByIdChunks call order: workflows, file legacy, file multi, doomed tables.
180+
// selectRowsByIdChunks call order: workflows, file legacy, file multi, doomed
181+
// tables. Each test queues the doomed-tables result (4th call) itself.
181182
mockSelectRowsByIdChunks
182183
.mockResolvedValueOnce([])
183184
.mockResolvedValueOnce([])
184185
.mockResolvedValueOnce([])
185-
.mockResolvedValueOnce([{ id: 'tbl-1' }])
186186
})
187187

188188
it('drains rows before deleting the table definition', async () => {
189+
mockSelectRowsByIdChunks.mockResolvedValueOnce([{ id: 'tbl-1' }])
189190
mockDrainRowsByColumn.mockResolvedValue({ deleted: 5, fullyDrained: true })
190191

191192
await runCleanupSoftDeletes(basePayload)
@@ -200,13 +201,28 @@ describe('cleanup soft deletes — archived user tables', () => {
200201
)
201202
})
202203

203-
it('defers the definition delete when the drain does not fully complete', async () => {
204-
// Budget stop or a drain error both surface as fullyDrained: false.
205-
mockDrainRowsByColumn.mockResolvedValue({ deleted: 200_000, fullyDrained: false })
204+
it('defers the definition delete when the budget is exhausted', async () => {
205+
mockSelectRowsByIdChunks.mockResolvedValueOnce([{ id: 'tbl-1' }])
206+
// Budget stop consumes the whole budget — deleted equals the per-run cap.
207+
mockDrainRowsByColumn.mockResolvedValue({ deleted: 1_000_000, fullyDrained: false })
206208

207209
await runCleanupSoftDeletes(basePayload)
208210

209211
expect(mockDrainRowsByColumn).toHaveBeenCalledTimes(1)
210212
expect(mockDeleteDefinition).not.toHaveBeenCalled()
211213
})
214+
215+
it('skips a table that errors mid-drain but keeps cleaning the rest', async () => {
216+
mockSelectRowsByIdChunks.mockResolvedValueOnce([{ id: 'tbl-err' }, { id: 'tbl-ok' }])
217+
// First table errors mid-drain (budget remains); second drains fully.
218+
mockDrainRowsByColumn
219+
.mockResolvedValueOnce({ deleted: 2, fullyDrained: false })
220+
.mockResolvedValueOnce({ deleted: 5, fullyDrained: true })
221+
222+
await runCleanupSoftDeletes(basePayload)
223+
224+
expect(mockDrainRowsByColumn).toHaveBeenCalledTimes(2)
225+
// Only the fully-drained table's definition is deleted.
226+
expect(mockDeleteDefinition).toHaveBeenCalledTimes(1)
227+
})
212228
})

apps/sim/background/cleanup-soft-deletes.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -307,12 +307,16 @@ async function cleanupArchivedUserTables(
307307
})
308308
rowBudget -= drain.deleted
309309

310-
// Only delete the definition once its rows are provably gone. A budget stop
311-
// or a drain error leaves rows behind, so defer to keep the cascade small.
312-
if (!drain.fullyDrained) break
310+
if (drain.fullyDrained) {
311+
await db.delete(userTableDefinitions).where(eq(userTableDefinitions.id, tableId))
312+
definitionsDeleted++
313+
continue
314+
}
313315

314-
await db.delete(userTableDefinitions).where(eq(userTableDefinitions.id, tableId))
315-
definitionsDeleted++
316+
// Not fully drained: a budget stop consumes the whole budget, so a positive
317+
// remainder means this one table errored mid-drain. Leave its definition for
318+
// a later run and keep cleaning the rest; only stop once the budget is spent.
319+
if (rowBudget <= 0) break
316320
}
317321

318322
logger.info(

0 commit comments

Comments
 (0)