Feature Request: Declarative Optimistic Locking via ON NO ROWS for UPDATE/DELETE

From: "Mark Dake" <mark(dot)drake(at)golden-hind(dot)com>
To: <pgsql-hackers(at)postgresql(dot)org>
Subject: Feature Request: Declarative Optimistic Locking via ON NO ROWS for UPDATE/DELETE
Date: 2025-06-07 20:11:40
Message-ID: 00f201dbd7e8$62255580$26700080$@golden-hind.com
Views: Raw Message | Whole Thread | Download mbox | Resend email
Thread:
Lists: pgsql-hackers

Hi all,

I'd like to propose a declarative syntax addition for UPDATE and DELETE that
supports optimistic locking directly in SQL: an ON NO ROWS clause that
triggers an error if no rows are affected by the DML statement.

Problem

In many applications, particularly those using optimistic concurrency
control, it's essential to detect when an UPDATE or DELETE statement fails
to affect any rows. Currently, this must be handled imperatively in
PL/pgSQL:

UPDATE my_table SET ... WHERE id = $1 AND version = $2;

IF NOT FOUND THEN

RAISE EXCEPTION 'Row not found or version mismatch';

END IF;

This logic cannot be encapsulated declaratively inside a single SQL
statement, making reuse in views, CTE chains, or SQL wrapper functions
awkward.

Proposal

Introduce an optional clause:

UPDATE my_table SET ... WHERE ... ON NO ROWS RAISE;

DELETE FROM my_table WHERE ... ON NO ROWS RAISE;

which raises the standard NO_DATA_FOUND (P0002) error if no rows are
affected by the DML statement.

This would complement INSERT ... ON CONFLICT by allowing UPDATE and DELETE
to fail-fast declaratively when no matching rows are found - an extremely
common pattern in modern applications.

Workaround Today

My current workaround relies on composing a RIGHT JOIN against a PG/PLSQL
function that explicitly checks for the affected row count and throws an
error:

WITH affected AS (

UPDATE my_table

SET ...

WHERE id = $1 AND version = $2

RETURNING 1 AS ok

)

SELECT *

FROM affected

RIGHT JOIN (SELECT check_row_count(count(ok)) FROM affected) checker ON
true;

-- Where check_row_count(n int) is a PG/PLSQL function like:

CREATE OR REPLACE FUNCTION check_row_count(n int) RETURNS void AS $$

BEGIN

IF n = 0 THEN

RAISE EXCEPTION 'Row not found or version mismatch';

END IF;

END;

$$ LANGUAGE plpgsql;

This workaround has two key downsides:

* It defers the error until all statements are complete, which
obscures causality and complicates error handling.
* It introduces syntactic and architectural overhead, masking the
intent compared to a declarative ON NO ROWS clause.

Benefits

* Clean declarative syntax
* Encourages safer DML operations
* Makes optimistic locking idioms easier to write, read, and maintain
* Better tooling support for error handling in SQL-only use cases
* Aligns naturally with INSERT ... ON CONFLICT semantics

About Me

Since this is my first post, a little background .I spent 20 years as a
product manager in the Oracle Database group, primarily working on XML and
JSON technologies. These days I work almost exclusively with PostgreSQL.
This idea grew out of real-world experience building a JSON-centric,
SQL-wrapped application where declarative control flow is critical. My
current stack uses a React front end and a lightweight Node/Express mid-tier
that communicates with SQL functions managed in PostgreSQL 17. JSON is the
primary interface between the frontend and the database, with business logic
encapsulated in SQL functions - often using CTEs for clarity and atomicity.
This architecture promotes separation of concerns and avoids tight coupling
between the database and backend application logic.

Happy to discuss further or refine the idea. Thanks for considering it.

Best regards,
Mark Drake

Browse pgsql-hackers by date

  From Date Subject
Next Message Mark Dake 2025-06-07 20:20:05 Inconsistent Behavior in JSONB Numeric Array Deletion
Previous Message Pavel Stehule 2025-06-07 17:11:16 Re: proposal: plpgsql, new check for extra_errors - strict_expr_check