{"record":{"id":"6d3b7e0fde748409","repo":"Budibase/budibase","slug":"cannot-update-view-type-after-creation","errorCode":null,"errorMessage":"Cannot update view type after creation","messagePattern":"Cannot update view type after creation","errorType":"http","errorClass":"HTTPError","httpStatus":400,"severity":"error","filePath":"packages/server/src/sdk/workspace/views/external.ts","lineNumber":91,"sourceCode":"export async function update(\n  tableId: string,\n  view: Readonly<ViewV2>\n): Promise<{ view: Readonly<ViewV2>; existingView: ViewV2 }> {\n  const db = context.getWorkspaceDB()\n\n  const { datasourceId, tableName } = breakExternalTableId(tableId)\n  const ds = await sdk.datasources.get(datasourceId)\n  ds.entities![tableName].views ??= {}\n  const views = ds.entities![tableName].views!\n\n  const existingView = Object.values(views).find(\n    v => isV2(v) && v.id === view.id\n  )\n  if (!existingView || !existingView.name) {\n    throw new HTTPError(`View ${view.id} not found in table ${tableId}`, 404)\n  }\n  if (isV2(existingView) && existingView.type !== view.type) {\n    throw new HTTPError(`Cannot update view type after creation`, 400)\n  }\n\n  view = ensureQuerySet(view)\n  view = ensureQueryUISet(view)\n\n  delete views[existingView.name]\n  views[view.name] = view\n  await db.put(ds)\n  return { view, existingView } as { view: ViewV2; existingView: ViewV2 }\n}\n\nexport async function remove(viewId: string): Promise<ViewV2> {\n  const db = context.getWorkspaceDB()\n\n  const view = await get(viewId)\n\n  if (!view) {\n    throw new HTTPError(`View ${viewId} not found`, 404)","sourceCodeStart":73,"sourceCodeEnd":109,"githubUrl":"https://github.com/Budibase/budibase/blob/a81a902e9a8fe55b467d106765f6638f12e35c49/packages/server/src/sdk/workspace/views/external.ts#L73-L109","documentation":"Thrown when an update payload tries to change the type of an existing V2 view (e.g. table -> calculation). View type is immutable after creation because the query/schema shape differs fundamentally between types, so the SDK rejects type-flipping updates with a 400.","triggerScenarios":"Calling update with a ViewV2 whose id matches an existing view but whose type property differs from existingView.type, e.g. sending a calculation view payload over an existing table view id.","commonSituations":"Frontend form reused across view types sends the wrong type; a migration script rewrites view.type; copy/pasted payloads between view kinds.","solutions":["Send the update with the same type as the existing view","Delete the view and create a new one of the desired type","Fix the client so the type selector is disabled when editing an existing view"],"exampleFix":"// before\nawait sdk.views.update(tableId, { ...view, type: ViewV2Type.CALCULATION })\n// after\nconst existing = await sdk.views.get(view.id)\nawait sdk.views.update(tableId, { ...view, type: existing.type })\n// or: await sdk.views.remove(view.id); await sdk.views.create(tableId, calcView)","handlingStrategy":"validation","validationCode":"const existing = await sdk.views.get(view.id)\nif (existing.type !== view.type) throw new Error(\"View type change requires delete+create\")","typeGuard":"function sameType(existing: ViewV2, next: ViewV2): boolean { return existing.type === next.type }","tryCatchPattern":"try { await sdk.views.update(tableId, view) }\ncatch (e) {\n  if (e instanceof HTTPError && e.status === 400 && /view type/i.test(e.message)) {\n    await sdk.views.remove(view.id)\n    await sdk.views.create(tableId, view)\n  } else throw e\n}","preventionTips":["Treat view type as immutable; disable the type selector when editing","Recreate views instead of mutating type","Never merge payloads across view kinds","Read the existing view first and preserve its type"],"tags":["http-400","views","immutable-field"],"backgroundTag":"immutable-field-update","analyzedSha":"a81a902e9a8fe55b467d106765f6638f12e35c49","analyzedAt":"2026-08-29T01:03:10.972Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}