From: Ayush Tiwari <ayushtiwari.slg01@gmail.com>
Date: Fri, 11 Sep 2026 11:30:00 +0530
Subject: [PATCH v1] Fix EXCLUDED virtual generated columns using tableoid

Commit 783425175 arranged for the EXCLUDED target list to remain
unexpanded while expanding references to virtual generated columns in
ON CONFLICT clauses.  If a generation expression uses tableoid, this
leaves a tableoid Var in the expanded clause.  Since the EXCLUDED target
list has no matching entry, set_plan_refs() fails with "variable not
found in subplan target lists".

Add a tableoid Var to the EXCLUDED target list.  This is the only system
column allowed in generation expressions, and the candidate insertion
slot supplies its value.  Test both the reported non-conflicting insert
and an insert that takes the conflict-update path.

Reported-by: Alexander Lakhin <exclusion@gmail.com>
Discussion: https://postgr.es/m/18fbeb33-9631-4f6e-9280-2c787aef254e@gmail.com
Backpatch-through: 18
---
 src/backend/parser/analyze.c                    | 11 +++++++++++
 src/test/regress/expected/generated_virtual.out | 10 ++++++++++
 src/test/regress/sql/generated_virtual.sql      |  5 +++++
 3 files changed, 26 insertions(+)

diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index 14202e5cae6bae976b32938728dc5dee79ae3256..7af7dc0bb3d6401ceb79c0239640d7e7059b777c 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -1674,6 +1674,17 @@ BuildOnConflictExcludedTargetlist(Relation targetrel,
 		result = lappend(result, te);
 	}
 
+	/*
+	 * Add an entry for tableoid, the only system column that can appear in a
+	 * generation expression, to support references to virtual generated
+	 * columns that use it.
+	 */
+	var = makeVar(exclRelIndex, TableOidAttributeNumber,
+				  OIDOID, -1, InvalidOid, 0);
+	te = makeTargetEntry((Expr *) var, TableOidAttributeNumber,
+						 pstrdup("tableoid"), true);
+	result = lappend(result, te);
+
 	/*
 	 * Add a whole-row-Var entry to support references to "EXCLUDED.*".  Like
 	 * the other entries in the EXCLUDED tlist, its resno must match the Var's
diff --git a/src/test/regress/expected/generated_virtual.out b/src/test/regress/expected/generated_virtual.out
index 6ee029796f1784f7330a7e8636091d94c24383ee..71868c12550effe455cb048330204c03c2ebbbe5 100644
--- a/src/test/regress/expected/generated_virtual.out
+++ b/src/test/regress/expected/generated_virtual.out
@@ -589,6 +589,16 @@ SELECT * FROM gtest_tableoid;
  2 | t | gtest_tableoid
 (2 rows)
 
+INSERT INTO gtest_tableoid VALUES (3)
+  ON CONFLICT (a) DO UPDATE SET a = excluded.a WHERE excluded.b;
+INSERT INTO gtest_tableoid VALUES (3)
+  ON CONFLICT (a) DO UPDATE SET a = excluded.a WHERE excluded.b
+  RETURNING a, b, c;
+ a | b |       c        
+---+---+----------------
+ 3 | t | gtest_tableoid
+(1 row)
+
 -- drop column behavior
 CREATE TABLE gtest10 (a int PRIMARY KEY, b int, c int GENERATED ALWAYS AS (b * 2) VIRTUAL);
 ALTER TABLE gtest10 DROP COLUMN b;  -- fails
diff --git a/src/test/regress/sql/generated_virtual.sql b/src/test/regress/sql/generated_virtual.sql
index e4ea63bb3a1f561a235b9f1ccf5acda766747eb9..daa0d4faf78c11fc9ed1d9d24ef7d57d6d929e2d 100644
--- a/src/test/regress/sql/generated_virtual.sql
+++ b/src/test/regress/sql/generated_virtual.sql
@@ -279,6 +279,11 @@ INSERT INTO gtest_tableoid VALUES (1), (2);
 ALTER TABLE gtest_tableoid ADD COLUMN
   c regclass GENERATED ALWAYS AS (tableoid) VIRTUAL;
 SELECT * FROM gtest_tableoid;
+INSERT INTO gtest_tableoid VALUES (3)
+  ON CONFLICT (a) DO UPDATE SET a = excluded.a WHERE excluded.b;
+INSERT INTO gtest_tableoid VALUES (3)
+  ON CONFLICT (a) DO UPDATE SET a = excluded.a WHERE excluded.b
+  RETURNING a, b, c;
 
 -- drop column behavior
 CREATE TABLE gtest10 (a int PRIMARY KEY, b int, c int GENERATED ALWAYS AS (b * 2) VIRTUAL);
-- 
2.43.0
