{"record":{"id":"38b4bb6ad37f0c4a","repo":"Budibase/budibase","slug":"cannot-delete-rows-through-a-calculation-view-38b4bb","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/internal.ts","lineNumber":105,"sourceCode":"    ctx.request.body = row as any\n    await userController.updateMetadata(ctx as any)\n    return { row: ctx.body as Row, table, oldRow }\n  }\n\n  const result = await finaliseRow(source, row, {\n    updateFormula: true,\n    updateAIColumns: true,\n  })\n\n  return { ...result, oldRow }\n}\n\nexport async function destroy(ctx: UserCtx) {\n  const db = context.getWorkspaceDB()\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  let table: Table\n  if (sdk.views.isView(source)) {\n    table = await sdk.views.getTable(source.id)\n  } else {\n    table = source\n  }\n\n  const { _id } = ctx.request.body\n  let row = await db.get<Row>(_id)\n  let _rev = ctx.request.body._rev || row._rev\n\n  if (row.tableId !== table._id) {\n    throw \"Supplied tableId doesn't match the row's tableId\"\n  }\n  // update the row to include full relationships before deleting them\n  row = await outputProcessing(table, row, {","sourceCodeStart":87,"sourceCodeEnd":123,"githubUrl":"https://github.com/Budibase/budibase/blob/a81a902e9a8fe55b467d106765f6638f12e35c49/packages/server/src/api/controllers/row/internal.ts#L87-L123","documentation":"The internal row destroy endpoint performs the same guard as the external one: deleting rows through a calculation view is not permitted because calculation views are read-only derived datasets. The request is rejected with HTTP 400 before any table lookup or deletion happens.","triggerScenarios":"Issuing an internal-app DELETE row request (e.g. from the builder or a published app through /api/.../rows) where the resolved source is a view and helpers.views.isCalculationView(source) is true; checked in destroy() at packages/server/src/api/controllers/row/internal.ts:105.","commonSituations":"A user deletes a row from a grid backed by a calculation view; automations or custom code reuse a view ID in place of a table ID; a recent app change repointed a screen's data provider from a table to a calculation view while delete actions remained wired up.","solutions":["Point the delete action at the base table ID, not the calculation view ID.","Remove or reconfigure delete buttons/actions on screens backed by calculation views since they are not deletable.","If deletion is required, build the screen against the underlying table or a non-calculation view."],"exampleFix":"// before\nawait api.delete(`/api/${viewId}/rows/${rowId}`)\n// after\nawait api.delete(`/api/${tableId}/rows/${rowId}`)","handlingStrategy":"validation","validationCode":"// before issuing an internal delete, ensure source is a table\nif (typeof sourceId === \"string\" && sourceId.startsWith(\"vd_\") && isCalculationViewId(sourceId)) {\n  throw new Error(\"Deleting via a calculation view is not supported\")\n}","typeGuard":"function isTableSource(source: Table | ViewV2): source is Table {\n  return !sdk.views.isView(source)\n}","tryCatchPattern":"try {\n  await api.delete(`/api/${sourceId}/rows/${rowId}`)\n} catch (err) {\n  if (err.status === 400 && /calculation view/.test(err.message)) {\n    // surface a user-friendly message: switch data provider to the table\n  }\n  throw err\n}","preventionTips":["Bind delete actions only to screens whose data provider uses a table.","Validate the data source type before rendering destructive controls.","Document that calculation views are read-only."],"tags":["rest-api","views","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"}