On Wed Feb 4, 2026 at 11:45 PM -03, Yugo Nagata wrote: >> Another possibility would be to get the actual values of "a" for example >> and show it on the error message, e.g: >> >> ERROR: new row for relation "t" violates check constraint "t_c_check" >> DETAIL: Failing row contains (5, 10, 5 * 2). > > That would indeed be more useful. One way to achieve this might be to > modify deparse_context and get_variable() so that a Var is displayed as its > actual value. > I'm not sure if I understand how modifying deparse_context_for() could help on this.
What I did was to use the expression_tree_mutator API to mutate the virtual column expression to replace any Var reference with the value into the TupleTableSlot. Please see the attached v2 version. > Another possibility would be to include column names in the DETAIL message, > for example: > > ERROR: new row for relation "t" violates check constraint "t_c_check" > DETAIL: Failing row contains (a, b, c)=(5, 10, a * 2). > > Although this would change the existing message format, including column > names could generally provide users with more information about the error. > I think that this could make the error output too verbose when there is a lot of columns involved on the statement. -- Matheus Alcantara EDB: https://www.enterprisedb.com
From 76b5b36ba3b103e7c501de0bd94e9bb1afcdcf75 Mon Sep 17 00:00:00 2001 From: Matheus Alcantara <[email protected]> Date: Mon, 2 Feb 2026 19:06:44 -0300 Subject: [PATCH v2] 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 55a7d930d26..2ffb97d48ca 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 249e68be654..a55470fd47f 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 (currently not supported) ERROR: ALTER TABLE / SET EXPRESSION is not supported for virtual generated columns in tables with check constraints DETAIL: Column "b" of relation "gtest20" is a virtual generated column. @@ -666,18 +666,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 @@ -687,17 +687,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 @@ -712,10 +712,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 9f5ee8fd482..0bc6f86b884 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2950,6 +2950,7 @@ SubscriptingRefState Subscription SubscriptionInfo SubscriptionRelState +substitute_actual_values_context SummarizerReadLocalXLogPrivate SupportRequestCost SupportRequestIndexCondition -- 2.52.0
