Budibase/budibase · error · HTTPError
Cannot delete rows through a calculation view
Error message
Cannot delete rows through a calculation view
What it means
The external (SQL-backed) row destroy endpoint refuses to delete rows when the request targets a calculation view. Calculation views are derived/aggregated read-only projections, so there is no underlying single row to delete through them. The server rejects the request up front with HTTP 400 rather than producing invalid SQL against the view.
Source
Thrown at packages/server/src/api/controllers/row/external.ts:119
outputProcessing(source, beforeRow, {
squash: true,
preserveLinks: true,
}),
])
return {
...response,
row: enrichedRow,
table,
oldRow,
}
}
export async function destroy(ctx: UserCtx) {
const source = await utils.getSource(ctx)
if (sdk.views.isView(source) && helpers.views.isCalculationView(source)) {
throw new HTTPError("Cannot delete rows through a calculation view", 400)
}
const _id = ctx.request.body._id
const { row } = await handleRequest(Operation.DELETE, source, {
id: breakRowIdField(_id),
includeSqlRelationships: IncludeRelationship.EXCLUDE,
})
return { response: { ok: true, id: _id }, row }
}
export async function bulkDestroy(ctx: UserCtx) {
const { rows } = ctx.request.body
const source = await utils.getSource(ctx)
let promises: Promise<{ row: Row; table: Table }>[] = []
for (let row of rows) {
promises.push(
handleRequest(Operation.DELETE, source, {
id: breakRowIdField(row._id),View on GitHub (pinned to a81a902e9a)
Solutions
- Delete using the underlying table ID (the table the calculation view is built on) rather than the view ID.
- Resolve the specific row's _id against the base table and issue the delete against that table.
- If rows must be 'removed' from a calculation view, adjust the view definition/source data so the row no longer matches, instead of deleting through the view.
Example fix
// before
deleteRow({ viewId: "vdatasource_view", rowId })
// after
const tableId = getTableIdFromViewId("vdatasource_view")
deleteRow({ tableId, rowId }) Defensive patterns
Strategy: validation
Validate before calling
// client-side check before DELETE
if (sourceId.startsWith("vd_") && isCalculationViewId(sourceId)) {
sourceId = getTableIdFromViewId(sourceId)
}
await api.delete(`/api/${sourceId}/rows/${rowId}`) Type guard
function isDeletableSource(source: Table | ViewV2): source is Table {
return !isView(source)
} Try / catch
try {
await api.delete(`/api/${sourceId}/rows/${rowId}`)
} catch (err) {
if (err.status === 400 && err.message.includes("calculation view")) {
// fall back to deleting via the base table
}
throw err
} Prevention
- Never pass view IDs to delete endpoints; always resolve to the base table ID.
- Hide/disable delete actions in UIs backed by calculation views.
- Keep a mapping of viewId -> tableId in client code for source resolution.
When it happens
Trigger: Calling DELETE on the external row API (e.g. DELETE /api/:tableId/rows or rows/:id) where the sourceId/tableId in the URL or body resolves to a calculation view instead of a table or ordinary view; the check `isView(source) && isCalculationView(source)` fires in destroy() at packages/server/src/api/controllers/row/external.ts:119.
Common situations: A client (automation, script, or SDK integration) was configured with a view ID instead of a table ID and the view happens to be a calculation view; UI lists rows from a calculation view and passes the view context through to the delete call; copying a working delete request for a table and swapping in a view ID.
Related errors
- Cannot delete rows through a calculation view
- sqs_error
- View name is required
- Import data or url is required
- Unsupported import type
AI-assisted analysis of Budibase/budibase@a81a902e9a (2026-08-29).
Data as JSON: /api/errors/80bd0121251eef6f.
Report an issue: GitHub.