Skip to content

Commit 5ee756f

Browse files
authored
chore(table): remove unused table-update background task (#8708)
1 parent 6adf4c6 commit 5ee756f

7 files changed

Lines changed: 4 additions & 891 deletions

File tree

‎apps/sim/background/table-update.ts‎

Lines changed: 0 additions & 45 deletions
This file was deleted.

‎apps/sim/lib/table/delete-runner.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ export async function runTableDelete(payload: TableDeletePayload): Promise<void>
119119
: undefined
120120
// A filter that was SUPPLIED but compiles to no clause must never widen into
121121
// "delete every row" — `and()` silently drops an undefined clause downstream.
122-
// Mirrors the guard in update-runner and the inline deleteRowsByFilter path;
122+
// Mirrors the guard in the inline deleteRowsByFilter path;
123123
// an absent filter is still legitimate (delete-all is an explicit caller mode).
124124
if (filter && !filterClause) throw new Error('Filter is required for bulk delete')
125125
const excluded = new Set(excludeRowIds ?? [])

‎apps/sim/lib/table/rows/ordering.ts‎

Lines changed: 0 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -671,67 +671,3 @@ export async function deletePageByIds(
671671
}
672672
return deleted
673673
}
674-
675-
/** The patch one update batch writes, or `null` when it writes nothing. */
676-
export interface PagePatch {
677-
patchJson: string
678-
secretProvenance: TableRowSecretProvenanceWrite
679-
}
680-
681-
/**
682-
* Applies a JSONB-merge patch (`data || patchJson`) to a page of row ids, committed in
683-
* UPDATE_BATCH_SIZE chunks (each its own transaction, 60s timeout) so a large background update
684-
* makes incremental, resumable progress. Each batch takes its patch from `prepare`, called inside
685-
* the batch's transaction with the definition `revalidate` read there and the batch's row ids, so a
686-
* caller can derive it, and check the rows it merges into, against the live schema. Returns the
687-
* number of rows updated.
688-
*/
689-
export async function updatePageByIds(
690-
tableId: string,
691-
workspaceId: string,
692-
rowIds: string[],
693-
prepare: (
694-
trx: DbTransaction,
695-
table: TableDefinition | undefined,
696-
batch: string[]
697-
) => Promise<PagePatch | null>,
698-
/** Proof the caller asserted the update lock (see `mutation-locks.ts`). */
699-
_proof: MutationProof<'update'>,
700-
/** Re-asserts the lock inside each batch transaction. See {@link guardBatch}. */
701-
revalidate?: MutationRevalidator
702-
): Promise<number> {
703-
const now = new Date()
704-
let updated = 0
705-
for (let i = 0; i < rowIds.length; i += TABLE_LIMITS.UPDATE_BATCH_SIZE) {
706-
const batch = rowIds.slice(i, i + TABLE_LIMITS.UPDATE_BATCH_SIZE)
707-
const rows = await db.transaction(async (trx) => {
708-
await setTableTxTimeouts(trx, { statementMs: 60_000 })
709-
const patch = await prepare(trx, await guardBatch(trx, tableId, revalidate), batch)
710-
if (!patch) return []
711-
return mutateTableRowsWithSecretProvenance(trx, {
712-
rows: batch.map((rowId) => ({ rowId, provenance: patch.secretProvenance })),
713-
rowState: 'existing',
714-
mode: 'merge',
715-
mutate: async () => {
716-
const rows = await trx
717-
.update(userTableRows)
718-
.set({
719-
data: sql`${userTableRows.data} || ${patch.patchJson}::jsonb`,
720-
updatedAt: now,
721-
})
722-
.where(
723-
and(
724-
eq(userTableRows.tableId, tableId),
725-
eq(userTableRows.workspaceId, workspaceId),
726-
inArray(userTableRows.id, batch)
727-
)
728-
)
729-
.returning({ id: userTableRows.id })
730-
return { value: rows, affectedRowIds: rows.map((row) => row.id) }
731-
},
732-
})
733-
})
734-
updated += rows.length
735-
}
736-
return updated
737-
}

‎apps/sim/lib/table/rows/row-writes.integration.ts‎

Lines changed: 2 additions & 248 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,9 @@ vi.mock('@/lib/table/trigger', () => tableTriggerMock)
2222
vi.mock('@/lib/table/workflow-columns', () => tableWorkflowColumnsMock)
2323
vi.mock('@/lib/uploads/core/storage-service', () => storageServiceMock)
2424

25-
import {
26-
deleteColumn,
27-
updateColumnConstraints,
28-
updateColumnOptions,
29-
} from '@/lib/table/columns/service'
30-
import { getMaxRowSizeBytes, TABLE_LIMITS } from '@/lib/table/constants'
25+
import { deleteColumn, updateColumnConstraints } from '@/lib/table/columns/service'
26+
import { getMaxRowSizeBytes } from '@/lib/table/constants'
3127
import { bulkInsertImportBatch, importReplaceRows } from '@/lib/table/import-data'
32-
import { markTableJobRunningInWorkspace } from '@/lib/table/jobs/service'
3328
import type { DbTransaction } from '@/lib/table/planner'
3429
import { lockLiveTableSchema } from '@/lib/table/rows/live-schema'
3530
import { acquireRowOrderLock } from '@/lib/table/rows/ordering'
@@ -46,7 +41,6 @@ import { lockUniqueColumns, lockUniqueValues } from '@/lib/table/rows/unique-loc
4641
import { getTableById } from '@/lib/table/service'
4742
import { getOrCreateTableSnapshot } from '@/lib/table/snapshot-cache'
4843
import type { ColumnDefinition, JsonValue, RowData, TableDefinition } from '@/lib/table/types'
49-
import { runTableUpdate, UpdatePatchRejectedError } from '@/lib/table/update-runner'
5044

5145
const url = readTestDatabaseUrl()
5246
if (process.env.DATABASE_URL !== url) {
@@ -1388,246 +1382,6 @@ describe('table row writes against real PostgreSQL', () => {
13881382
})
13891383
})
13901384

1391-
describe('background updates validate their patch against the live schema', () => {
1392-
const columns: ColumnDefinition[] = [
1393-
{ id: 'email', name: 'email', type: 'string' },
1394-
{ id: 'kind', name: 'kind', type: 'string' },
1395-
{
1396-
id: 'status',
1397-
name: 'status',
1398-
type: 'select',
1399-
options: [{ id: 'opt_open', name: 'Open' }],
1400-
},
1401-
{ id: 'note', name: 'note', type: 'string' },
1402-
{ id: 'due', name: 'due', type: 'date' },
1403-
]
1404-
/** Two update batches' worth of rows. */
1405-
const ROWS = TABLE_LIMITS.UPDATE_BATCH_SIZE + 50
1406-
1407-
async function seededJob(data: RowData) {
1408-
const table = await createTable(columns)
1409-
await control`INSERT INTO user_table_rows (id, table_id, workspace_id, data, position, order_key)
1410-
SELECT ${table.id} || '-' || lpad(g::text, 4, '0'), ${table.id}, ${workspaceId},
1411-
jsonb_build_object('email', 'e' || g || '@example.test', 'kind', 'seed'), g, 'a' || lpad(g::text, 4, '0')
1412-
FROM generate_series(1, ${ROWS}) g`
1413-
const jobId = generateId()
1414-
const filter = { kind: 'seed' }
1415-
expect(
1416-
await markTableJobRunningInWorkspace(table.id, workspaceId, jobId, 'update', {
1417-
filter,
1418-
data,
1419-
})
1420-
).toBe(true)
1421-
const run = () =>
1422-
runTableUpdate({
1423-
jobId,
1424-
tableId: table.id,
1425-
workspaceId,
1426-
filter,
1427-
data: { ...data },
1428-
cutoff: new Date(),
1429-
})
1430-
/** Re-runs the job as a retry after a crash would: the job is still running. */
1431-
const retry = async () => {
1432-
await control`UPDATE table_jobs SET status = 'running', completed_at = NULL
1433-
WHERE id = ${jobId}`
1434-
return run()
1435-
}
1436-
return { table, run, retry }
1437-
}
1438-
1439-
/**
1440-
* Commits `change` between the job's first and second batch. A held schema lock stops the
1441-
* first batch; `change` then queues behind it, and a waiting lock is granted in queue order, so
1442-
* the first batch commits, then `change`, then the second batch.
1443-
*/
1444-
async function changeBetweenBatches(
1445-
tableId: string,
1446-
run: () => Promise<void>,
1447-
change: () => Promise<unknown>
1448-
): Promise<PromiseSettledResult<unknown>[]> {
1449-
const holder = await control.reserve()
1450-
try {
1451-
await holder`BEGIN`
1452-
await holder`SELECT pg_advisory_xact_lock(hashtextextended(${`user_table_schema:${tableId}`}, 0))`
1453-
const job = run()
1454-
await untilSchemaLockWaiters(tableId, 1)
1455-
const changed = change()
1456-
await untilSchemaLockWaiters(tableId, 2)
1457-
await holder`COMMIT`
1458-
return await Promise.allSettled([job, changed])
1459-
} finally {
1460-
await holder`ROLLBACK`.catch(() => {})
1461-
holder.release()
1462-
}
1463-
}
1464-
1465-
/** Commits `schema = <change>` for the table under its exclusive schema lock. */
1466-
async function changeSchemaUnderLock(tableId: string, change: string): Promise<void> {
1467-
const changer = await control.reserve()
1468-
try {
1469-
await changer`BEGIN`
1470-
await changer`SELECT pg_advisory_xact_lock(hashtextextended(${`user_table_schema:${tableId}`}, 0))`
1471-
await changer.unsafe(`UPDATE user_table_definitions SET schema = ${change} WHERE id = $1`, [
1472-
tableId,
1473-
])
1474-
await changer`COMMIT`
1475-
} finally {
1476-
changer.release()
1477-
}
1478-
}
1479-
1480-
async function countEmail(tableId: string, email: string): Promise<number> {
1481-
const [{ count }] = await control<{ count: number }[]>`SELECT count(*)::int AS count
1482-
FROM user_table_rows WHERE table_id = ${tableId} AND data->>'email' = ${email}`
1483-
return count
1484-
}
1485-
1486-
it('refuses a patch to a column made unique before the job started, retry included', async () => {
1487-
const { table, run } = await seededJob({ email: 'same@example.test' })
1488-
await updateColumnConstraints(
1489-
{ tableId: table.id, columnName: 'email', unique: true },
1490-
'bulk-update'
1491-
)
1492-
1493-
await expect(run()).rejects.toBeInstanceOf(UpdatePatchRejectedError)
1494-
await expect(run()).rejects.toBeInstanceOf(UpdatePatchRejectedError)
1495-
expect(await countEmail(table.id, 'same@example.test')).toBe(0)
1496-
})
1497-
1498-
it('refuses the next batch once a column it clears is made required mid-job', async () => {
1499-
const { table, run } = await seededJob({ note: null })
1500-
await control`UPDATE user_table_rows SET data = data || '{"note":"filled"}' WHERE table_id = ${table.id}`
1501-
1502-
const [job, change] = await changeBetweenBatches(table.id, run, async () => {
1503-
const changer = await control.reserve()
1504-
try {
1505-
await changer`BEGIN`
1506-
await changer`SELECT pg_advisory_xact_lock(hashtextextended(${`user_table_schema:${table.id}`}, 0))`
1507-
await changer`UPDATE user_table_definitions
1508-
SET schema = jsonb_set(schema, '{columns,3,required}', 'true') WHERE id = ${table.id}`
1509-
await changer`COMMIT`
1510-
} finally {
1511-
changer.release()
1512-
}
1513-
})
1514-
1515-
expect(change.status).toBe('fulfilled')
1516-
expect(job.status === 'rejected' && job.reason).toBeInstanceOf(UpdatePatchRejectedError)
1517-
const [{ count }] = await control<{ count: number }[]>`SELECT count(*)::int AS count
1518-
FROM user_table_rows WHERE table_id = ${table.id} AND data->'note' = 'null'::jsonb`
1519-
expect(count).toBe(TABLE_LIMITS.UPDATE_BATCH_SIZE)
1520-
})
1521-
1522-
it('refuses the next batch once a patched column is made unique mid-job, retry included', async () => {
1523-
const { table, run } = await seededJob({ email: 'same@example.test' })
1524-
1525-
const [job, change] = await changeBetweenBatches(table.id, run, () =>
1526-
changeSchemaUnderLock(table.id, `jsonb_set(schema, '{columns,0,unique}', 'true')`)
1527-
)
1528-
1529-
expect(change.status).toBe('fulfilled')
1530-
expect(job.status === 'rejected' && job.reason).toBeInstanceOf(UpdatePatchRejectedError)
1531-
await expect(run()).rejects.toBeInstanceOf(UpdatePatchRejectedError)
1532-
expect(await countEmail(table.id, 'same@example.test')).toBe(TABLE_LIMITS.UPDATE_BATCH_SIZE)
1533-
})
1534-
1535-
it('finishes when a column it writes gains a select option mid-job', async () => {
1536-
const { table, run } = await seededJob({ status: 'Open' })
1537-
1538-
const [job, change] = await changeBetweenBatches(table.id, run, () =>
1539-
updateColumnOptions(
1540-
{
1541-
tableId: table.id,
1542-
columnName: 'status',
1543-
options: [
1544-
{ id: 'opt_open', name: 'Open' },
1545-
{ id: 'opt_closed', name: 'Closed' },
1546-
],
1547-
},
1548-
'bulk-update'
1549-
)
1550-
)
1551-
1552-
expect(change.status).toBe('fulfilled')
1553-
expect(job.status).toBe('fulfilled')
1554-
const [{ count }] = await control<{ count: number }[]>`SELECT count(*)::int AS count
1555-
FROM user_table_rows WHERE table_id = ${table.id} AND data->>'status' = 'opt_open'`
1556-
expect(count).toBe(ROWS)
1557-
})
1558-
1559-
it('re-derives the patch for later batches once a patched column is retyped mid-job', async () => {
1560-
const { table, run, retry } = await seededJob({ note: '7' })
1561-
const notes = async () =>
1562-
control<{ kind: string; count: number }[]>`SELECT jsonb_typeof(data->'note') AS kind,
1563-
count(*)::int AS count FROM user_table_rows WHERE table_id = ${table.id}
1564-
GROUP BY 1 ORDER BY 1`
1565-
1566-
const [job, change] = await changeBetweenBatches(table.id, run, () =>
1567-
changeSchemaUnderLock(table.id, `jsonb_set(schema, '{columns,3,type}', '"number"')`)
1568-
)
1569-
1570-
expect([job.status, change.status]).toEqual(['fulfilled', 'fulfilled'])
1571-
expect(await notes()).toEqual([
1572-
{ kind: 'number', count: ROWS - TABLE_LIMITS.UPDATE_BATCH_SIZE },
1573-
{ kind: 'string', count: TABLE_LIMITS.UPDATE_BATCH_SIZE },
1574-
])
1575-
const [{ seven }] = await control<{ seven: number }[]>`SELECT count(*)::int AS seven
1576-
FROM user_table_rows WHERE table_id = ${table.id} AND data->'note' IN ('"7"', '7')`
1577-
expect(seven).toBe(ROWS)
1578-
1579-
await expect(retry()).resolves.toBeUndefined()
1580-
expect(await notes()).toEqual([{ kind: 'number', count: ROWS }])
1581-
})
1582-
1583-
it('drops a column deleted mid-job from later batches', async () => {
1584-
const { table, run } = await seededJob({ note: 'x', email: 'y@example.test' })
1585-
1586-
const [job, change] = await changeBetweenBatches(table.id, run, () =>
1587-
changeSchemaUnderLock(table.id, `schema #- '{columns,3}'`)
1588-
)
1589-
1590-
expect([job.status, change.status]).toEqual(['fulfilled', 'fulfilled'])
1591-
expect(await countEmail(table.id, 'y@example.test')).toBe(ROWS)
1592-
const [{ count }] = await control<{ count: number }[]>`SELECT count(*)::int AS count
1593-
FROM user_table_rows WHERE table_id = ${table.id} AND data->>'note' = 'x'`
1594-
expect(count).toBe(TABLE_LIMITS.UPDATE_BATCH_SIZE)
1595-
})
1596-
1597-
it('refuses a batch whose re-derived patch grows a row past the size limit', async () => {
1598-
const { table, run } = await seededJob({ note: 7 })
1599-
await changeSchemaUnderLock(table.id, `jsonb_set(schema, '{columns,3,type}', '"number"')`)
1600-
const bigRowId = `${table.id}-${String(TABLE_LIMITS.UPDATE_BATCH_SIZE + 1).padStart(4, '0')}`
1601-
// Stored jsonb orders keys by length, so a merged row reads kind, email, filler, then note.
1602-
const email = `e${TABLE_LIMITS.UPDATE_BATCH_SIZE + 1}@example.test`
1603-
const base = Buffer.byteLength(JSON.stringify({ kind: 'seed', email, filler: '', note: 7 }))
1604-
await control`UPDATE user_table_rows
1605-
SET data = data || jsonb_build_object('filler', repeat('x', ${getMaxRowSizeBytes() - base}))
1606-
WHERE id = ${bigRowId}`
1607-
1608-
const [job, change] = await changeBetweenBatches(table.id, run, () =>
1609-
changeSchemaUnderLock(table.id, `jsonb_set(schema, '{columns,3,type}', '"string"')`)
1610-
)
1611-
1612-
expect(change.status).toBe('fulfilled')
1613-
expect(job.status === 'rejected' && job.reason).toBeInstanceOf(UpdatePatchRejectedError)
1614-
const [{ count }] = await control<{ count: number }[]>`SELECT count(*)::int AS count
1615-
FROM user_table_rows WHERE table_id = ${table.id} AND data ? 'note'`
1616-
expect(count).toBe(TABLE_LIMITS.UPDATE_BATCH_SIZE)
1617-
})
1618-
1619-
it('writes a date patch given as an epoch number', async () => {
1620-
const { table, run } = await seededJob({ due: 1704067200000 })
1621-
1622-
await run()
1623-
1624-
const [{ count }] = await control<{ count: number }[]>`SELECT count(*)::int AS count
1625-
FROM user_table_rows WHERE table_id = ${table.id}
1626-
AND (data->>'due')::timestamptz = '2024-01-01T00:00:00Z'`
1627-
expect(count).toBe(ROWS)
1628-
})
1629-
})
1630-
16311385
describe.skipIf(!migrated)('rows_version', () => {
16321386
it('advances once for a transaction that edits cells across several statements', async () => {
16331387
const table = await createTable(textColumns('name'))

0 commit comments

Comments
 (0)