{"record":{"id":"fe44b93e54854364","repo":"remix-run/remix","slug":"upsert-requires-at-least-one-value","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-mysql/src/lib/sql-compiler.ts","lineNumber":192,"sourceCode":"  )\n\n  return {\n    text:\n      'insert into ' +\n      quotePath(getTableName(table)) +\n      ' (' +\n      columns.map((column) => quotePath(column)).join(', ') +\n      ') values ' +\n      values.join(', '),\n    values: context.values,\n  }\n}\n\nfunction compileUpsertOperation(operation: UpsertOperation, context: CompileContext): SqlStatement {\n  let insertColumns = Object.keys(operation.values)\n\n  if (insertColumns.length === 0) {\n    throw new Error('upsert requires at least one value')\n  }\n\n  let updateValues = operation.update ?? operation.values\n  let updateColumns = Object.keys(updateValues)\n  let fallbackNoopColumn = getTablePrimaryKey(operation.table)[0]\n\n  let onDuplicate =\n    updateColumns.length > 0\n      ? updateColumns\n          .map((column) => quotePath(column) + ' = ' + pushValue(context, updateValues[column]))\n          .join(', ')\n      : quotePath(fallbackNoopColumn) + ' = ' + quotePath(fallbackNoopColumn)\n\n  return {\n    text:\n      'insert into ' +\n      quotePath(getTableName(operation.table)) +\n      ' (' +","sourceCodeStart":174,"sourceCodeEnd":210,"githubUrl":"https://github.com/remix-run/remix/blob/9696913134be3a4423513d2775f7b31d6917c049/packages/data-table-mysql/src/lib/sql-compiler.ts#L174-L210","documentation":"compileUpsertOperation throws when the values object of an upsert operation has no keys, because MySQL INSERT ... ON DUPLICATE KEY UPDATE requires at least one column to insert. An empty payload makes the generated SQL invalid, so the compiler rejects it up front.","triggerScenarios":"Calling the upsert builder/compileMysqlOperation with values = {} (e.g. Object.keys(payload).length === 0 after filtering); spreading an empty request body or an emptied object into upsert values.","commonSituations":"Bulk-sync code upserting rows from user input where all fields were stripped by validation/sanitization; mapping over an empty dataset but still emitting upsert operations; optional-payload paths that default to {}.","solutions":["Skip the upsert when the values object is empty (guard with Object.keys(values).length > 0)","Default missing payloads to a meaningful sentinel column or treat empty as a no-op earlier in the flow","Validate API payloads before constructing database operations"],"exampleFix":"// before\nawait upsert(driver, 'users', payload) // payload === {}\n// after\nif (Object.keys(payload).length > 0) {\n  await upsert(driver, 'users', payload)\n}","handlingStrategy":"validation","validationCode":"if (Object.keys(values).length === 0) {\n  // nothing to upsert — skip\n} else {\n  await upsert(driver, 'users', values)\n}","typeGuard":"function hasUpsertValues(values: Record<string, unknown>): boolean {\n  return Object.keys(values).length > 0\n}","tryCatchPattern":"try { const stmt = compileMysqlOperation(op, ctx) } catch (e) { if (e instanceof Error && e.message === 'upsert requires at least one value') { return /* no-op */ } throw e }","preventionTips":["Validate payloads have at least one field before building upserts","Guard bulk-sync loops that map possibly-empty objects to operations"],"tags":["data-table","mysql","upsert","validation","sql-compiler"],"backgroundTag":"empty-values-rejected","analyzedSha":"9696913134be3a4423513d2775f7b31d6917c049","analyzedAt":"2026-08-27T19:55:01.024Z","schemaVersion":2},"datasetVersion":"2026-08-28T00:17:15.603Z"}