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
- Skip the update when the changes object is empty: if (Object.keys(changes).length === 0) return
- Provide at least one real column to change in the changes object
- 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
- Guard PATCH-style handlers with an empty-changes check before calling update()
- Strip-undefined helpers should count surviving keys, not assume fields exist
- Consider an explicit updatedAt touch if an update must always occur
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
- upsert requires at least one value
- update() failed to find row for table "' + getTableName(tabl
- insertMany() requires at least one explicit value across the
- Invalid afterRead callback result for table "' + tableName +
- Invalid beforeDelete callback result for table "' + context.
AI-assisted analysis of remix-run/remix@9696913134 (2026-08-27).
Data as JSON: /api/errors/408ff9e2881a26ac.
Report an issue: GitHub.