Hi,

Attached rebased v3 due to f80bedd52b1. No additional changes compared to v2.

--
Matheus Alcantara
EDB: https://www.enterprisedb.com
From 4572053a997576f08df97565674aed9515b01d50 Mon Sep 17 00:00:00 2001
From: Matheus Alcantara <[email protected]>
Date: Mon, 2 Feb 2026 19:06:44 -0300
Subject: [PATCH v3] Show expression of virtual columns in error messages

Previously, when a constraint violation occurred on a table with virtual
generated columns, the "Failing row contains" error message would display
the literal string "virtual" as a placeholder for those columns. This was
not helpful for debugging.

Now, the generation expression is shown instead, making it easier to
understand what value would be computed for the virtual column.

For example, instead of:
  Failing row contains (5, 10, virtual).

The error message now shows:
  Failing row contains (5, 10, a * 2).

This required changing ExecBuildSlotValueDescription() to accept a
Relation instead of just an Oid, so that build_generation_expression()
can be called to retrieve the column's generation expression.
---
 src/backend/executor/execMain.c               | 97 ++++++++++++++++---
 src/backend/replication/logical/conflict.c    |  7 +-
 src/include/executor/executor.h               |  2 +-
 .../regress/expected/generated_virtual.out    | 18 ++--
 src/test/regress/expected/partition_merge.out |  2 +-
 src/tools/pgindent/typedefs.list              |  1 +
 6 files changed, 101 insertions(+), 26 deletions(-)

diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index bfd3ebc601e..b82c500ba90 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -51,6 +51,8 @@
 #include "foreign/fdwapi.h"
 #include "mb/pg_wchar.h"
 #include "miscadmin.h"
+#include "nodes/makefuncs.h"
+#include "nodes/nodeFuncs.h"
 #include "nodes/queryjumble.h"
 #include "parser/parse_relation.h"
 #include "pgstat.h"
@@ -61,8 +63,18 @@
 #include "utils/lsyscache.h"
 #include "utils/partcache.h"
 #include "utils/rls.h"
+#include "utils/ruleutils.h"
 #include "utils/snapmgr.h"
 
+/*
+ * Context for substitute_actual_values_mutator
+ */
+typedef struct
+{
+       TupleTableSlot *slot;
+       TupleDesc       tupdesc;
+} substitute_actual_values_context;
+
 
 /* Hooks for plugins to get control in ExecutorStart/Run/Finish/End */
 ExecutorStart_hook_type ExecutorStart_hook = NULL;
@@ -93,6 +105,9 @@ static void ReportNotNullViolationError(ResultRelInfo 
*resultRelInfo,
                                                                                
TupleTableSlot *slot,
                                                                                
EState *estate, int attnum);
 
+static Node *substitute_actual_values_mutator(Node *node,
+                                                                               
          substitute_actual_values_context *context);
+
 /* end of local decls */
 
 
@@ -1914,7 +1929,7 @@ ExecPartitionCheckEmitError(ResultRelInfo *resultRelInfo,
                                                        TupleTableSlot *slot,
                                                        EState *estate)
 {
-       Oid                     root_relid;
+       Relation        root_rel;
        TupleDesc       tupdesc;
        char       *val_desc;
        Bitmapset  *modifiedCols;
@@ -1931,8 +1946,8 @@ ExecPartitionCheckEmitError(ResultRelInfo *resultRelInfo,
                TupleDesc       old_tupdesc;
                AttrMap    *map;
 
-               root_relid = RelationGetRelid(rootrel->ri_RelationDesc);
-               tupdesc = RelationGetDescr(rootrel->ri_RelationDesc);
+               root_rel = rootrel->ri_RelationDesc;
+               tupdesc = RelationGetDescr(root_rel);
 
                old_tupdesc = RelationGetDescr(resultRelInfo->ri_RelationDesc);
                /* a reverse map */
@@ -1950,13 +1965,13 @@ ExecPartitionCheckEmitError(ResultRelInfo 
*resultRelInfo,
        }
        else
        {
-               root_relid = RelationGetRelid(resultRelInfo->ri_RelationDesc);
-               tupdesc = RelationGetDescr(resultRelInfo->ri_RelationDesc);
+               root_rel = resultRelInfo->ri_RelationDesc;
+               tupdesc = RelationGetDescr(root_rel);
                modifiedCols = bms_union(ExecGetInsertedCols(resultRelInfo, 
estate),
                                                                 
ExecGetUpdatedCols(resultRelInfo, estate));
        }
 
-       val_desc = ExecBuildSlotValueDescription(root_relid,
+       val_desc = ExecBuildSlotValueDescription(root_rel,
                                                                                
         slot,
                                                                                
         tupdesc,
                                                                                
         modifiedCols,
@@ -2068,7 +2083,7 @@ ExecConstraints(ResultRelInfo *resultRelInfo,
                        else
                                modifiedCols = 
bms_union(ExecGetInsertedCols(resultRelInfo, estate),
                                                                                
 ExecGetUpdatedCols(resultRelInfo, estate));
-                       val_desc = 
ExecBuildSlotValueDescription(RelationGetRelid(rel),
+                       val_desc = ExecBuildSlotValueDescription(rel,
                                                                                
                         slot,
                                                                                
                         tupdesc,
                                                                                
                         modifiedCols,
@@ -2205,7 +2220,7 @@ ReportNotNullViolationError(ResultRelInfo *resultRelInfo, 
TupleTableSlot *slot,
                modifiedCols = bms_union(ExecGetInsertedCols(resultRelInfo, 
estate),
                                                                 
ExecGetUpdatedCols(resultRelInfo, estate));
 
-       val_desc = ExecBuildSlotValueDescription(RelationGetRelid(rel),
+       val_desc = ExecBuildSlotValueDescription(rel,
                                                                                
         slot,
                                                                                
         tupdesc,
                                                                                
         modifiedCols,
@@ -2313,7 +2328,7 @@ ExecWithCheckOptions(WCOKind kind, ResultRelInfo 
*resultRelInfo,
                                        else
                                                modifiedCols = 
bms_union(ExecGetInsertedCols(resultRelInfo, estate),
                                                                                
                 ExecGetUpdatedCols(resultRelInfo, estate));
-                                       val_desc = 
ExecBuildSlotValueDescription(RelationGetRelid(rel),
+                                       val_desc = 
ExecBuildSlotValueDescription(rel,
                                                                                
                                         slot,
                                                                                
                                         tupdesc,
                                                                                
                                         modifiedCols,
@@ -2392,12 +2407,13 @@ ExecWithCheckOptions(WCOKind kind, ResultRelInfo 
*resultRelInfo,
  * columns they are.
  */
 char *
-ExecBuildSlotValueDescription(Oid reloid,
+ExecBuildSlotValueDescription(Relation rel,
                                                          TupleTableSlot *slot,
                                                          TupleDesc tupdesc,
                                                          Bitmapset 
*modifiedCols,
                                                          int maxfieldlen)
 {
+       Oid                     reloid = RelationGetRelid(rel);
        StringInfoData buf;
        StringInfoData collist;
        bool            write_comma = false;
@@ -2477,7 +2493,23 @@ ExecBuildSlotValueDescription(Oid reloid,
                if (table_perm || column_perm)
                {
                        if (att->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
-                               val = "virtual";
+                       {
+                               Node       *genexpr = 
build_generation_expression(rel, att->attnum);
+                               substitute_actual_values_context cxt;
+                               List       *dpcontext;
+
+                               cxt.slot = slot;
+                               cxt.tupdesc = tupdesc;
+                               genexpr = 
substitute_actual_values_mutator(genexpr, &cxt);
+
+                               /*
+                                * We need dpcontext for any remaining Vars 
that weren't
+                                * substituted (e.g system columns).
+                                */
+                               dpcontext = 
deparse_context_for(RelationGetRelationName(rel), reloid);
+
+                               val = deparse_expression(genexpr, dpcontext, 
false, false);
+                       }
                        else if (slot->tts_isnull[i])
                                val = "null";
                        else
@@ -3241,3 +3273,46 @@ EvalPlanQualEnd(EPQState *epqstate)
        epqstate->relsubs_done = NULL;
        epqstate->relsubs_blocked = NULL;
 }
+
+/*
+ * Replaces Var nodes with Const nodes containing the actual values from the
+ * tuple slot.
+ *
+ * This is used to display the actual values used in virtual generated column
+ * expressions for error messages.
+ */
+static Node *
+substitute_actual_values_mutator(Node *node,
+                                                                
substitute_actual_values_context *context)
+{
+       if (node == NULL)
+               return NULL;
+
+       if (IsA(node, Var))
+       {
+               Var                *var = (Var *) node;
+               int                     attnum = var->varattno;
+
+               if (attnum > 0 && attnum <= context->tupdesc->natts)
+               {
+                       Form_pg_attribute att = TupleDescAttr(context->tupdesc, 
attnum - 1);
+                       Datum           value;
+                       bool            isnull;
+
+                       value = context->slot->tts_values[attnum - 1];
+                       isnull = context->slot->tts_isnull[attnum - 1];
+
+                       return (Node *) makeConst(att->atttypid,
+                                                                         
att->atttypmod,
+                                                                         
att->attcollation,
+                                                                         
att->attlen,
+                                                                         value,
+                                                                         
isnull,
+                                                                         
att->attbyval);
+               }
+       }
+
+       return expression_tree_mutator(node,
+                                                                  
substitute_actual_values_mutator,
+                                                                  context);
+}
diff --git a/src/backend/replication/logical/conflict.c 
b/src/backend/replication/logical/conflict.c
index ca71a81c7bf..478c0a223fc 100644
--- a/src/backend/replication/logical/conflict.c
+++ b/src/backend/replication/logical/conflict.c
@@ -432,7 +432,6 @@ get_tuple_desc(EState *estate, ResultRelInfo *relinfo, 
ConflictType type,
                           Oid indexoid)
 {
        Relation        localrel = relinfo->ri_RelationDesc;
-       Oid                     relid = RelationGetRelid(localrel);
        TupleDesc       tupdesc = RelationGetDescr(localrel);
        char       *desc = NULL;
 
@@ -461,7 +460,7 @@ get_tuple_desc(EState *estate, ResultRelInfo *relinfo, 
ConflictType type,
                 * The 'modifiedCols' only applies to the new tuple, hence we 
pass
                 * NULL for the local row.
                 */
-               desc = ExecBuildSlotValueDescription(relid, localslot, tupdesc,
+               desc = ExecBuildSlotValueDescription(localrel, localslot, 
tupdesc,
                                                                                
         NULL, 64);
 
                if (desc)
@@ -481,7 +480,7 @@ get_tuple_desc(EState *estate, ResultRelInfo *relinfo, 
ConflictType type,
                 */
                modifiedCols = bms_union(ExecGetInsertedCols(relinfo, estate),
                                                                 
ExecGetUpdatedCols(relinfo, estate));
-               desc = ExecBuildSlotValueDescription(relid, remoteslot,
+               desc = ExecBuildSlotValueDescription(localrel, remoteslot,
                                                                                
         tupdesc, modifiedCols,
                                                                                
         64);
 
@@ -510,7 +509,7 @@ get_tuple_desc(EState *estate, ResultRelInfo *relinfo, 
ConflictType type,
                if (OidIsValid(replica_index))
                        desc = build_index_value_desc(estate, localrel, 
searchslot, replica_index);
                else
-                       desc = ExecBuildSlotValueDescription(relid, searchslot, 
tupdesc, NULL, 64);
+                       desc = ExecBuildSlotValueDescription(localrel, 
searchslot, tupdesc, NULL, 64);
 
                if (desc)
                {
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index d46ba59895d..6cc4a96046e 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -269,7 +269,7 @@ extern void ExecPartitionCheckEmitError(ResultRelInfo 
*resultRelInfo,
                                                                                
TupleTableSlot *slot, EState *estate);
 extern void ExecWithCheckOptions(WCOKind kind, ResultRelInfo *resultRelInfo,
                                                                 TupleTableSlot 
*slot, EState *estate);
-extern char *ExecBuildSlotValueDescription(Oid reloid, TupleTableSlot *slot,
+extern char *ExecBuildSlotValueDescription(Relation rel, TupleTableSlot *slot,
                                                                                
   TupleDesc tupdesc,
                                                                                
   Bitmapset *modifiedCols,
                                                                                
   int maxfieldlen);
diff --git a/src/test/regress/expected/generated_virtual.out 
b/src/test/regress/expected/generated_virtual.out
index 6dab60c937b..f4e98286906 100644
--- a/src/test/regress/expected/generated_virtual.out
+++ b/src/test/regress/expected/generated_virtual.out
@@ -638,7 +638,7 @@ CREATE TABLE gtest20 (a int PRIMARY KEY, b int GENERATED 
ALWAYS AS (a * 2) VIRTU
 INSERT INTO gtest20 (a) VALUES (10);  -- ok
 INSERT INTO gtest20 (a) VALUES (30);  -- violates constraint
 ERROR:  new row for relation "gtest20" violates check constraint 
"gtest20_b_check"
-DETAIL:  Failing row contains (30, virtual).
+DETAIL:  Failing row contains (30, (30 * 2)).
 ALTER TABLE gtest20 ALTER COLUMN b SET EXPRESSION AS (a * 100);  -- violates 
constraint
 ERROR:  check constraint "gtest20_b_check" of relation "gtest20" is violated 
by some row
 ALTER TABLE gtest20 ALTER COLUMN b SET EXPRESSION AS (a * 3);  -- ok
@@ -684,18 +684,18 @@ ALTER TABLE gtest20c ADD CONSTRAINT whole_row_check CHECK 
(gtest20c IS NOT NULL)
 INSERT INTO gtest20c VALUES (1);  -- ok
 INSERT INTO gtest20c VALUES (NULL);  -- fails
 ERROR:  new row for relation "gtest20c" violates check constraint 
"whole_row_check"
-DETAIL:  Failing row contains (null, virtual).
+DETAIL:  Failing row contains (null, (NULL::integer * 2)).
 -- not-null constraints
 CREATE TABLE gtest21a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (nullif(a, 
0)) VIRTUAL NOT NULL);
 INSERT INTO gtest21a (a) VALUES (1);  -- ok
 INSERT INTO gtest21a (a) VALUES (0);  -- violates constraint
 ERROR:  null value in column "b" of relation "gtest21a" violates not-null 
constraint
-DETAIL:  Failing row contains (0, virtual).
+DETAIL:  Failing row contains (0, NULLIF(0, 0)).
 -- also check with table constraint syntax
 CREATE TABLE gtest21ax (a int PRIMARY KEY, b int GENERATED ALWAYS AS 
(nullif(a, 0)) VIRTUAL, CONSTRAINT cc NOT NULL b);
 INSERT INTO gtest21ax (a) VALUES (0);  -- violates constraint
 ERROR:  null value in column "b" of relation "gtest21ax" violates not-null 
constraint
-DETAIL:  Failing row contains (0, virtual).
+DETAIL:  Failing row contains (0, NULLIF(0, 0)).
 INSERT INTO gtest21ax (a) VALUES (1);  --ok
 -- SET EXPRESSION supports not null constraint
 ALTER TABLE gtest21ax ALTER COLUMN b SET EXPRESSION AS (nullif(a, 1)); --error
@@ -705,17 +705,17 @@ CREATE TABLE gtest21ax (a int PRIMARY KEY, b int 
GENERATED ALWAYS AS (nullif(a,
 ALTER TABLE gtest21ax ADD CONSTRAINT cc NOT NULL b;
 INSERT INTO gtest21ax (a) VALUES (0);  -- violates constraint
 ERROR:  null value in column "b" of relation "gtest21ax" violates not-null 
constraint
-DETAIL:  Failing row contains (0, virtual).
+DETAIL:  Failing row contains (0, NULLIF(0, 0)).
 DROP TABLE gtest21ax;
 CREATE TABLE gtest21b (a int, b int GENERATED ALWAYS AS (nullif(a, 0)) 
VIRTUAL);
 ALTER TABLE gtest21b ALTER COLUMN b SET NOT NULL;
 INSERT INTO gtest21b (a) VALUES (1);  -- ok
 INSERT INTO gtest21b (a) VALUES (2), (0);  -- violates constraint
 ERROR:  null value in column "b" of relation "gtest21b" violates not-null 
constraint
-DETAIL:  Failing row contains (0, virtual).
+DETAIL:  Failing row contains (0, NULLIF(0, 0)).
 INSERT INTO gtest21b (a) VALUES (NULL);  -- error
 ERROR:  null value in column "b" of relation "gtest21b" violates not-null 
constraint
-DETAIL:  Failing row contains (null, virtual).
+DETAIL:  Failing row contains (null, NULLIF(NULL::integer, 0)).
 ALTER TABLE gtest21b ALTER COLUMN b DROP NOT NULL;
 INSERT INTO gtest21b (a) VALUES (0);  -- ok now
 -- not-null constraint with partitioned table
@@ -730,10 +730,10 @@ CREATE TABLE gtestnn_childdef PARTITION OF gtestnn_parent 
default;
 INSERT INTO gtestnn_parent VALUES (2, 2, default), (3, 5, default), (14, 12, 
default);  -- ok
 INSERT INTO gtestnn_parent VALUES (1, 2, default);  -- error
 ERROR:  null value in column "f3" of relation "gtestnn_child" violates 
not-null constraint
-DETAIL:  Failing row contains (1, 2, virtual).
+DETAIL:  Failing row contains (1, 2, (NULLIF(1, 1) + NULLIF('2'::bigint, 10))).
 INSERT INTO gtestnn_parent VALUES (2, 10, default);  -- error
 ERROR:  null value in column "f3" of relation "gtestnn_child" violates 
not-null constraint
-DETAIL:  Failing row contains (2, 10, virtual).
+DETAIL:  Failing row contains (2, 10, (NULLIF(2, 1) + NULLIF('10'::bigint, 
10))).
 ALTER TABLE gtestnn_parent ALTER COLUMN f3 SET EXPRESSION AS (nullif(f1, 2) + 
nullif(f2, 11));  -- error
 ERROR:  column "f3" of relation "gtestnn_child" contains null values
 INSERT INTO gtestnn_parent VALUES (10, 11, default);  -- ok
diff --git a/src/test/regress/expected/partition_merge.out 
b/src/test/regress/expected/partition_merge.out
index 925fe4f570a..ae40cb9cfcb 100644
--- a/src/test/regress/expected/partition_merge.out
+++ b/src/test/regress/expected/partition_merge.out
@@ -1073,7 +1073,7 @@ INSERT INTO t VALUES (16);
 -- ERROR:  new row for relation "tp_12" violates check constraint "t_i_check"
 INSERT INTO t VALUES (0);
 ERROR:  new row for relation "tp_12" violates check constraint "t_i_check"
-DETAIL:  Failing row contains (0, virtual).
+DETAIL:  Failing row contains (0, (0 + (tableoid)::integer)).
 -- Should be 3 rows: (5), (15), (16):
 SELECT i FROM t ORDER BY i;
  i  
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 1a89ef94bec..a4520dfd502 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2956,6 +2956,7 @@ SubscriptingRefState
 Subscription
 SubscriptionInfo
 SubscriptionRelState
+substitute_actual_values_context
 SummarizerReadLocalXLogPrivate
 SupportRequestCost
 SupportRequestIndexCondition
-- 
2.52.0

Reply via email to