From c0917ef6260f01b7e53ae036cd91ee7867fd8816 Mon Sep 17 00:00:00 2001 From: Ayush Tiwari Date: Fri, 11 Sep 2026 11:30:00 +0530 Subject: [PATCH v2] Reject tableoid in EXCLUDED virtual generated columns Commit 783425175 kept the EXCLUDED target list unexpanded while expanding references to virtual generated columns in ON CONFLICT expressions. If a generation expression uses tableoid, the expanded expression contains a Var that has no matching entry in that target list, causing setrefs.c to report "variable not found in subplan target lists". EXCLUDED represents the row proposed for insertion and has no system columns. A virtual generated column should not make tableoid available indirectly when a direct reference to EXCLUDED.tableoid is not allowed. After expanding the EXCLUDED virtual columns, reject tableoid references with ERRCODE_INVALID_COLUMN_REFERENCE instead of the internal error. Inspect only the expressions actually used by ON CONFLICT. Unused generation expressions and references to the target row remain unaffected. Before 783425175, this case happened to use the insertion slot's tableoid. Do not restore that behavior, since it would make a system column available indirectly through EXCLUDED when it is not available there directly. Add regression coverage for the reported failure. Reported-by: Alexander Lakhin Discussion: https://postgr.es/m/18fbeb33-9631-4f6e-9280-2c787aef254e@gmail.com Backpatch-through: 18 --- src/backend/optimizer/prep/prepjointree.c | 25 +++++++++++++++++++ .../regress/expected/generated_virtual.out | 4 +++ src/test/regress/sql/generated_virtual.sql | 3 +++ 3 files changed, 32 insertions(+) diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c index dfe320beccd..1d04c0a9113 100644 --- a/src/backend/optimizer/prep/prepjointree.c +++ b/src/backend/optimizer/prep/prepjointree.c @@ -26,6 +26,7 @@ */ #include "postgres.h" +#include "access/sysattr.h" #include "access/table.h" #include "catalog/pg_type.h" #include "funcapi.h" @@ -599,7 +600,31 @@ expand_virtual_generated_columns(PlannerInfo *root, Query *parse, parse = (Query *) pullup_replace_vars((Node *) parse, &rvcontext); if (parse->onConflict) + { + if (rt_index == parse->onConflict->exclRelIndex) + { + List *vars; + ListCell *cell; + + /* EXCLUDED has no system columns for a generation expression. */ + vars = pull_vars_of_level((Node *) parse->onConflict, 0); + foreach(cell, vars) + { + Var *var = lfirst(cell); + + if (IsA(var, Var) && + var->varno == rt_index && + var->varattno == TableOidAttributeNumber) + ereport(ERROR, + (errcode(ERRCODE_INVALID_COLUMN_REFERENCE), + errmsg("cannot use system column \"%s\" in EXCLUDED", + "tableoid"))); + } + list_free(vars); + } + parse->onConflict->exclRelTlist = save_exclRelTlist; + } } return parse; diff --git a/src/test/regress/expected/generated_virtual.out b/src/test/regress/expected/generated_virtual.out index 6ee029796f1..39fa77d287e 100644 --- a/src/test/regress/expected/generated_virtual.out +++ b/src/test/regress/expected/generated_virtual.out @@ -589,6 +589,10 @@ SELECT * FROM gtest_tableoid; 2 | t | gtest_tableoid (2 rows) +-- but not through EXCLUDED, which has no system columns +INSERT INTO gtest_tableoid VALUES (1) + ON CONFLICT (a) DO UPDATE SET a = excluded.a WHERE excluded.b; +ERROR: cannot use system column "tableoid" in EXCLUDED -- 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 e4ea63bb3a1..b6482a433ca 100644 --- a/src/test/regress/sql/generated_virtual.sql +++ b/src/test/regress/sql/generated_virtual.sql @@ -279,6 +279,9 @@ INSERT INTO gtest_tableoid VALUES (1), (2); ALTER TABLE gtest_tableoid ADD COLUMN c regclass GENERATED ALWAYS AS (tableoid) VIRTUAL; SELECT * FROM gtest_tableoid; +-- but not through EXCLUDED, which has no system columns +INSERT INTO gtest_tableoid VALUES (1) + ON CONFLICT (a) DO UPDATE SET a = excluded.a WHERE excluded.b; -- drop column behavior CREATE TABLE gtest10 (a int PRIMARY KEY, b int, c int GENERATED ALWAYS AS (b * 2) VIRTUAL);