Budibase/budibase · error · HTTPError

Cannot delete rows through a calculation view

Error message

Cannot delete rows through a calculation view

What it means

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.

Source

Thrown at packages/server/src/api/controllers/row/internal.ts:105

    ctx.request.body = row as any
    await userController.updateMetadata(ctx as any)
    return { row: ctx.body as Row, table, oldRow }
  }

  const result = await finaliseRow(source, row, {
    updateFormula: true,
    updateAIColumns: true,
  })

  return { ...result, oldRow }
}

export async function destroy(ctx: UserCtx) {
  const db = context.getWorkspaceDB()
  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)
  }

  let table: Table
  if (sdk.views.isView(source)) {
    table = await sdk.views.getTable(source.id)
  } else {
    table = source
  }

  const { _id } = ctx.request.body
  let row = await db.get<Row>(_id)
  let _rev = ctx.request.body._rev || row._rev

  if (row.tableId !== table._id) {
    throw "Supplied tableId doesn't match the row's tableId"
  }
  // update the row to include full relationships before deleting them
  row = await outputProcessing(table, row, {

View on GitHub (pinned to a81a902e9a)

Solutions

  1. Point the delete action at the base table ID, not the calculation view ID.
  2. Remove or reconfigure delete buttons/actions on screens backed by calculation views since they are not deletable.
  3. If deletion is required, build the screen against the underlying table or a non-calculation view.

Example fix

// before
await api.delete(`/api/${viewId}/rows/${rowId}`)
// after
await api.delete(`/api/${tableId}/rows/${rowId}`)
Defensive patterns

Strategy: validation

Validate before calling

// before issuing an internal delete, ensure source is a table
if (typeof sourceId === "string" && sourceId.startsWith("vd_") && isCalculationViewId(sourceId)) {
  throw new Error("Deleting via a calculation view is not supported")
}

Type guard

function isTableSource(source: Table | ViewV2): source is Table {
  return !sdk.views.isView(source)
}

Try / catch

try {
  await api.delete(`/api/${sourceId}/rows/${rowId}`)
} catch (err) {
  if (err.status === 400 && /calculation view/.test(err.message)) {
    // surface a user-friendly message: switch data provider to the table
  }
  throw err
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of Budibase/budibase@a81a902e9a (2026-08-29). Data as JSON: /api/errors/38b4bb6ad37f0c4a. Report an issue: GitHub.