{"record":{"id":"80bd0121251eef6f","repo":"Budibase/budibase","slug":"cannot-delete-rows-through-a-calculation-view","errorCode":null,"errorMessage":"Cannot delete rows through a calculation view","messagePattern":"Cannot delete rows through a calculation view","errorType":"http","errorClass":"HTTPError","httpStatus":400,"severity":"error","filePath":"packages/server/src/api/controllers/row/external.ts","lineNumber":119,"sourceCode":"    outputProcessing(source, beforeRow, {\n      squash: true,\n      preserveLinks: true,\n    }),\n  ])\n\n  return {\n    ...response,\n    row: enrichedRow,\n    table,\n    oldRow,\n  }\n}\n\nexport async function destroy(ctx: UserCtx) {\n  const source = await utils.getSource(ctx)\n\n  if (sdk.views.isView(source) && helpers.views.isCalculationView(source)) {\n    throw new HTTPError(\"Cannot delete rows through a calculation view\", 400)\n  }\n\n  const _id = ctx.request.body._id\n  const { row } = await handleRequest(Operation.DELETE, source, {\n    id: breakRowIdField(_id),\n    includeSqlRelationships: IncludeRelationship.EXCLUDE,\n  })\n  return { response: { ok: true, id: _id }, row }\n}\n\nexport async function bulkDestroy(ctx: UserCtx) {\n  const { rows } = ctx.request.body\n  const source = await utils.getSource(ctx)\n  let promises: Promise<{ row: Row; table: Table }>[] = []\n  for (let row of rows) {\n    promises.push(\n      handleRequest(Operation.DELETE, source, {\n        id: breakRowIdField(row._id),","sourceCodeStart":101,"sourceCodeEnd":137,"githubUrl":"https://github.com/Budibase/budibase/blob/a81a902e9a8fe55b467d106765f6638f12e35c49/packages/server/src/api/controllers/row/external.ts#L101-L137","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\ndeleteRow({ viewId: \"vdatasource_view\", rowId })\n// after\nconst tableId = getTableIdFromViewId(\"vdatasource_view\")\ndeleteRow({ tableId, rowId })","handlingStrategy":"validation","validationCode":"// client-side check before DELETE\nif (sourceId.startsWith(\"vd_\") && isCalculationViewId(sourceId)) {\n  sourceId = getTableIdFromViewId(sourceId)\n}\nawait api.delete(`/api/${sourceId}/rows/${rowId}`)","typeGuard":"function isDeletableSource(source: Table | ViewV2): source is Table {\n  return !isView(source)\n}","tryCatchPattern":"try {\n  await api.delete(`/api/${sourceId}/rows/${rowId}`)\n} catch (err) {\n  if (err.status === 400 && err.message.includes(\"calculation view\")) {\n    // fall back to deleting via the base table\n  }\n  throw err\n}","preventionTips":["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."],"tags":["rest-api","views","sql","bad-request"],"backgroundTag":"delete-through-calculation-view","analyzedSha":"a81a902e9a8fe55b467d106765f6638f12e35c49","analyzedAt":"2026-08-29T01:03:10.972Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}