public inbox for [email protected]  
help / color / mirror / Atom feed
[PATCH 6/8] Add default_toast_compression GUC
7+ messages / 4 participants
[nested] [flat]

* [PATCH 6/8] Add default_toast_compression GUC
@ 2021-03-10 08:35 Dilip Kumar <[email protected]>
  0 siblings, 0 replies; 7+ messages in thread

From: Dilip Kumar @ 2021-03-10 08:35 UTC (permalink / raw)

Justin Pryzby and Dilip Kumar
---
 src/backend/access/common/toast_compression.c | 46 +++++++++++++++++++
 src/backend/access/common/tupdesc.c           |  2 +-
 src/backend/bootstrap/bootstrap.c             |  2 +-
 src/backend/commands/tablecmds.c              |  8 ++--
 src/backend/utils/misc/guc.c                  | 12 +++++
 src/backend/utils/misc/postgresql.conf.sample |  1 +
 src/include/access/toast_compression.h        | 22 ++++++++-
 src/test/regress/expected/compression.out     | 16 +++++++
 src/test/regress/expected/compression_1.out   | 19 ++++++++
 src/test/regress/sql/compression.sql          |  8 ++++
 10 files changed, 128 insertions(+), 8 deletions(-)

diff --git a/src/backend/access/common/toast_compression.c b/src/backend/access/common/toast_compression.c
index db4911ce43..27fe47b201 100644
--- a/src/backend/access/common/toast_compression.c
+++ b/src/backend/access/common/toast_compression.c
@@ -55,6 +55,9 @@ const CompressionRoutine toast_compression[] =
 	}
 };
 
+/* Compile-time default */
+char	*default_toast_compression = DEFAULT_TOAST_COMPRESSION;
+
 /*
  * pglz_cmcompress - compression routine for pglz compression method
  *
@@ -306,3 +309,46 @@ GetCompressionRoutines(char method)
 {
 	return &toast_compression[CompressionMethodToId(method)];
 }
+
+/* check_hook: validate new default_toast_compression */
+bool
+check_default_toast_compression(char **newval, void **extra, GucSource source)
+{
+	if (**newval == '\0')
+	{
+		GUC_check_errdetail("%s cannot be empty.",
+							"default_toast_compression");
+		return false;
+	}
+
+	if (strlen(*newval) >= NAMEDATALEN)
+	{
+		GUC_check_errdetail("%s is too long (maximum %d characters).",
+							"default_toast_compression", NAMEDATALEN - 1);
+		return false;
+	}
+
+	if (!CompressionMethodIsValid(CompressionNameToMethod(*newval)))
+	{
+		/*
+		 * When source == PGC_S_TEST, don't throw a hard error for a
+		 * nonexistent compression method, only a NOTICE. See comments in
+		 * guc.h.
+		 */
+		if (source == PGC_S_TEST)
+		{
+			ereport(NOTICE,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+						errmsg("compression method \"%s\" does not exist",
+							*newval)));
+		}
+		else
+		{
+			GUC_check_errdetail("Compression method \"%s\" does not exist.",
+								*newval);
+			return false;
+		}
+	}
+
+	return true;
+}
diff --git a/src/backend/access/common/tupdesc.c b/src/backend/access/common/tupdesc.c
index a66d1113db..362a371a6c 100644
--- a/src/backend/access/common/tupdesc.c
+++ b/src/backend/access/common/tupdesc.c
@@ -668,7 +668,7 @@ TupleDescInitEntry(TupleDesc desc,
 	att->attcollation = typeForm->typcollation;
 
 	if (IsStorageCompressible(typeForm->typstorage))
-		att->attcompression = DefaultCompressionMethod;
+		att->attcompression = GetDefaultToastCompression();
 	else
 		att->attcompression = InvalidCompressionMethod;
 
diff --git a/src/backend/bootstrap/bootstrap.c b/src/backend/bootstrap/bootstrap.c
index af0c0aa77e..9be0841d08 100644
--- a/src/backend/bootstrap/bootstrap.c
+++ b/src/backend/bootstrap/bootstrap.c
@@ -733,7 +733,7 @@ DefineAttr(char *name, char *type, int attnum, int nullness)
 	attrtypes[attnum]->atttypmod = -1;
 	attrtypes[attnum]->attislocal = true;
 	if (IsStorageCompressible(attrtypes[attnum]->attstorage))
-		attrtypes[attnum]->attcompression = DefaultCompressionMethod;
+		attrtypes[attnum]->attcompression = GetDefaultToastCompression();
 	else
 		attrtypes[attnum]->attcompression = InvalidCompressionMethod;
 
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index e20dd5f63c..a0d2af0cde 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -11971,7 +11971,7 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
 		if (!IsStorageCompressible(tform->typstorage))
 			attTup->attcompression = InvalidCompressionMethod;
 		else if (!CompressionMethodIsValid(attTup->attcompression))
-			attTup->attcompression = DefaultCompressionMethod;
+			attTup->attcompression = GetDefaultToastCompression();
 	}
 	else
 		attTup->attcompression = InvalidCompressionMethod;
@@ -17847,9 +17847,9 @@ GetAttributeCompression(Form_pg_attribute att, char *compression)
 
 	/* fallback to default compression if it's not specified */
 	if (compression == NULL)
-		return DefaultCompressionMethod;
-
-	cmethod = CompressionNameToMethod(compression);
+		cmethod = GetDefaultToastCompression();
+	else
+		cmethod = CompressionNameToMethod(compression);
 
 	return cmethod;
 }
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 953837085f..e59ed5b214 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -33,6 +33,7 @@
 #include "access/gin.h"
 #include "access/rmgr.h"
 #include "access/tableam.h"
+#include "access/toast_compression.h"
 #include "access/transam.h"
 #include "access/twophase.h"
 #include "access/xact.h"
@@ -3915,6 +3916,17 @@ static struct config_string ConfigureNamesString[] =
 		check_default_table_access_method, NULL, NULL
 	},
 
+	{
+		{"default_toast_compression", PGC_USERSET, CLIENT_CONN_STATEMENT,
+			gettext_noop("Sets the default compression for new columns."),
+			NULL,
+			GUC_IS_NAME
+		},
+		&default_toast_compression,
+		DEFAULT_TOAST_COMPRESSION,
+		check_default_toast_compression, NULL, NULL
+	},
+
 	{
 		{"default_tablespace", PGC_USERSET, CLIENT_CONN_STATEMENT,
 			gettext_noop("Sets the default tablespace to create tables and indexes in."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index f46c2dd7a8..7d7a433dcc 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -659,6 +659,7 @@
 #temp_tablespaces = ''			# a list of tablespace names, '' uses
 					# only default tablespace
 #default_table_access_method = 'heap'
+#default_toast_compression = 'pglz'	# 'pglz' or 'lz4'
 #check_function_bodies = on
 #default_transaction_isolation = 'read committed'
 #default_transaction_read_only = off
diff --git a/src/include/access/toast_compression.h b/src/include/access/toast_compression.h
index 38800cb97a..e9d9ce5634 100644
--- a/src/include/access/toast_compression.h
+++ b/src/include/access/toast_compression.h
@@ -15,6 +15,14 @@
 
 #include "postgres.h"
 
+#include "utils/guc.h"
+
+/* default compression method if not specified. */
+#define DEFAULT_TOAST_COMPRESSION	"pglz"
+
+/* GUCs */
+extern char *default_toast_compression;
+
 /*
  * Built-in compression methods.  pg_attribute will store this in the
  * attcompression column.
@@ -36,8 +44,6 @@ typedef enum CompressionId
 	LZ4_COMPRESSION_ID = 1
 } CompressionId;
 
-/* use default compression method if it is not specified. */
-#define DefaultCompressionMethod PGLZ_COMPRESSION
 #define IsValidCompression(cm)  ((cm) != InvalidCompressionMethod)
 
 #define IsStorageCompressible(storage) ((storage) != TYPSTORAGE_PLAIN && \
@@ -67,6 +73,8 @@ typedef struct CompressionRoutine
 
 extern char CompressionNameToMethod(char *compression);
 extern const CompressionRoutine *GetCompressionRoutines(char method);
+extern bool check_default_toast_compression(char **newval, void **extra,
+											GucSource source);
 
 /*
  * CompressionMethodToId - Convert compression method to compression id.
@@ -115,5 +123,15 @@ GetCompressionMethodName(char method)
 	return GetCompressionRoutines(method)->cmname;
 }
 
+/*
+ * GetDefaultToastCompression -- get the current toast compression
+ *
+ * This exists to hide the use of the default_toast_compression GUC variable.
+ */
+static inline char
+GetDefaultToastCompression(void)
+{
+	return CompressionNameToMethod(default_toast_compression);
+}
 
 #endif							/* TOAST_COMPRESSION_H */
diff --git a/src/test/regress/expected/compression.out b/src/test/regress/expected/compression.out
index 6981d580dc..9ddffc8b18 100644
--- a/src/test/regress/expected/compression.out
+++ b/src/test/regress/expected/compression.out
@@ -231,6 +231,22 @@ CREATE TABLE cminh(f1 TEXT COMPRESSION lz4) INHERITS(cmdata);
 NOTICE:  merging column "f1" with inherited definition
 ERROR:  column "f1" has a compression method conflict
 DETAIL:  pglz versus lz4
+-- test default_toast_compression GUC
+SET default_toast_compression = '';
+ERROR:  invalid value for parameter "default_toast_compression": ""
+DETAIL:  default_toast_compression cannot be empty.
+SET default_toast_compression = 'I do not exist compression';
+ERROR:  invalid value for parameter "default_toast_compression": "I do not exist compression"
+DETAIL:  Compression method "I do not exist compression" does not exist.
+SET default_toast_compression = 'lz4';
+DROP TABLE cmdata2;
+CREATE TABLE cmdata2 (f1 text);
+\d+ cmdata2
+                                        Table "public.cmdata2"
+ Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
+--------+------+-----------+----------+---------+----------+-------------+--------------+-------------
+ f1     | text |           |          |         | extended | lz4         |              | 
+
 -- check data is ok
 SELECT length(f1) FROM cmdata;
  length 
diff --git a/src/test/regress/expected/compression_1.out b/src/test/regress/expected/compression_1.out
index 893c18c7fa..03a7f46554 100644
--- a/src/test/regress/expected/compression_1.out
+++ b/src/test/regress/expected/compression_1.out
@@ -237,6 +237,25 @@ CREATE TABLE cminh(f1 TEXT COMPRESSION lz4) INHERITS(cmdata);
 NOTICE:  merging column "f1" with inherited definition
 ERROR:  column "f1" has a compression method conflict
 DETAIL:  pglz versus lz4
+-- test default_toast_compression GUC
+SET default_toast_compression = '';
+ERROR:  invalid value for parameter "default_toast_compression": ""
+DETAIL:  default_toast_compression cannot be empty.
+SET default_toast_compression = 'I do not exist compression';
+ERROR:  invalid value for parameter "default_toast_compression": "I do not exist compression"
+DETAIL:  Compression method "I do not exist compression" does not exist.
+SET default_toast_compression = 'lz4';
+ERROR:  unsupported LZ4 compression method
+DETAIL:  This functionality requires the server to be built with lz4 support.
+HINT:  You need to rebuild PostgreSQL using --with-lz4.
+DROP TABLE cmdata2;
+CREATE TABLE cmdata2 (f1 text);
+\d+ cmdata2
+                                        Table "public.cmdata2"
+ Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
+--------+------+-----------+----------+---------+----------+-------------+--------------+-------------
+ f1     | text |           |          |         | extended | pglz        |              | 
+
 -- check data is ok
 SELECT length(f1) FROM cmdata;
  length 
diff --git a/src/test/regress/sql/compression.sql b/src/test/regress/sql/compression.sql
index b085c9fbd9..6c9b24f1f7 100644
--- a/src/test/regress/sql/compression.sql
+++ b/src/test/regress/sql/compression.sql
@@ -103,6 +103,14 @@ SELECT pg_column_compression(f1) FROM cmpart;
 CREATE TABLE cminh() INHERITS(cmdata, cmdata1);
 CREATE TABLE cminh(f1 TEXT COMPRESSION lz4) INHERITS(cmdata);
 
+-- test default_toast_compression GUC
+SET default_toast_compression = '';
+SET default_toast_compression = 'I do not exist compression';
+SET default_toast_compression = 'lz4';
+DROP TABLE cmdata2;
+CREATE TABLE cmdata2 (f1 text);
+\d+ cmdata2
+
 -- check data is ok
 SELECT length(f1) FROM cmdata;
 SELECT length(f1) FROM cmdata1;
-- 
2.17.0


--C94crkcyjafcjHxo
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
 filename="0007-Alter-table-set-compression.patch"



^ permalink  raw  reply  [nested|flat] 7+ messages in thread

* Re: Show expression of virtual columns in error messages
@ 2026-02-05 18:51 Matheus Alcantara <[email protected]>
  2026-02-06 03:54 ` Re: Show expression of virtual columns in error messages Yugo Nagata <[email protected]>
  0 siblings, 1 reply; 7+ messages in thread

From: Matheus Alcantara @ 2026-02-05 18:51 UTC (permalink / raw)
  To: Yugo Nagata <[email protected]>; +Cc: Peter Eisentraut <[email protected]>; [email protected]

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



Attachments:

  [text/plain] v2-0001-Show-expression-of-virtual-columns-in-error-messa.patch (15.2K, ../../[email protected]/2-v2-0001-Show-expression-of-virtual-columns-in-error-messa.patch)
  download | inline diff:
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



^ permalink  raw  reply  [nested|flat] 7+ messages in thread

* Re: Show expression of virtual columns in error messages
  2026-02-05 18:51 Re: Show expression of virtual columns in error messages Matheus Alcantara <[email protected]>
@ 2026-02-06 03:54 ` Yugo Nagata <[email protected]>
  2026-02-06 12:25   ` Re: Show expression of virtual columns in error messages Matheus Alcantara <[email protected]>
  0 siblings, 1 reply; 7+ messages in thread

From: Yugo Nagata @ 2026-02-06 03:54 UTC (permalink / raw)
  To: Matheus Alcantara <[email protected]>; +Cc: Peter Eisentraut <[email protected]>; [email protected]

On Thu, 05 Feb 2026 15:51:54 -0300
"Matheus Alcantara" <[email protected]> wrote:

> 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.

I initially thought about having deparse_context_for() directly output
actual values for Var references, but that does not seem like a good
approach. Replacing Vars with Consts beforehand using
expression_tree_mutator, as you did, looks like a better solution.

One thing I noticed is that some expressions in virtual columns (e.g.,
NULL and negative integers) differ from those in normal columns, for
example in the following (null vs. NULL):

+DETAIL:  Failing row contains (null, NULLIF(NULL::integer, 0)).

However, this seems acceptable.

Overall, this patch looks good to me.

Regards,
Yugo Nagata

> > 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
> 


-- 
Yugo Nagata <[email protected]>






^ permalink  raw  reply  [nested|flat] 7+ messages in thread

* Re: Show expression of virtual columns in error messages
  2026-02-05 18:51 Re: Show expression of virtual columns in error messages Matheus Alcantara <[email protected]>
  2026-02-06 03:54 ` Re: Show expression of virtual columns in error messages Yugo Nagata <[email protected]>
@ 2026-02-06 12:25   ` Matheus Alcantara <[email protected]>
  2026-02-26 18:06     ` Re: Show expression of virtual columns in error messages Matheus Alcantara <[email protected]>
  0 siblings, 1 reply; 7+ messages in thread

From: Matheus Alcantara @ 2026-02-06 12:25 UTC (permalink / raw)
  To: Yugo Nagata <[email protected]>; +Cc: Peter Eisentraut <[email protected]>; [email protected]

On Fri Feb 6, 2026 at 12:54 AM -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.
>
> I initially thought about having deparse_context_for() directly output
> actual values for Var references, but that does not seem like a good
> approach. Replacing Vars with Consts beforehand using
> expression_tree_mutator, as you did, looks like a better solution.
>
> One thing I noticed is that some expressions in virtual columns (e.g.,
> NULL and negative integers) differ from those in normal columns, for
> example in the following (null vs. NULL):
>
> +DETAIL:  Failing row contains (null, NULLIF(NULL::integer, 0)).
>
> However, this seems acceptable.
>
Yeah, after parsing we don't have the original string representation of
the parse node, so at deparse_expression() it has it's own style to
deparse nodes. I don't think that it's an issue and I also think that
it's accepttable. 

> Overall, this patch looks good to me.
>
Thank you for reviewing the patch!

--
Matheus Alcantara
EDB: https://www.enterprisedb.com






^ permalink  raw  reply  [nested|flat] 7+ messages in thread

* Re: Show expression of virtual columns in error messages
  2026-02-05 18:51 Re: Show expression of virtual columns in error messages Matheus Alcantara <[email protected]>
  2026-02-06 03:54 ` Re: Show expression of virtual columns in error messages Yugo Nagata <[email protected]>
  2026-02-06 12:25   ` Re: Show expression of virtual columns in error messages Matheus Alcantara <[email protected]>
@ 2026-02-26 18:06     ` Matheus Alcantara <[email protected]>
  2026-02-26 18:47       ` Re: Show expression of virtual columns in error messages Tom Lane <[email protected]>
  0 siblings, 1 reply; 7+ messages in thread

From: Matheus Alcantara @ 2026-02-26 18:06 UTC (permalink / raw)
  To: [email protected]; +Cc: Peter Eisentraut <[email protected]>; Yugo Nagata <[email protected]>

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



Attachments:

  [text/plain] v3-0001-Show-expression-of-virtual-columns-in-error-messa.patch (15.2K, ../../[email protected]/2-v3-0001-Show-expression-of-virtual-columns-in-error-messa.patch)
  download | inline diff:
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



^ permalink  raw  reply  [nested|flat] 7+ messages in thread

* Re: Show expression of virtual columns in error messages
  2026-02-05 18:51 Re: Show expression of virtual columns in error messages Matheus Alcantara <[email protected]>
  2026-02-06 03:54 ` Re: Show expression of virtual columns in error messages Yugo Nagata <[email protected]>
  2026-02-06 12:25   ` Re: Show expression of virtual columns in error messages Matheus Alcantara <[email protected]>
  2026-02-26 18:06     ` Re: Show expression of virtual columns in error messages Matheus Alcantara <[email protected]>
@ 2026-02-26 18:47       ` Tom Lane <[email protected]>
  2026-02-26 20:29         ` Re: Show expression of virtual columns in error messages Matheus Alcantara <[email protected]>
  0 siblings, 1 reply; 7+ messages in thread

From: Tom Lane @ 2026-02-26 18:47 UTC (permalink / raw)
  To: Matheus Alcantara <[email protected]>; +Cc: [email protected]; Peter Eisentraut <[email protected]>; Yugo Nagata <[email protected]>

Matheus Alcantara <[email protected]> writes:
> Attached rebased v3 due to f80bedd52b1. No additional changes compared 
> to v2.

I looked at this and frankly I think it's going in the wrong
direction.  I agree that showing "virtual" is unhelpful, but
I don't like this approach for a couple of reasons:

1. Inserting a column expression could easily make the error
message annoyingly long.

2. Adding this much complexity in an error code path doesn't
seem like a good idea.  Such paths are typically not well
tested, and if there is any bug lurking, it will result in
an error message quite removed from the user's problem.

3. It's making virtual generated columns behave (even more)
differently from stored generated columns.  I think the
general plan has been to make them act as alike as possible.

So what would comport better with the behavior of stored columns
is to show the expression's value.  I agree with you that
calculating that in the error path is a no-go, but haven't we
computed it earlier?  Or could we do so, if there are constraints
to be checked?

			regards, tom lane






^ permalink  raw  reply  [nested|flat] 7+ messages in thread

* Re: Show expression of virtual columns in error messages
  2026-02-05 18:51 Re: Show expression of virtual columns in error messages Matheus Alcantara <[email protected]>
  2026-02-06 03:54 ` Re: Show expression of virtual columns in error messages Yugo Nagata <[email protected]>
  2026-02-06 12:25   ` Re: Show expression of virtual columns in error messages Matheus Alcantara <[email protected]>
  2026-02-26 18:06     ` Re: Show expression of virtual columns in error messages Matheus Alcantara <[email protected]>
  2026-02-26 18:47       ` Re: Show expression of virtual columns in error messages Tom Lane <[email protected]>
@ 2026-02-26 20:29         ` Matheus Alcantara <[email protected]>
  0 siblings, 0 replies; 7+ messages in thread

From: Matheus Alcantara @ 2026-02-26 20:29 UTC (permalink / raw)
  To: Tom Lane <[email protected]>; +Cc: [email protected]; Peter Eisentraut <[email protected]>; Yugo Nagata <[email protected]>

On Thu Feb 26, 2026 at 3:47 PM -03, Tom Lane wrote:
> Matheus Alcantara <[email protected]> writes:
>> Attached rebased v3 due to f80bedd52b1. No additional changes compared 
>> to v2.
>
> I looked at this and frankly I think it's going in the wrong
> direction.  I agree that showing "virtual" is unhelpful, but
> I don't like this approach for a couple of reasons:
>
> 1. Inserting a column expression could easily make the error
> message annoyingly long.
>
> 2. Adding this much complexity in an error code path doesn't
> seem like a good idea.  Such paths are typically not well
> tested, and if there is any bug lurking, it will result in
> an error message quite removed from the user's problem.
>
> 3. It's making virtual generated columns behave (even more)
> differently from stored generated columns.  I think the
> general plan has been to make them act as alike as possible.
>
> So what would comport better with the behavior of stored columns
> is to show the expression's value.  I agree with you that
> calculating that in the error path is a no-go, but haven't we
> computed it earlier?  Or could we do so, if there are constraints
> to be checked?

Thanks for the feedback. After investigating the code a bit, I found
that IIUC virtual column values are actually never computed and stored
separately, they're computed by expanding the expression wherever the
column is referenced.

According to my understanding, here's how it works during constraint
checking in ExecRelCheck():

1. The constraint expression (e.g., c > 10) is loaded from the catalog
2. Virtual column references are expanded via
expand_generated_columns_in_expr(), transforming it to (a + b) > 10 
3. ExecCheck() evaluates the entire expanded expression, reading base
column values (a, b) directly from the slot and returns the if the check
was true or false.

The virtual column value (a + b) is computed as an intermediate result
during expression evaluation of a check constraint but is never stored
anywhere accessible.

So, if all of this make sense and it's correct it seems to me that we
indeed need to compute the virtual expression value on error path to
show on error message although it would not be desirable, or I'm missing
something?

--
Matheus Alcantara
EDB: https://www.enterprisedb.com






^ permalink  raw  reply  [nested|flat] 7+ messages in thread


end of thread, other threads:[~2026-02-26 20:29 UTC | newest]

Thread overview: 7+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2021-03-10 08:35 [PATCH 6/8] Add default_toast_compression GUC Dilip Kumar <[email protected]>
2026-02-05 18:51 Re: Show expression of virtual columns in error messages Matheus Alcantara <[email protected]>
2026-02-06 03:54 ` Re: Show expression of virtual columns in error messages Yugo Nagata <[email protected]>
2026-02-06 12:25   ` Re: Show expression of virtual columns in error messages Matheus Alcantara <[email protected]>
2026-02-26 18:06     ` Re: Show expression of virtual columns in error messages Matheus Alcantara <[email protected]>
2026-02-26 18:47       ` Re: Show expression of virtual columns in error messages Tom Lane <[email protected]>
2026-02-26 20:29         ` Re: Show expression of virtual columns in error messages Matheus Alcantara <[email protected]>

This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox