remix-run/remix · error · DataTableQueryError

update() requires at least one change

Error message

update() requires at least one change

What it means

update() requires the set of changes to contain at least one column after preparation. If the changes object is empty (or all its keys resolved to nothing after defaults/timestamps are excluded), an UPDATE with no SET clause would be invalid SQL, so a DataTableQueryError is thrown before hitting the database.

Source

Thrown at packages/data-table/src/lib/database/query-execution.ts:387

async function executeUpdate(
  database: QueryExecutionContext,
  table: AnyTable,
  state: QueryState,
  changes: Record<string, unknown>,
  options?: { returning?: ReturningInput<Record<string, unknown>>; touch?: boolean },
): Promise<WriteResult | WriteRowsResult<Record<string, unknown>>> {
  let returning = options?.returning
  assertReturningCapability(database.capabilities, 'update', returning)
  let preparedChanges = prepareUpdateValues(
    table,
    changes as never,
    database.now(),
    options?.touch ?? true,
  )

  if (Object.keys(preparedChanges).length === 0) {
    throw new DataTableQueryError('update() requires at least one change')
  }

  let result: DataManipulationResult

  if (hasScopedWriteModifiers(state)) {
    result = await database[runInTransaction](async (tx) => {
      let primaryKeys = await loadPrimaryKeyRowsForScope(tx, table, state)
      let primaryKeyPredicate = buildPrimaryKeyPredicate(table, primaryKeys)

      if (!primaryKeyPredicate) {
        return {
          affectedRows: 0,
          insertId: undefined,
          rows: returning ? [] : undefined,
        }
      }

      return tx[executeOperation]({

View on GitHub (pinned to 9696913134)

Solutions

  1. Skip the update when the changes object is empty: if (Object.keys(changes).length === 0) return
  2. Provide at least one real column to change in the changes object
  3. If you intended a touch-only update, include the timestamp column explicitly

Example fix

// before
await userTable.update({ where: { id }, changes: submittedChanges })

// after
if (Object.keys(submittedChanges).length > 0) {
  await userTable.update({ where: { id }, changes: submittedChanges })
}
Defensive patterns

Strategy: validation

Validate before calling

if (Object.keys(changes).length === 0) {
  return // nothing to update
}
await table.update({ where, changes })

Type guard

function hasUpdateChanges(changes: Record<string, unknown>): boolean {
  return Object.keys(changes).length > 0
}

Try / catch

try {
  await table.update({ where, changes })
} catch (error) {
  if (error instanceof DataTableQueryError && error.message === 'update() requires at least one change') {
    return // treat as no-op
  } else throw error
}

Prevention

When it happens

Trigger: await table.update({ where: ..., changes: {} }); passing a changes object whose only keys are filtered out during preparation (e.g. undefined-only or read-only columns).

Common situations: Building changes from a form or JSON PATCH body where no fields were submitted; spreading an object that turned out to be empty; forgetting to default a 'updatedAt' touch column when the user changed nothing.

Related errors


AI-assisted analysis of remix-run/remix@9696913134 (2026-08-27). Data as JSON: /api/errors/408ff9e2881a26ac. Report an issue: GitHub.