Subject: [BUG]: WHERE CURRENT OF cursor fail on tables that have virtual generated columns

From: SATYANARAYANA NARLAPURAM <[email-masked]>
Date: Fri, 17 Apr 2026 13:03:36 -0700

Hi hackers,

UPDATE and DELETE with WHERE CURRENT OF cursor fail on tables that have
virtual generated columns, erroring with "WHERE CURRENT OF on a view is not
implemented" even though the target is a regular table, not a view.

Repro:

create table gtest_cursor (id int primary key, a int, b int generated
always as (a * 2) virtual);
insert into gtest_cursor values (1, 10), (2, 20), (3, 30);

begin;
declare cur1 cursor for select * from gtest_cursor order by id for update;
fetch 1 from cur1;
update gtest_cursor set a = 99 where current of cur1;
select * from gtest_cursor order by id;
commit;

Analysis:
The bug stems from replace_rte_variables_mutator() in rewriteManip.c, which
unconditionally errors on any CurrentOfExpr referencing the target
relation. This appears to a check designed for view rewriting, where WHERE
CURRENT OF cannot be translated through a view. However, virtual generated
column (VGC) expansion also routes through this mutator. The rewriter's
expand_generated_columns_internal() calls ReplaceVarsFromTargetList(), and
the planner's expand_virtual_generated_columns() calls
pullup_replace_vars(), which calls replace_rte_variables(). Since virtual
generated columns use same mutator, while expanding virtual generated
columns returns the same error even though the table is not a view and the
cursor position is perfectly valid.

The fix adds bool error_on_current_of to replace_rte_variables_context. The
existing replace_rte_variables() is refactored into a static
replace_rte_variables_internal() that accepts the flag, with two public
wrappers: replace_rte_variables() (passes true, preserving existing
behavior) and replace_rte_variables_ext() (exposes the flag). The same
pattern is applied to ReplaceVarsFromTargetList() /
ReplaceVarsFromTargetListExtended(). In replace_rte_variables_mutator(),
the CurrentOfExpr error is now conditional on context->error_on_current_of.
The two VGC expansion call sites pass false; all other callers pass true.
The down side of this approach is that it is adding additional public API.

Alternative considered: RTE-lookup approach. Instead of a flag, the mutator
could look up the target RTE in the query's range table and check
rte->rtekind, if it is RTE_RELATION, skip the error. Since the mutator
doesn't have access to the range table and threading an RTE or range table
pointer through the context would be equally invasive I didn't pursue this
further. Went with the flag approach because it is simpler, explicit, and
keeps the mutator's contract clean.

Thoughts or any other ideas how to fix this?

Thanks,
Satya

---

From: Dean Rasheed <[email-masked]>
Date: Sun, 19 Apr 2026 11:42:28 +0100

On Fri, 17 Apr 2026 at 21:04, SATYANARAYANA NARLAPURAM
<[email-masked]> wrote:
>
> Hi hackers,
>
> UPDATE and DELETE with WHERE CURRENT OF cursor fail on tables that have virtual generated columns, erroring with "WHERE CURRENT OF on a view is not implemented" even though the target is a regular table, not a view.
>

Nice catch!

> Analysis:
> The bug stems from replace_rte_variables_mutator() in rewriteManip.c, which unconditionally errors on any CurrentOfExpr referencing the target relation. This appears to a check designed for view rewriting, where WHERE CURRENT OF cannot be translated through a view. However, virtual generated column (VGC) expansion also routes through this mutator. The rewriter's expand_generated_columns_internal() calls ReplaceVarsFromTargetList(), and the planner's expand_virtual_generated_columns() calls pullup_replace_vars(), which calls replace_rte_variables(). Since virtual generated columns use same mutator, while expanding virtual generated columns returns the same error even though the table is not a view and the cursor position is perfectly valid.
>
> The fix adds bool error_on_current_of to replace_rte_variables_context. The existing replace_rte_variables() is refactored into a static replace_rte_variables_internal() that accepts the flag, with two public wrappers: replace_rte_variables() (passes true, preserving existing behavior) and replace_rte_variables_ext() (exposes the flag). The same pattern is applied to ReplaceVarsFromTargetList() / ReplaceVarsFromTargetListExtended(). In replace_rte_variables_mutator(), the CurrentOfExpr error is now conditional on context->error_on_current_of. The two VGC expansion call sites pass false; all other callers pass true. The down side of this approach is that it is adding additional public API.
>

Hmm, it seems to me that a much simpler fix is to check for use of
WHERE CURRENT OF on a view at parse time, and throw the error there.
Then the problematic rewriter check can simply be removed, as in the
attached v2 patch.

Regards,
Dean
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
new file mode 100644
index 84deed9..d984535
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -595,6 +595,14 @@ transformDeleteStmt(ParseState *pstate,
 										 ACL_DELETE);
 	nsitem = pstate->p_target_nsitem;
 
+	/* disallow DELETE ... WHERE CURRENT OF on a view */
+	if (stmt->whereClause &&
+		IsA(stmt->whereClause, CurrentOfExpr) &&
+		pstate->p_target_relation->rd_rel->relkind == RELKIND_VIEW)
+		ereport(ERROR,
+				errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				errmsg("WHERE CURRENT OF on a view is not implemented"));
+
 	/* there's no DISTINCT in DELETE */
 	qry->distinctClause = NIL;
 
@@ -2868,6 +2876,14 @@ transformUpdateStmt(ParseState *pstate,
 										 true,
 										 ACL_UPDATE);
 
+	/* disallow UPDATE ... WHERE CURRENT OF on a view */
+	if (stmt->whereClause &&
+		IsA(stmt->whereClause, CurrentOfExpr) &&
+		pstate->p_target_relation->rd_rel->relkind == RELKIND_VIEW)
+		ereport(ERROR,
+				errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				errmsg("WHERE CURRENT OF on a view is not implemented"));
+
 	if (stmt->forPortionOf)
 		qry->forPortionOf = transformForPortionOfClause(pstate,
 														qry->resultRelation,
diff --git a/src/backend/rewrite/rewriteManip.c b/src/backend/rewrite/rewriteManip.c
new file mode 100644
index 4bf4aa0..9aa7ef6
--- a/src/backend/rewrite/rewriteManip.c
+++ b/src/backend/rewrite/rewriteManip.c
@@ -1514,25 +1514,6 @@ replace_rte_variables_mutator(Node *node
 		}
 		/* otherwise fall through to copy the var normally */
 	}
-	else if (IsA(node, CurrentOfExpr))
-	{
-		CurrentOfExpr *cexpr = (CurrentOfExpr *) node;
-
-		if (cexpr->cvarno == context->target_varno &&
-			context->sublevels_up == 0)
-		{
-			/*
-			 * We get here if a WHERE CURRENT OF expression turns out to apply
-			 * to a view.  Someday we might be able to translate the
-			 * expression to apply to an underlying table of the view, but
-			 * right now it's not implemented.
-			 */
-			ereport(ERROR,
-					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-					 errmsg("WHERE CURRENT OF on a view is not implemented")));
-		}
-		/* otherwise fall through to copy the expr normally */
-	}
 	else if (IsA(node, Query))
 	{
 		/* Recurse into RTE subquery or not-yet-planned sublink subquery */
diff --git a/src/test/regress/expected/generated_virtual.out b/src/test/regress/expected/generated_virtual.out
new file mode 100644
index fc41c48..c238ef8
--- a/src/test/regress/expected/generated_virtual.out
+++ b/src/test/regress/expected/generated_virtual.out
@@ -1723,3 +1723,43 @@ select * from gtest33 where b is null;
 
 reset constraint_exclusion;
 drop table gtest33;
+create table gtest_cursor (id int primary key, a int, b int generated always as (a * 2) virtual);
+insert into gtest_cursor values (1, 10), (2, 20), (3, 30);
+-- UPDATE via cursor
+begin;
+declare cur1 cursor for select * from gtest_cursor order by id for update;
+fetch 1 from cur1;
+ id | a  | b  
+----+----+----
+  1 | 10 | 20
+(1 row)
+
+update gtest_cursor set a = 99 where current of cur1;
+select * from gtest_cursor order by id;
+ id | a  |  b  
+----+----+-----
+  1 | 99 | 198
+  2 | 20 |  40
+  3 | 30 |  60
+(3 rows)
+
+commit;
+-- DELETE via cursor
+begin;
+declare cur2 cursor for select * from gtest_cursor order by id for update;
+fetch 1 from cur2;
+ id | a  |  b  
+----+----+-----
+  1 | 99 | 198
+(1 row)
+
+delete from gtest_cursor where current of cur2;
+select * from gtest_cursor order by id;
+ id | a  | b  
+----+----+----
+  2 | 20 | 40
+  3 | 30 | 60
+(2 rows)
+
+commit;
+drop table gtest_cursor;
diff --git a/src/test/regress/sql/generated_virtual.sql b/src/test/regress/sql/generated_virtual.sql
new file mode 100644
index 9b32413..5bab7d3
--- a/src/test/regress/sql/generated_virtual.sql
+++ b/src/test/regress/sql/generated_virtual.sql
@@ -906,3 +906,24 @@ select * from gtest33 where b is null;
 
 reset constraint_exclusion;
 drop table gtest33;
+
+create table gtest_cursor (id int primary key, a int, b int generated always as (a * 2) virtual);
+insert into gtest_cursor values (1, 10), (2, 20), (3, 30);
+
+-- UPDATE via cursor
+begin;
+declare cur1 cursor for select * from gtest_cursor order by id for update;
+fetch 1 from cur1;
+update gtest_cursor set a = 99 where current of cur1;
+select * from gtest_cursor order by id;
+commit;
+
+-- DELETE via cursor
+begin;
+declare cur2 cursor for select * from gtest_cursor order by id for update;
+fetch 1 from cur2;
+delete from gtest_cursor where current of cur2;
+select * from gtest_cursor order by id;
+commit;
+
+drop table gtest_cursor;

---

From: SATYANARAYANA NARLAPURAM <[email-masked]>
Date: Sun, 19 Apr 2026 17:32:55 -0700

HI,

On Sun, Apr 19, 2026 at 3:42 AM Dean Rasheed <[email-masked]>
wrote:

> > The bug stems from replace_rte_variables_mutator() in rewriteManip.c,
> which unconditionally errors on any CurrentOfExpr referencing the target
> > The fix adds bool error_on_current_of to replace_rte_variables_context.
> The existing replace_rte_variables() is refactored into a static

This patch looks simple and neat, is there any reason why it was done
differently earlier?

> Then the problematic rewriter check can simply be removed, as in the

I reviewed the patch, and it addresses the original bug and other existing
tests.
I verified it rejects the view with WHERE CURRENT OF on update and delete.

Updated the patch to include a test case to reject view update with WHERE
CURRENT OF.

Thanks,
Satya

---

From: Dean Rasheed <[email-masked]>
Date: Mon, 20 Apr 2026 08:56:23 +0100

On Mon, 20 Apr 2026 at 01:33, SATYANARAYANA NARLAPURAM
<[email-masked]> wrote:
>
> On Sun, Apr 19, 2026 at 3:42 AM Dean Rasheed <[email-masked]> wrote:
>>
> This patch looks simple and neat, is there any reason why it was done differently earlier?
>

I'm not sure, but possibly because it used to be possible to turn a
table into a view by defining a SELECT rule on it, which could have
rendered a parse-time check insufficient. That's no longer the case
though, and we now have similar parse-time relkind tests elsewhere
(e.g., for MERGE).

> Updated the patch to include a test case to reject view update with WHERE CURRENT OF.
>

That's already tested in portals.sql, which seems like a better place
for that test, since it's not related to virtual generated columns. I
don't think another test is necessary -- admittedly portals.sql only
tests DELETE, but the UPDATE code is the same, so I think the existing
test is sufficient. We don't obsessively try to achieve 100% coverage
in our tests.

Regards,
Dean

---

From: SATYANARAYANA NARLAPURAM <[email-masked]>
Date: Mon, 20 Apr 2026 01:03:18 -0700

Hi

> test is sufficient. We don't obsessively try to achieve 100% coverage

Sounds good. Thanks for letting me know.

---

From: Dean Rasheed <[email-masked]>
Date: Wed, 22 Apr 2026 12:07:20 +0100

>> test is sufficient. We don't obsessively try to achieve 100% coverage

I changed my mind on that and decided to add another test to
portals.sql, just in case, since it is a separate code branch.

I also spent some time looking to see whether there were any other
ways that replace_rte_variables() could have been called (e.g.,
subquery pullup) that would fall foul of this, but I couldn't find
any, so I think this bug only affects virtual generated columns.

Pushed and back-patched to v18. Thanks for the report!

Regards,
Dean