{"record":{"id":"211a0875d9b6409f","repo":"remix-run/remix","slug":"upsert-requires-at-least-one-value-211a08","errorCode":null,"errorMessage":"upsert requires at least one value","messagePattern":"upsert requires at least one value","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/data-table-postgres/src/lib/sql-compiler.ts","lineNumber":212,"sourceCode":"  return {\n    text:\n      'insert into ' +\n      quotePath(getTableName(table)) +\n      ' (' +\n      quotedColumns.join(', ') +\n      ') values ' +\n      valueSets.join(', ') +\n      compileReturningClause(returning),\n    values: context.values,\n  }\n}\n\nfunction compileUpsertOperation(operation: UpsertOperation, context: CompileContext): SqlStatement {\n  let insertColumns = Object.keys(operation.values)\n  let conflictTarget = operation.conflictTarget ?? [...getTablePrimaryKey(operation.table)]\n\n  if (insertColumns.length === 0) {\n    throw new Error('upsert requires at least one value')\n  }\n\n  let quotedInsertColumns = insertColumns.map((column) => quotePath(column))\n  let insertPlaceholders = insertColumns.map((column) =>\n    pushValue(context, operation.values[column]),\n  )\n\n  let updateValues = operation.update ?? operation.values\n  let updateColumns = Object.keys(updateValues)\n  let onConflictClause = ''\n\n  if (updateColumns.length === 0) {\n    onConflictClause =\n      ' on conflict (' +\n      conflictTarget.map((column: string) => quotePath(column)).join(', ') +\n      ') do nothing'\n  } else {\n    onConflictClause =","sourceCodeStart":194,"sourceCodeEnd":230,"githubUrl":"https://github.com/remix-run/remix/blob/9696913134be3a4423513d2775f7b31d6917c049/packages/data-table-postgres/src/lib/sql-compiler.ts#L194-L230","documentation":"compileUpsertOperation requires at least one column to insert; if the values record is empty there is nothing to upsert and the SQL would be invalid, so the compiler throws immediately.","triggerScenarios":"Calling upsert with `{}` (empty object) as values — commonly from spreading an optional update payload that is undefined, or building values conditionally so no keys are ever set; mapping over an empty batch item.","commonSituations":"`...changes` where changes is empty; form submissions with no changed fields that still hit the upsert path; batch loops that generate empty rows.","solutions":["Skip the upsert call when the values object has no keys","Default to required fields or validate that the payload produced at least one column before calling upsert","Fix upstream logic so empty payloads don't reach the database layer"],"exampleFix":"// before\nawait table.upsert({ ...maybeChanges }) // throws when empty\n\n// after\nif (Object.keys(changes).length > 0) {\n  await table.upsert(changes)\n}","handlingStrategy":"validation","validationCode":"if (Object.keys(values).length === 0) { /* skip or throw a clearer error */ return }","typeGuard":"function hasValues(v: Record<string, unknown>): boolean { return Object.keys(v).length > 0 }","tryCatchPattern":null,"preventionTips":["Guard upsert calls with an empty-values check","Default required fields in the payload builder","Reject empty form submissions before the data layer"],"tags":["postgres","upsert","validation","empty-values"],"backgroundTag":"empty-payload-validation","analyzedSha":"9696913134be3a4423513d2775f7b31d6917c049","analyzedAt":"2026-08-27T19:55:01.024Z","schemaVersion":2},"datasetVersion":"2026-08-28T00:17:15.603Z"}