Skip to content

Commit ed095a3

Browse files
fix(tables): probe for leftover rows on budget exhaustion to avoid needless deferral
1 parent 4ad8c0b commit ed095a3

2 files changed

Lines changed: 36 additions & 8 deletions

File tree

apps/sim/lib/cleanup/batch-delete.test.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* @vitest-environment node
33
*/
44

5-
import { dbChainMock, dbChainMockFns } from '@sim/testing'
5+
import { dbChainMock, dbChainMockFns, resetDbChainMock } from '@sim/testing'
66
import { beforeEach, describe, expect, it, vi } from 'vitest'
77

88
vi.mock('@sim/db', () => dbChainMock)
@@ -24,6 +24,7 @@ function returnRows(count: number) {
2424
describe('drainRowsByColumn', () => {
2525
beforeEach(() => {
2626
vi.clearAllMocks()
27+
resetDbChainMock()
2728
})
2829

2930
it('drains in batches until a short batch and reports the set fully drained', async () => {
@@ -37,17 +38,31 @@ describe('drainRowsByColumn', () => {
3738
expect(dbChainMockFns.returning).toHaveBeenCalledTimes(2)
3839
})
3940

40-
it('stops at the row budget and reports the set not fully drained', async () => {
41+
it('stops at the budget and reports not fully drained when rows remain', async () => {
4142
dbChainMockFns.returning
4243
.mockResolvedValueOnce(returnRows(2))
4344
.mockResolvedValueOnce(returnRows(2))
45+
// Existence probe after the budget is spent finds a leftover row.
46+
dbChainMockFns.limit.mockResolvedValue([{ id: 'leftover' }])
4447

4548
const result = await drainRowsByColumn({ ...baseOpts, batchSize: 2, rowBudget: 4 })
4649

4750
expect(result).toEqual({ deleted: 4, fullyDrained: false })
4851
expect(dbChainMockFns.returning).toHaveBeenCalledTimes(2)
4952
})
5053

54+
it('reports fully drained when the budget is hit but the set emptied exactly', async () => {
55+
dbChainMockFns.returning
56+
.mockResolvedValueOnce(returnRows(2))
57+
.mockResolvedValueOnce(returnRows(2))
58+
// Existence probe finds nothing remaining.
59+
dbChainMockFns.limit.mockResolvedValue([])
60+
61+
const result = await drainRowsByColumn({ ...baseOpts, batchSize: 2, rowBudget: 4 })
62+
63+
expect(result).toEqual({ deleted: 4, fullyDrained: true })
64+
})
65+
5166
it('reports fully drained immediately when the match set is already empty', async () => {
5267
dbChainMockFns.returning.mockResolvedValueOnce([])
5368

apps/sim/lib/cleanup/batch-delete.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -279,10 +279,11 @@ export interface DrainByColumnOptions {
279279
export interface DrainResult {
280280
deleted: number
281281
/**
282-
* True only when the match set was confirmed empty (a short final batch).
283-
* Budget exhaustion and batch errors both yield `false` — callers must treat
284-
* `false` as "rows may remain" and defer any dependent parent-delete (whose
285-
* ON DELETE CASCADE would otherwise fire on the leftovers) to a later run.
282+
* True only when the match set was confirmed empty — via a short final batch
283+
* or an existence probe after the budget was spent. Batch errors yield
284+
* `false`; callers must treat `false` as "rows may remain" and defer any
285+
* dependent parent-delete (whose ON DELETE CASCADE would otherwise fire on the
286+
* leftovers) to a later run.
286287
*/
287288
fullyDrained: boolean
288289
}
@@ -331,6 +332,18 @@ export async function drainRowsByColumn({
331332
if (batchDeleted.length < limit) return { deleted, fullyDrained: true }
332333
}
333334

334-
// Hit the per-call budget on a full batch — rows may remain.
335-
return { deleted, fullyDrained: false }
335+
// Budget hit on a full final batch — rows may or may not remain. A cheap
336+
// indexed existence probe disambiguates so a set whose size divides the budget
337+
// exactly isn't needlessly deferred to a later run.
338+
try {
339+
const [leftover] = await db
340+
.select({ id: idCol })
341+
.from(tableDef)
342+
.where(eq(matchCol, matchValue))
343+
.limit(1)
344+
return { deleted, fullyDrained: !leftover }
345+
} catch (error) {
346+
logger.error(`[${tableName}] Drain remainder probe failed for ${matchValue}:`, { error })
347+
return { deleted, fullyDrained: false }
348+
}
336349
}

0 commit comments

Comments
 (0)