Subject: support virtual generated column not null constraint

From: jian he <[email-masked]>
Date: Mon, 10 Feb 2025 21:33:41 +0800

hi.

Virtual generated columns committed,
https://git.postgresql.org/cgit/postgresql.git/commit/?id=83ea6c54025bea67bcd4949a6d58d3fc11c3e21b

This patch is for implementing not null constraints on virtual
generated columns.

NOT NULL constraints on virtual generated columns mean that
if we INSERT a row into the table and the evaluation of the generated
expression results in a null value,
an ERRCODE_NOT_NULL_VIOLATION error will be reported.

main gotcha is in ExecConstraints, expand the generated expression
and convert a not null constraint to a check constraint and evaluate it.
From 945ee799beca62e76f128660aef658ef61a11a8a Mon Sep 17 00:00:00 2001
From: jian he <[email-masked]>
Date: Mon, 10 Feb 2025 21:20:02 +0800
Subject: [PATCH v1 1/1] support virtual generated column not null constraint

now we can add not null constraint on virtual generated column.  not null
constraint on virtual generated column make sure evaulation of the expanded
generated expression does not yield null.

discussion: https://postgr.es/m/
---
 src/backend/catalog/heap.c                    |  10 --
 src/backend/commands/tablecmds.c              |   8 -
 src/backend/executor/execMain.c               | 152 ++++++++++++++++++
 src/backend/parser/parse_utilcmd.c            |  14 --
 src/include/nodes/execnodes.h                 |   2 +
 .../regress/expected/generated_virtual.out    |  27 ++--
 src/test/regress/sql/generated_virtual.sql    |  12 +-
 7 files changed, 172 insertions(+), 53 deletions(-)

diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 956f196fc9..2d42761c91 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -2564,11 +2564,6 @@ AddRelationNewConstraints(Relation rel,
 						errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 						errmsg("cannot add not-null constraint on system column \"%s\"",
 							   strVal(linitial(cdef->keys))));
-			/* TODO: see transformColumnDefinition() */
-			if (get_attgenerated(RelationGetRelid(rel), colnum) == ATTRIBUTE_GENERATED_VIRTUAL)
-				ereport(ERROR,
-						errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-						errmsg("not-null constraints are not supported on virtual generated columns"));
 
 			/*
 			 * If the column already has a not-null constraint, we don't want
@@ -2884,11 +2879,6 @@ AddRelationNotNullConstraints(Relation rel, List *constraints,
 					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 					errmsg("cannot add not-null constraint on system column \"%s\"",
 						   strVal(linitial(constr->keys))));
-		/* TODO: see transformColumnDefinition() */
-		if (get_attgenerated(RelationGetRelid(rel), attnum) == ATTRIBUTE_GENERATED_VIRTUAL)
-			ereport(ERROR,
-					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-					errmsg("not-null constraints are not supported on virtual generated columns"));
 
 		/*
 		 * A column can only have one not-null constraint, so discard any
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 5823fce934..d672d1b060 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -7803,14 +7803,6 @@ ATExecSetNotNull(List **wqueue, Relation rel, char *conName, char *colName,
 				 errmsg("cannot alter system column \"%s\"",
 						colName)));
 
-	/* TODO: see transformColumnDefinition() */
-	if (TupleDescAttr(RelationGetDescr(rel), attnum - 1)->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
-		ereport(ERROR,
-				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-				 errmsg("not-null constraints are not supported on virtual generated columns"),
-				 errdetail("Column \"%s\" of relation \"%s\" is a virtual generated column.",
-						   colName, RelationGetRelationName(rel))));
-
 	/* See if there's already a constraint */
 	tuple = findNotNullConstraintAttnum(RelationGetRelid(rel), attnum);
 	if (HeapTupleIsValid(tuple))
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 39d80ccfba..70eb2370bb 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -51,6 +51,7 @@
 #include "foreign/fdwapi.h"
 #include "mb/pg_wchar.h"
 #include "miscadmin.h"
+#include "nodes/makefuncs.h"
 #include "nodes/queryjumble.h"
 #include "parser/parse_relation.h"
 #include "pgstat.h"
@@ -1268,6 +1269,7 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
 	resultRelInfo->ri_FdwState = NULL;
 	resultRelInfo->ri_usesFdwDirectModify = false;
 	resultRelInfo->ri_ConstraintExprs = NULL;
+	resultRelInfo->ri_VirGeneratedConstraintExprs = NULL;
 	resultRelInfo->ri_GeneratedExprsI = NULL;
 	resultRelInfo->ri_GeneratedExprsU = NULL;
 	resultRelInfo->ri_projectReturning = NULL;
@@ -1737,6 +1739,83 @@ ExecutePlan(QueryDesc *queryDesc,
 }

+/*
+ * check whether the virtual generated column is null.
+ * Expand the generated expression and evaluate it.  A return value of -1
+ * indicates that the generated expression is not null, while a value greater
+ * than 0 signifies null. This is similar to ExecRelCheck.
+*/
+static int
+ExecRelCheckGenVirtualNotNull(ResultRelInfo *resultRelInfo, TupleTableSlot *slot,
+							  EState *estate, Bitmapset *gen_virtual_cols)
+{
+	Relation	rel = resultRelInfo->ri_RelationDesc;
+	TupleDesc	tupdesc = RelationGetDescr(rel);
+	ExprContext *econtext;
+	MemoryContext oldContext;
+	int			i = 0;
+	int			attidx = -1;
+	int			*attnums;
+	attnums = (int *) palloc0(bms_num_members(gen_virtual_cols) * sizeof(int));
+
+	if (resultRelInfo->ri_VirGeneratedConstraintExprs == NULL)
+	{
+		oldContext = MemoryContextSwitchTo(estate->es_query_cxt);
+		resultRelInfo->ri_VirGeneratedConstraintExprs =
+			(ExprState **) palloc0(bms_num_members(gen_virtual_cols) * sizeof(ExprState *));
+
+		while ((attidx = bms_next_member(gen_virtual_cols, attidx)) >= 0)
+		{
+			Expr	   *checkconstr;
+			NullTest   *nnulltest = makeNode(NullTest);
+			Form_pg_attribute attr = TupleDescAttr(tupdesc, attidx -1);
+
+			nnulltest->arg = (Expr *) makeVar(1,
+											  attr->attnum,
+											  attr->atttypid,
+											  attr->atttypmod,
+											  attr->attcollation,
+											  0);
+			nnulltest->nulltesttype = IS_NOT_NULL;
+			nnulltest->argisrow = false;
+			nnulltest->location = -1;
+
+			checkconstr = (Expr *) nnulltest;
+			checkconstr = (Expr *) expand_generated_columns_in_expr((Node *) checkconstr, rel, 1);
+			resultRelInfo->ri_VirGeneratedConstraintExprs[i] = ExecPrepareExpr(checkconstr, estate);
+			attnums[i++] = attidx;
+		}
+		MemoryContextSwitchTo(oldContext);
+	}
+	else
+	{
+		while ((attidx = bms_next_member(gen_virtual_cols, attidx)) >= 0)
+			attnums[i++] = attidx;
+	}
+
+	/*
+	 * We will use the EState's per-tuple context for evaluating virtual
+	 * generated column check constraint expressions (creating it if it's not
+	 * already there).
+	 */
+	econtext = GetPerTupleExprContext(estate);
+
+	/* Arrange for econtext's scan tuple to be the tuple under test */
+	econtext->ecxt_scantuple = slot;
+
+	/* And evaluate the check constraints for virtual generated column */
+	for (i = 0; i < bms_num_members(gen_virtual_cols); i++)
+	{
+		ExprState  *gen_virtualnn = resultRelInfo->ri_VirGeneratedConstraintExprs[i];
+
+		if (gen_virtualnn && !ExecCheck(gen_virtualnn, econtext))
+			return attnums[i];
+	}
+
+	/* -1 result means no error */
+	return -1;
+}
+
 /*
  * ExecRelCheck --- check that tuple meets constraints for result relation
  *
@@ -1972,6 +2051,8 @@ ExecConstraints(ResultRelInfo *resultRelInfo,
 				Relation	orig_rel = rel;
 				TupleDesc	orig_tupdesc = RelationGetDescr(rel);
 
+				if (att->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+					continue;
 				/*
 				 * If the tuple has been routed, it's been converted to the
 				 * partition's rowtype, which might differ from the root
@@ -2021,6 +2102,77 @@ ExecConstraints(ResultRelInfo *resultRelInfo,
 		}
 	}
 
+	/* check virtual generated column is not null or not */
+	if (constr->has_not_null && constr->has_generated_virtual)
+	{
+		int			attnum;
+		Bitmapset  *vir_gencols = NULL;
+		char	   *val_desc = NULL;
+		Form_pg_attribute att;
+
+		for (attnum = 1; attnum <= tupdesc->natts; attnum++)
+		{
+			att = TupleDescAttr(tupdesc, attnum - 1);
+			if (att->attnotnull && att->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+				vir_gencols = bms_add_member(vir_gencols, att->attnum);
+		}
+		attnum = -1;
+
+		if(vir_gencols != NULL)
+		{
+			Relation	orig_rel_nn = rel;
+
+			attnum = ExecRelCheckGenVirtualNotNull(resultRelInfo, slot, estate, vir_gencols);
+
+			if (attnum > 0)
+			{
+				/* null value on virtual generated column. now format the error message */
+				if (resultRelInfo->ri_RootResultRelInfo)
+				{
+					ResultRelInfo *rootrel = resultRelInfo->ri_RootResultRelInfo;
+					TupleDesc	old_tupdesc = RelationGetDescr(rel);
+					AttrMap    *map;
+
+					tupdesc = RelationGetDescr(rootrel->ri_RelationDesc);
+					/* a reverse map */
+					map = build_attrmap_by_name_if_req(old_tupdesc,
+													   tupdesc,
+													   false);
+					/*
+					* Partition-specific slot's tupdesc can't be changed, so
+					* allocate a new one.
+					*/
+					if (map != NULL)
+						slot = execute_attr_map_slot(map, slot,
+													 MakeTupleTableSlot(tupdesc, &TTSOpsVirtual));
+					modifiedCols = bms_union(ExecGetInsertedCols(rootrel, estate),
+											 ExecGetUpdatedCols(rootrel, estate));
+
+					rel = rootrel->ri_RelationDesc;
+				}
+				else
+					modifiedCols = bms_union(ExecGetInsertedCols(resultRelInfo, estate),
+											 ExecGetUpdatedCols(resultRelInfo, estate));
+
+				val_desc = ExecBuildSlotValueDescription(RelationGetRelid(rel),
+														 slot,
+														 tupdesc,
+														 modifiedCols,
+														 64);
+
+				att = TupleDescAttr(tupdesc, attnum - 1);
+
+				ereport(ERROR,
+						errcode(ERRCODE_NOT_NULL_VIOLATION),
+						errmsg("null value in column \"%s\" of relation \"%s\" violates not-null constraint",
+								NameStr(att->attname),
+								RelationGetRelationName(orig_rel_nn)),
+						val_desc ? errdetail("Failing row contains %s.", val_desc) : 0,
+						errtablecol(orig_rel_nn, attnum));
+			}
+		}
+	}
+
 	if (rel->rd_rel->relchecks > 0)
 	{
 		const char *failed;
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index eb7716cd84..ba309e47f0 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -988,20 +988,6 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column)
 							column->colname, cxt->relation->relname),
 					 parser_errposition(cxt->pstate,
 										constraint->location)));
-
-		/*
-		 * TODO: Straightforward not-null constraints won't work on virtual
-		 * generated columns, because there is no support for expanding the
-		 * column when the constraint is checked.  Maybe we could convert the
-		 * not-null constraint into a full check constraint, so that the
-		 * generation expression can be expanded at check time.
-		 */
-		if (column->is_not_null && column->generated == ATTRIBUTE_GENERATED_VIRTUAL)
-			ereport(ERROR,
-					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-					 errmsg("not-null constraints are not supported on virtual generated columns"),
-					 parser_errposition(cxt->pstate,
-										constraint->location)));
 	}
 
 	/*
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index e2d1dc1e06..1595fae215 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -548,6 +548,8 @@ typedef struct ResultRelInfo
 	/* array of constraint-checking expr states */
 	ExprState **ri_ConstraintExprs;
 
+	/* array of virtual generated not null constraint-checking expr states */
+	ExprState **ri_VirGeneratedConstraintExprs;
 	/*
 	 * Arrays of stored generated columns ExprStates for INSERT/UPDATE/MERGE.
 	 */
diff --git a/src/test/regress/expected/generated_virtual.out b/src/test/regress/expected/generated_virtual.out
index 35638812be..826c76b900 100644
--- a/src/test/regress/expected/generated_virtual.out
+++ b/src/test/regress/expected/generated_virtual.out
@@ -660,28 +660,25 @@ 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).
--- not-null constraints (currently not supported)
 CREATE TABLE gtest21a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (nullif(a, 0)) VIRTUAL NOT NULL);
-ERROR:  not-null constraints are not supported on virtual generated columns
-LINE 1: ... 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
+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).
 -- 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);  -- error
-ERROR:  not-null constraints are not supported on virtual generated columns
+DROP TABLE gtest21ax;
 CREATE TABLE gtest21ax (a int PRIMARY KEY, b int GENERATED ALWAYS AS (nullif(a, 0)) VIRTUAL);
 ALTER TABLE gtest21ax ADD CONSTRAINT cc NOT NULL b;  -- error
-ERROR:  not-null constraints are not supported on virtual generated columns
 DROP TABLE gtest21ax;
 CREATE TABLE gtest21b (a int PRIMARY KEY, b int GENERATED ALWAYS AS (nullif(a, 0)) VIRTUAL);
 ALTER TABLE gtest21b ALTER COLUMN b SET NOT NULL;
-ERROR:  not-null constraints are not supported on virtual generated columns
-DETAIL:  Column "b" of relation "gtest21b" is a virtual generated column.
---INSERT INTO gtest21b (a) VALUES (1);  -- ok
---INSERT INTO gtest21b (a) VALUES (0);  -- violates constraint
+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).
 ALTER TABLE gtest21b ALTER COLUMN b DROP NOT NULL;
---INSERT INTO gtest21b (a) VALUES (0);  -- ok now
+INSERT INTO gtest21b (a) VALUES (0);  -- ok now
 -- index constraints
 CREATE TABLE gtest22a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a / 2) VIRTUAL UNIQUE);
 ERROR:  unique constraints on virtual generated columns are not supported
@@ -689,7 +686,7 @@ ERROR:  unique constraints on virtual generated columns are not supported
 --INSERT INTO gtest22a VALUES (3);
 --INSERT INTO gtest22a VALUES (4);
 CREATE TABLE gtest22b (a int, b int GENERATED ALWAYS AS (a / 2) VIRTUAL, PRIMARY KEY (a, b));
-ERROR:  not-null constraints are not supported on virtual generated columns
+ERROR:  unique constraints on virtual generated columns are not supported
 --INSERT INTO gtest22b VALUES (2);
 --INSERT INTO gtest22b VALUES (2);
 -- indexes
@@ -734,7 +731,7 @@ ERROR:  foreign key constraints on virtual generated columns are not supported
 --DROP TABLE gtest23b;
 --DROP TABLE gtest23a;
 CREATE TABLE gtest23p (x int, y int GENERATED ALWAYS AS (x * 2) VIRTUAL, PRIMARY KEY (y));
-ERROR:  not-null constraints are not supported on virtual generated columns
+ERROR:  unique constraints on virtual generated columns are not supported
 --INSERT INTO gtest23p VALUES (1), (2), (3);
 CREATE TABLE gtest23q (a int PRIMARY KEY, b int REFERENCES gtest23p (y));
 ERROR:  relation "gtest23p" does not exist
diff --git a/src/test/regress/sql/generated_virtual.sql b/src/test/regress/sql/generated_virtual.sql
index 3487081391..1ac92b2204 100644
--- a/src/test/regress/sql/generated_virtual.sql
+++ b/src/test/regress/sql/generated_virtual.sql
@@ -332,23 +332,23 @@ 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
 
--- not-null constraints (currently not supported)
 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
+INSERT INTO gtest21a (a) VALUES (1);  -- ok
+INSERT INTO gtest21a (a) VALUES (0);  -- violates constraint
 
 -- 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);  -- error
+DROP TABLE gtest21ax;
 CREATE TABLE gtest21ax (a int PRIMARY KEY, b int GENERATED ALWAYS AS (nullif(a, 0)) VIRTUAL);
 ALTER TABLE gtest21ax ADD CONSTRAINT cc NOT NULL b;  -- error
 DROP TABLE gtest21ax;
 
 CREATE TABLE gtest21b (a int PRIMARY KEY, 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 (0);  -- violates constraint
+INSERT INTO gtest21b (a) VALUES (1);  -- ok
+INSERT INTO gtest21b (a) VALUES (2), (0);  -- violates constraint
 ALTER TABLE gtest21b ALTER COLUMN b DROP NOT NULL;
---INSERT INTO gtest21b (a) VALUES (0);  -- ok now
+INSERT INTO gtest21b (a) VALUES (0);  -- ok now
 
 -- index constraints
 CREATE TABLE gtest22a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a / 2) VIRTUAL UNIQUE);
-- 
2.34.1

---

From: ego alter <[email-masked]>
Date: Wed, 26 Feb 2025 15:00:48 +0800

Hi, I’ve had a chance to review the patch.  As I am still getting familiar
with executor part, questions and feedbacks could be relatively trivial.

There are two minor issues i want to discuss:
1. The way of caching constraint-checking expr for virtual generated not
null
The existing array for caching constraint-checking expr is
 /* array of constraint-checking expr states */
ExprState **ri_ConstraintExprs;

the proposed changes for virtual generated not null in patch
+  /* array of virtual generated not null constraint-checking expr states */
+  ExprState **ri_VirGeneratedConstraintExprs;
/*
Could you explain the rationale behind adding this new field instead of
reusing ri_ConstraintExprs? The comment suggests it’s used specifically for
not null constraint-checking, but could it be extended to handle other
constraints in the future as well? I assume one benefit is that it
separates the handling of normal constraints from virtual ones, but I'd
like to appreciate more context on the decision.

2. The naming of variable gen_virtualnn.
Gen_virtualnn looks confusing at first glance. Checkconstr seems to be more
straightforward.

    /* And evaluate the check constraints for virtual generated column */
+  for (i = 0; i < bms_num_members(gen_virtual_cols); i++)
+  {
+  ExprState  *gen_virtualnn =
resultRelInfo->ri_VirGeneratedConstraintExprs[i];
+
+  if (gen_virtualnn && !ExecCheck(gen_virtualnn, econtext))
+  return attnums[i];
+  }
+

  /* And evaluate the constraints */
for (i = 0; i < ncheck; i++)
{
ExprState  *checkconstr = resultRelInfo->ri_ConstraintExprs[i];

/*
* NOTE: SQL specifies that a NULL result from a constraint expression
* is not to be treated as a failure.  Therefore, use ExecCheck not
* ExecQual.
*/
if (checkconstr && !ExecCheck(checkconstr, econtext))
return check[i].ccname;
}

/* NULL result means no error */
return NULL;

Best regards,
Xuneng

---

From: jian he <[email-masked]>
Date: Thu, 6 Mar 2025 21:44:11 +0800

On Wed, Feb 26, 2025 at 3:01 PM ego alter <[email-masked]> wrote:
>
> Hi, I’ve had a chance to review the patch.  As I am still getting familiar with executor part, questions and feedbacks could be relatively trivial.
>
> 1. The way of caching constraint-checking expr for virtual generated not null
> Could you explain the rationale behind adding this new field instead of reusing ri_ConstraintExprs? The comment suggests it’s used specifically for not null constraint-checking, but could it be extended to handle other constraints in the future as well? I assume one benefit is that it separates the handling of normal constraints from virtual ones, but I'd like to appreciate more context on the decision.
>

it's a cache mechanism, just like ResultRelInfo.ri_ConstraintExprs
you can see comments in ExecRelCheck
    /*
     * If first time through for this result relation, build expression
     * nodetrees for rel's constraint expressions.  Keep them in the per-query
     * memory context so they'll survive throughout the query.
     */

for example:
create table t(a int, check (a > 3));
insert into t values (4),(3);
we need to call ExecRelCheck for values 4 and 3.
The second time ExecRelCheck called for values 3, if
ri_ConstraintExprs is not null then
we didn't need to call ExecPrepareExpr again for the same check
constraint expression.

+  ExprState **ri_VirGeneratedConstraintExprs;
is specifically only for virtual generated columns not null constraint
evaluation.
Anyway, I renamed it to ri_GeneratedNotNullExprs.

If you want to extend cache for other constraints in the future,
you can add it to struct ResultRelInfo.
Currently struct ResultRelInfo, we have ri_GeneratedExprsU,
ri_GeneratedExprsI for generated expressions;
ri_ConstraintExprs for check constraints.

> Gen_virtualnn looks confusing at first glance. Checkconstr seems to be more straightforward.
>
thanks. I changed it to exprstate.

new patch attached.
0001 not null for virtual generated columns, some refactoring happened
within ExecConstraints
to avoid duplicated error handling code.

0002 and 0003 is the domain for virtual generated columns. domain can
have not-null constraints,
this is built on top of 0001, so I guess posting here for review should be fine?

currently it works as intended. but add a virtual generated column
with domain_with_constraints
requires table rewrite. but some cases table scan should just be enough.
some case we do need table rewrite.
for example:
create domain d1 as int check(value > random(min=>11::int, max=>12));
create domain d2 as int check(value > 12);
create table t(a int);
insert into t select g from generate_series(1, 10) g;
alter table t add column b d1 generated always as (a+11) virtual; --we
do need table rewrite in phase 3.
alter table t add column c d2 generated always as (a + 12) virtual;
--we can only do table scan in phase 3.
From 5e1c83c18cd954abcbae4bd52ef9c77de1273f9c Mon Sep 17 00:00:00 2001
From: jian he <[email-masked]>
Date: Thu, 6 Mar 2025 12:35:19 +0800
Subject: [PATCH v2 2/3] rename ExecComputeGenerated to ExecComputeGenerated

to support virtual generated column over domain type, we actually
need compute the virtual generated expression.
we did it at ExecComputeGenerated for stored, we can use it for virtual.
so do the rename.

discussion: https://postgr.es/m/[email-masked]
---
 src/backend/commands/copyfrom.c        |  4 ++--
 src/backend/executor/execReplication.c |  8 ++++----
 src/backend/executor/nodeModifyTable.c | 20 ++++++++++----------
 src/include/executor/nodeModifyTable.h |  5 ++---
 4 files changed, 18 insertions(+), 19 deletions(-)

diff --git a/src/backend/commands/copyfrom.c b/src/backend/commands/copyfrom.c
index bcf66f0adf8..7f1078dfe39 100644
--- a/src/backend/commands/copyfrom.c
+++ b/src/backend/commands/copyfrom.c
@@ -1346,8 +1346,8 @@ CopyFrom(CopyFromState cstate)
 				/* Compute stored generated columns */
 				if (resultRelInfo->ri_RelationDesc->rd_att->constr &&
 					resultRelInfo->ri_RelationDesc->rd_att->constr->has_generated_stored)
-					ExecComputeStoredGenerated(resultRelInfo, estate, myslot,
-											   CMD_INSERT);
+					ExecComputeGenerated(resultRelInfo, estate, myslot,
+										 CMD_INSERT);
 
 				/*
 				 * If the target is a plain table, check the constraints of
diff --git a/src/backend/executor/execReplication.c b/src/backend/executor/execReplication.c
index 5cef54f00ed..fdc379e7a44 100644
--- a/src/backend/executor/execReplication.c
+++ b/src/backend/executor/execReplication.c
@@ -549,8 +549,8 @@ ExecSimpleRelationInsert(ResultRelInfo *resultRelInfo,
 		/* Compute stored generated columns */
 		if (rel->rd_att->constr &&
 			rel->rd_att->constr->has_generated_stored)
-			ExecComputeStoredGenerated(resultRelInfo, estate, slot,
-									   CMD_INSERT);
+			ExecComputeGenerated(resultRelInfo, estate, slot,
+								 CMD_INSERT);
 
 		/* Check the constraints of the tuple */
 		if (rel->rd_att->constr)
@@ -646,8 +646,8 @@ ExecSimpleRelationUpdate(ResultRelInfo *resultRelInfo,
 		/* Compute stored generated columns */
 		if (rel->rd_att->constr &&
 			rel->rd_att->constr->has_generated_stored)
-			ExecComputeStoredGenerated(resultRelInfo, estate, slot,
-									   CMD_UPDATE);
+			ExecComputeGenerated(resultRelInfo, estate, slot,
+								 CMD_UPDATE);
 
 		/* Check the constraints of the tuple */
 		if (rel->rd_att->constr)
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index b0fe50075ad..53755ccdbad 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -517,12 +517,12 @@ ExecInitGenerated(ResultRelInfo *resultRelInfo,
 }
 
 /*
- * Compute stored generated columns for a tuple
+ * Compute generated columns for a tuple.
+ * we might support virtual generated column in future, currently not.
  */
 void
-ExecComputeStoredGenerated(ResultRelInfo *resultRelInfo,
-						   EState *estate, TupleTableSlot *slot,
-						   CmdType cmdtype)
+ExecComputeGenerated(ResultRelInfo *resultRelInfo, EState *estate,
+					 TupleTableSlot *slot, CmdType cmdtype)
 {
 	Relation	rel = resultRelInfo->ri_RelationDesc;
 	TupleDesc	tupdesc = RelationGetDescr(rel);
@@ -911,8 +911,8 @@ ExecInsert(ModifyTableContext *context,
 		 */
 		if (resultRelationDesc->rd_att->constr &&
 			resultRelationDesc->rd_att->constr->has_generated_stored)
-			ExecComputeStoredGenerated(resultRelInfo, estate, slot,
-									   CMD_INSERT);
+			ExecComputeGenerated(resultRelInfo, estate, slot,
+								 CMD_INSERT);
 
 		/*
 		 * If the FDW supports batching, and batching is requested, accumulate
@@ -1038,8 +1038,8 @@ ExecInsert(ModifyTableContext *context,
 		 */
 		if (resultRelationDesc->rd_att->constr &&
 			resultRelationDesc->rd_att->constr->has_generated_stored)
-			ExecComputeStoredGenerated(resultRelInfo, estate, slot,
-									   CMD_INSERT);
+			ExecComputeGenerated(resultRelInfo, estate, slot,
+								 CMD_INSERT);
 
 		/*
 		 * Check any RLS WITH CHECK policies.
@@ -2126,8 +2126,8 @@ ExecUpdatePrepareSlot(ResultRelInfo *resultRelInfo,
 	 */
 	if (resultRelationDesc->rd_att->constr &&
 		resultRelationDesc->rd_att->constr->has_generated_stored)
-		ExecComputeStoredGenerated(resultRelInfo, estate, slot,
-								   CMD_UPDATE);
+		ExecComputeGenerated(resultRelInfo, estate, slot,
+							 CMD_UPDATE);
 }
 
 /*
diff --git a/src/include/executor/nodeModifyTable.h b/src/include/executor/nodeModifyTable.h
index bf3b592e28f..de374c46d3c 100644
--- a/src/include/executor/nodeModifyTable.h
+++ b/src/include/executor/nodeModifyTable.h
@@ -19,9 +19,8 @@ extern void ExecInitGenerated(ResultRelInfo *resultRelInfo,
 							  EState *estate,
 							  CmdType cmdtype);
 
-extern void ExecComputeStoredGenerated(ResultRelInfo *resultRelInfo,
-									   EState *estate, TupleTableSlot *slot,
-									   CmdType cmdtype);
+extern void ExecComputeGenerated(ResultRelInfo *resultRelInfo, EState *estate,
+								 TupleTableSlot *slot,CmdType cmdtype);
 
 extern ModifyTableState *ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags);
 extern void ExecEndModifyTable(ModifyTableState *node);
-- 
2.34.1

From a4a5343dfbac4130bbf6e3fb63992b8ac03591cd Mon Sep 17 00:00:00 2001
From: jian he <[email-masked]>
Date: Thu, 6 Mar 2025 21:17:52 +0800
Subject: [PATCH v2 3/3] domain over virtual generated column.

domain don't have constraints works just be fine.
domain constraint check is happend within ExecComputeGenerated.
we compute the virtual generated column in ExecComputeGenerated and discarded the value.

domain_with_constaint can be element type of range, multirange, array, composite.
to be bullet proof, if this column type is not type of TYPTYPE_BASE or it's a array type,
then we do actlly compute the generated expression.
This can have negative performance for INSERT, if vertain virtual column data types requires computation.

the followig two query shows in pg_catalog, most of the system type is TYPTYPE_BASE.
so i guess this compromise is fine.

select count(*),typtype from pg_type where typnamespace = 11 group by 2;
select typname, typrelid, pc.reltype, pc.oid, pt.oid
from pg_type pt join pg_class pc on pc.oid = pt.typrelid
where pt.typnamespace = 11 and pt.typtype = 'c' and pc.reltype = 0;

ALTER DOMAIN ADD CONSTRAINT variant is supported.
DOMAIN with default values are supported. but virtual generated column already have
generated expression, so domain default expression doesn't matter.

ALTER TABLE ADD VIRTUAL GENERATED COLUMN.
need table rewrite to vertify new column value satisfied the generation expression.
this can be reduced to table scan in some cases.

TODO: deal with domain with volatile check constraints.
TODO: only table rewrites in somes cases.

discussion: https://postgr.es/m/[email-masked]
---
 src/backend/catalog/heap.c                    |  11 -
 src/backend/commands/copyfrom.c               |   5 +-
 src/backend/commands/tablecmds.c              |   6 +-
 src/backend/commands/typecmds.c               | 198 +++++++++++++++---
 src/backend/executor/nodeModifyTable.c        |  84 ++++++--
 .../regress/expected/generated_virtual.out    |  67 +++++-
 src/test/regress/sql/generated_virtual.sql    |  49 ++++-
 7 files changed, 345 insertions(+), 75 deletions(-)

diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index b807ab66668..39e2ac38141 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -583,17 +583,6 @@ CheckAttributeType(const char *attname,
 	}
 	else if (att_typtype == TYPTYPE_DOMAIN)
 	{
-		/*
-		 * Prevent virtual generated columns from having a domain type.  We
-		 * would have to enforce domain constraints when columns underlying
-		 * the generated column change.  This could possibly be implemented,
-		 * but it's not.
-		 */
-		if (flags & CHKATYPE_IS_VIRTUAL)
-			ereport(ERROR,
-					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-					errmsg("virtual generated column \"%s\" cannot have a domain type", attname));
-
 		/*
 		 * If it's a domain, recurse to check its base type.
 		 */
diff --git a/src/backend/commands/copyfrom.c b/src/backend/commands/copyfrom.c
index 7f1078dfe39..b8c984e1ec2 100644
--- a/src/backend/commands/copyfrom.c
+++ b/src/backend/commands/copyfrom.c
@@ -1343,9 +1343,10 @@ CopyFrom(CopyFromState cstate)
 			}
 			else
 			{
-				/* Compute stored generated columns */
+				/* Compute generated columns */
 				if (resultRelInfo->ri_RelationDesc->rd_att->constr &&
-					resultRelInfo->ri_RelationDesc->rd_att->constr->has_generated_stored)
+					(resultRelInfo->ri_RelationDesc->rd_att->constr->has_generated_stored ||
+					 resultRelInfo->ri_RelationDesc->rd_att->constr->has_generated_virtual))
 					ExecComputeGenerated(resultRelInfo, estate, myslot,
 										 CMD_INSERT);
 
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c89f1cb9936..d314cfb8f7d 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -7453,10 +7453,14 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 				/*
 				 * Failed to use missing mode.  We have to do a table rewrite
 				 * to install the value --- unless it's a virtual generated
-				 * column.
+				 * column. However if virtual generated column is domain with
+				 * constraints then we have to table rewrite to evaulate the
+				 * generation expression. (TODO, in some case, we don't need rewrite)
 				 */
 				if (colDef->generated != ATTRIBUTE_GENERATED_VIRTUAL)
 					tab->rewrite |= AT_REWRITE_DEFAULT_VAL;
+				else if (has_domain_constraints)
+					tab->rewrite |= AT_REWRITE_DEFAULT_VAL;
 			}
 		}
 
diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c
index 3cb3ca1cca1..a247aa937f9 100644
--- a/src/backend/commands/typecmds.c
+++ b/src/backend/commands/typecmds.c
@@ -65,6 +65,7 @@
 #include "parser/parse_expr.h"
 #include "parser/parse_func.h"
 #include "parser/parse_type.h"
+#include "rewrite/rewriteHandler.h"
 #include "utils/builtins.h"
 #include "utils/fmgroids.h"
 #include "utils/inval.h"
@@ -144,6 +145,7 @@ static void AlterTypeRecurse(Oid typeOid, bool isImplicitArray,
 							 AlterTypeRecurseParams *atparams);

+static bool ValidateDomainNeedGeneratedExprs(RelToCheck *rtc);
 /*
  * DefineType
  *		Registers a new base type.
@@ -3112,6 +3114,28 @@ AlterDomainValidateConstraint(List *names, const char *constrName)
 	return address;
 }
 
+static bool
+ValidateDomainNeedGeneratedExprs(RelToCheck *rtc)
+{
+	Relation	testrel = rtc->rel;
+	TupleDesc	tupdesc = RelationGetDescr(testrel);
+
+	if (tupdesc->constr && tupdesc->constr->has_generated_virtual)
+	{
+		int			i;
+		int			attnum;
+		Form_pg_attribute attr;
+		for (i = 0; i < rtc->natts; i++)
+		{
+			attnum = rtc->atts[i];
+			attr = TupleDescAttr(tupdesc, attnum - 1);
+			if (attr->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+				return true;
+		}
+	}
+	return false;
+}
+
 /*
  * Verify that all columns currently using the domain are not null.
  */
@@ -3121,6 +3145,15 @@ validateDomainNotNullConstraint(Oid domainoid)
 	List	   *rels;
 	ListCell   *rt;
 
+	/*
+	 * for domain not null constraint over virtual generated column
+	 * it maybe need it in below.
+	*/
+	EState	   *estate;
+	ExprContext *econtext;
+	estate = CreateExecutorState();
+	econtext = GetPerTupleExprContext(estate);
+
 	/* Fetch relation list with attributes based on this domain */
 	/* ShareLock is sufficient to prevent concurrent data changes */
 
@@ -3134,6 +3167,39 @@ validateDomainNotNullConstraint(Oid domainoid)
 		TupleTableSlot *slot;
 		TableScanDesc scan;
 		Snapshot	snapshot;
+		Form_pg_attribute attr;
+		ExprState **ri_GeneratedExprs;
+		bool		need_GeneratedExprs = false;
+		int			attnum;
+
+		/* cacahe generated columns handling */
+		need_GeneratedExprs = ValidateDomainNeedGeneratedExprs(rtc);
+		if (need_GeneratedExprs)
+		{
+			ri_GeneratedExprs = (ExprState **) palloc0(tupdesc->natts * sizeof(ExprState *));
+
+			for (int i = 0; i < rtc->natts; i++)
+			{
+				attnum = rtc->atts[i];
+				attr = TupleDescAttr(tupdesc, attnum - 1);
+
+				if (attr->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+				{
+					NullTest   *nnulltest;
+					Expr	   *expr;
+
+					nnulltest = makeNode(NullTest);
+					nnulltest->arg = (Expr *) build_column_default(testrel, attnum);
+					nnulltest->nulltesttype = IS_NOT_NULL;
+					nnulltest->argisrow = false;
+					nnulltest->location = -1;
+
+					expr = (Expr *) nnulltest;
+					ri_GeneratedExprs[attnum - 1] = ExecPrepareExpr(expr, estate);
+				}
+			}
+		}
+
 
 		/* Scan all tuples in this relation */
 		snapshot = RegisterSnapshot(GetLatestSnapshot());
@@ -3146,10 +3212,10 @@ validateDomainNotNullConstraint(Oid domainoid)
 			/* Test attributes that are of the domain */
 			for (i = 0; i < rtc->natts; i++)
 			{
-				int			attnum = rtc->atts[i];
-				Form_pg_attribute attr = TupleDescAttr(tupdesc, attnum - 1);
+				attnum = rtc->atts[i];
+				attr = TupleDescAttr(tupdesc, attnum - 1);
 
-				if (slot_attisnull(slot, attnum))
+				if (slot_attisnull(slot, attnum) && attr->attgenerated != ATTRIBUTE_GENERATED_VIRTUAL)
 				{
 					/*
 					 * In principle the auxiliary information for this error
@@ -3165,7 +3231,22 @@ validateDomainNotNullConstraint(Oid domainoid)
 									RelationGetRelationName(testrel)),
 							 errtablecol(testrel, attnum)));
 				}
+
+				if (attr->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+				{
+					econtext->ecxt_scantuple = slot;
+					Assert(ri_GeneratedExprs[attnum - 1] != NULL);
+					if (!ExecCheck(ri_GeneratedExprs[attnum - 1] , econtext))
+						ereport(ERROR,
+								errcode(ERRCODE_NOT_NULL_VIOLATION),
+								errmsg("column \"%s\" of table \"%s\" contains null values",
+										NameStr(attr->attname),
+										RelationGetRelationName(testrel)),
+								errtablecol(testrel, attnum));
+				}
 			}
+
+			ResetExprContext(econtext);
 		}
 		ExecDropSingleTupleTableSlot(slot);
 		table_endscan(scan);
@@ -3174,6 +3255,8 @@ validateDomainNotNullConstraint(Oid domainoid)
 		/* Close each rel after processing, but keep lock */
 		table_close(testrel, NoLock);
 	}
+
+	FreeExecutorState(estate);
 }
 
 /*
@@ -3210,6 +3293,32 @@ validateDomainCheckConstraint(Oid domainoid, const char *ccbin)
 		TupleTableSlot *slot;
 		TableScanDesc scan;
 		Snapshot	snapshot;
+		ExprState **ri_GeneratedExprs;
+		bool		need_GeneratedExprs = false;
+		Form_pg_attribute attr;
+		int			attnum;
+
+		/* cacahe generated columns handling */
+		need_GeneratedExprs = ValidateDomainNeedGeneratedExprs(rtc);
+
+		if (need_GeneratedExprs)
+		{
+			ri_GeneratedExprs = (ExprState **) palloc0(tupdesc->natts * sizeof(ExprState *));
+			for (int i = 0; i < rtc->natts; i++)
+			{
+				Expr	   *gen_defexpr;
+
+				attnum = rtc->atts[i];
+				attr = TupleDescAttr(tupdesc, attnum - 1);
+
+				/* Fetch the GENERATED AS expression tree */
+				gen_defexpr = (Expr *) build_column_default(testrel, attnum);
+				if (gen_defexpr == NULL)
+					elog(ERROR, "no generation expression found for column number %d of table \"%s\"",
+						attnum, RelationGetRelationName(testrel));
+				ri_GeneratedExprs[attnum - 1] = ExecPrepareExpr(gen_defexpr, estate);
+			}
+		}
 
 		/* Scan all tuples in this relation */
 		snapshot = RegisterSnapshot(GetLatestSnapshot());
@@ -3222,37 +3331,74 @@ validateDomainCheckConstraint(Oid domainoid, const char *ccbin)
 			/* Test attributes that are of the domain */
 			for (i = 0; i < rtc->natts; i++)
 			{
-				int			attnum = rtc->atts[i];
 				Datum		d;
 				bool		isNull;
 				Datum		conResult;
-				Form_pg_attribute attr = TupleDescAttr(tupdesc, attnum - 1);
 
-				d = slot_getattr(slot, attnum, &isNull);
+				attnum = rtc->atts[i];
+				attr = TupleDescAttr(tupdesc, attnum - 1);
 
-				econtext->domainValue_datum = d;
-				econtext->domainValue_isNull = isNull;
-
-				conResult = ExecEvalExprSwitchContext(exprstate,
-													  econtext,
-													  &isNull);
-
-				if (!isNull && !DatumGetBool(conResult))
+				/*
+				 * we first actually compute the virtual generated column value
+				 * then do the CoerceToDomainValue check.
+				 */
+				if (attr->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
 				{
+					Datum		ret;
+					bool		isnull;
+
+					econtext = GetPerTupleExprContext(estate);
+					econtext->ecxt_scantuple = slot;
+
+					ret = ExecEvalExprSwitchContext(ri_GeneratedExprs[attnum - 1],
+													econtext,
+													&isnull);
 					/*
-					 * In principle the auxiliary information for this error
-					 * should be errdomainconstraint(), but errtablecol()
-					 * seems considerably more useful in practice.  Since this
-					 * code only executes in an ALTER DOMAIN command, the
-					 * client should already know which domain is in question,
-					 * and which constraint too.
+					 * we probally don't need datumCopy to copy ret, since here
+					 * we just check the value.
 					 */
-					ereport(ERROR,
-							(errcode(ERRCODE_CHECK_VIOLATION),
-							 errmsg("column \"%s\" of table \"%s\" contains values that violate the new constraint",
-									NameStr(attr->attname),
-									RelationGetRelationName(testrel)),
-							 errtablecol(testrel, attnum)));
+					econtext->domainValue_datum = ret;
+					econtext->domainValue_isNull = isnull;
+					econtext->ecxt_scantuple = NULL;
+
+					conResult = ExecEvalExprSwitchContext(exprstate,
+														  econtext,
+														  &isNull);
+					if (!isNull && !DatumGetBool(conResult))
+						ereport(ERROR,
+								errcode(ERRCODE_CHECK_VIOLATION),
+								errmsg("column \"%s\" of table \"%s\" contains values that violate the new constraint",
+										NameStr(attr->attname), RelationGetRelationName(testrel)),
+								errtablecol(testrel, attnum));
+				}
+				else
+				{
+					d = slot_getattr(slot, attnum, &isNull);
+
+					econtext->domainValue_datum = d;
+					econtext->domainValue_isNull = isNull;
+
+					conResult = ExecEvalExprSwitchContext(exprstate,
+														  econtext,
+														  &isNull);
+
+					if (!isNull && !DatumGetBool(conResult))
+					{
+						/*
+						* In principle the auxiliary information for this error
+						* should be errdomainconstraint(), but errtablecol()
+						* seems considerably more useful in practice.  Since this
+						* code only executes in an ALTER DOMAIN command, the
+						* client should already know which domain is in question,
+						* and which constraint too.
+						*/
+						ereport(ERROR,
+								(errcode(ERRCODE_CHECK_VIOLATION),
+								errmsg("column \"%s\" of table \"%s\" contains values that violate the new constraint",
+										NameStr(attr->attname),
+										RelationGetRelationName(testrel)),
+								errtablecol(testrel, attnum)));
+					}
 				}
 			}
 
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 53755ccdbad..e83e57bea0a 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -67,6 +67,7 @@
 #include "storage/lmgr.h"
 #include "utils/builtins.h"
 #include "utils/datum.h"
+#include "utils/lsyscache.h"
 #include "utils/rel.h"
 #include "utils/snapmgr.h"
 
@@ -395,7 +396,7 @@ ExecCheckTIDVisible(EState *estate,
  *
  * This fills the resultRelInfo's ri_GeneratedExprsI/ri_NumGeneratedNeededI or
  * ri_GeneratedExprsU/ri_NumGeneratedNeededU fields, depending on cmdtype.
- * This is used only for stored generated columns.
+ * This is used for stored and virtual generated columns.
  *
  * If cmdType == CMD_UPDATE, the ri_extraUpdatedCols field is filled too.
  * This is used by both stored and virtual generated columns.
@@ -477,6 +478,33 @@ ExecInitGenerated(ResultRelInfo *resultRelInfo,
 				ri_GeneratedExprs[i] = ExecPrepareExpr(expr, estate);
 				ri_NumGeneratedNeeded++;
 			}
+			else
+			{
+				/*
+				* Virtual generated columns only need to be computed when the
+				* type is domain_with_constraint.  However,
+				* domain_with_constraint can be the element type of a range, the
+				* element type of a composite, or the element type of an array.
+				*
+				* To determine whether the true base element type is
+				* domain_with_constraint, multiple lookup_type_cache calls seems
+				* doable. However, this would add a lot of code.
+				* So, simplified it: only if the type is TYPTYPE_BASE and it's
+				* not an array type, computation is not needed.
+				*/
+				Oid		typelem	= InvalidOid;
+				Oid		atttypid = TupleDescAttr(tupdesc, i)->atttypid;
+				char	att_typtype = get_typtype(atttypid);
+
+				if (att_typtype == TYPTYPE_BASE)
+					typelem = get_element_type(atttypid);
+
+				if (att_typtype != TYPTYPE_BASE || OidIsValid(typelem))
+				{
+					ri_GeneratedExprs[i] = ExecPrepareExpr(expr, estate);
+					ri_NumGeneratedNeeded++;
+				}
+			}
 
 			/* If UPDATE, mark column in resultRelInfo->ri_extraUpdatedCols */
 			if (cmdtype == CMD_UPDATE)
@@ -518,7 +546,9 @@ ExecInitGenerated(ResultRelInfo *resultRelInfo,
 
 /*
  * Compute generated columns for a tuple.
- * we might support virtual generated column in future, currently not.
+ * Generally, we don't need compute virtual generated column, except for column
+ * type is domain with constraint. Since virtual generated column don't have
+ * storage, we don't need stored the computed value.
  */
 void
 ExecComputeGenerated(ResultRelInfo *resultRelInfo, EState *estate,
@@ -532,9 +562,11 @@ ExecComputeGenerated(ResultRelInfo *resultRelInfo, EState *estate,
 	MemoryContext oldContext;
 	Datum	   *values;
 	bool	   *nulls;
+	bool	   need_compute = false;
 
 	/* We should not be called unless this is true */
-	Assert(tupdesc->constr && tupdesc->constr->has_generated_stored);
+	Assert(tupdesc->constr);
+	Assert(tupdesc->constr->has_generated_stored || tupdesc->constr->has_generated_virtual);
 
 	/*
 	 * Initialize the expressions if we didn't already, and check whether we
@@ -553,7 +585,8 @@ ExecComputeGenerated(ResultRelInfo *resultRelInfo, EState *estate,
 		if (resultRelInfo->ri_GeneratedExprsI == NULL)
 			ExecInitGenerated(resultRelInfo, estate, cmdtype);
 		/* Early exit is impossible given the prior Assert */
-		Assert(resultRelInfo->ri_NumGeneratedNeededI > 0);
+		if (resultRelInfo->ri_GeneratedExprsI != NULL)
+			Assert(resultRelInfo->ri_NumGeneratedNeededI > 0);
 		ri_GeneratedExprs = resultRelInfo->ri_GeneratedExprsI;
 	}
 
@@ -565,30 +598,40 @@ ExecComputeGenerated(ResultRelInfo *resultRelInfo, EState *estate,
 	slot_getallattrs(slot);
 	memcpy(nulls, slot->tts_isnull, sizeof(*nulls) * natts);
 
+	need_compute = (resultRelInfo->ri_NumGeneratedNeededI > 0 ||
+					resultRelInfo->ri_NumGeneratedNeededU > 0);
+
 	for (int i = 0; i < natts; i++)
 	{
 		CompactAttribute *attr = TupleDescCompactAttr(tupdesc, i);
 
-		if (ri_GeneratedExprs[i])
+		if (need_compute && ri_GeneratedExprs[i] )
 		{
 			Datum		val;
 			bool		isnull;
 
-			Assert(TupleDescAttr(tupdesc, i)->attgenerated == ATTRIBUTE_GENERATED_STORED);
-
 			econtext->ecxt_scantuple = slot;
 
 			val = ExecEvalExpr(ri_GeneratedExprs[i], econtext, &isnull);
 
-			/*
-			 * We must make a copy of val as we have no guarantees about where
-			 * memory for a pass-by-reference Datum is located.
-			 */
-			if (!isnull)
-				val = datumCopy(val, attr->attbyval, attr->attlen);
+			if (TupleDescAttr(tupdesc, i)->attgenerated == ATTRIBUTE_GENERATED_STORED)
+			{
+				/*
+				 * We must make a copy of val as we have no guarantees about where
+				 * memory for a pass-by-reference Datum is located.
+				*/
+				if (!isnull)
+					val = datumCopy(val, attr->attbyval, attr->attlen);
 
-			values[i] = val;
-			nulls[i] = isnull;
+				values[i] = val;
+				nulls[i] = isnull;
+			}
+			else
+			{
+				Assert(TupleDescAttr(tupdesc, i)->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL);
+				values[i] = (Datum) 0;
+				nulls[i] = true;
+			}
 		}
 		else
 		{
@@ -910,7 +953,8 @@ ExecInsert(ModifyTableContext *context,
 		 * Compute stored generated columns
 		 */
 		if (resultRelationDesc->rd_att->constr &&
-			resultRelationDesc->rd_att->constr->has_generated_stored)
+			(resultRelationDesc->rd_att->constr->has_generated_stored ||
+			 resultRelationDesc->rd_att->constr->has_generated_virtual))
 			ExecComputeGenerated(resultRelInfo, estate, slot,
 								 CMD_INSERT);
 
@@ -1037,7 +1081,8 @@ ExecInsert(ModifyTableContext *context,
 		 * Compute stored generated columns
 		 */
 		if (resultRelationDesc->rd_att->constr &&
-			resultRelationDesc->rd_att->constr->has_generated_stored)
+			(resultRelationDesc->rd_att->constr->has_generated_stored ||
+			 resultRelationDesc->rd_att->constr->has_generated_virtual))
 			ExecComputeGenerated(resultRelInfo, estate, slot,
 								 CMD_INSERT);
 
@@ -2122,10 +2167,11 @@ ExecUpdatePrepareSlot(ResultRelInfo *resultRelInfo,
 	slot->tts_tableOid = RelationGetRelid(resultRelationDesc);
 
 	/*
-	 * Compute stored generated columns
+	 * Compute generated columns
 	 */
 	if (resultRelationDesc->rd_att->constr &&
-		resultRelationDesc->rd_att->constr->has_generated_stored)
+		(resultRelationDesc->rd_att->constr->has_generated_stored ||
+		 resultRelationDesc->rd_att->constr->has_generated_virtual))
 		ExecComputeGenerated(resultRelInfo, estate, slot,
 							 CMD_UPDATE);
 }
diff --git a/src/test/regress/expected/generated_virtual.out b/src/test/regress/expected/generated_virtual.out
index 3bd5130587c..c1528477cea 100644
--- a/src/test/regress/expected/generated_virtual.out
+++ b/src/test/regress/expected/generated_virtual.out
@@ -763,16 +763,65 @@ ERROR:  relation "gtest23p" does not exist
 --INSERT INTO gtest23q VALUES (1, 2);  -- ok
 --INSERT INTO gtest23q VALUES (2, 5);  -- error
 -- domains
-CREATE DOMAIN gtestdomain1 AS int CHECK (VALUE < 10);
-CREATE TABLE gtest24 (a int PRIMARY KEY, b gtestdomain1 GENERATED ALWAYS AS (a * 2) VIRTUAL);
-ERROR:  virtual generated column "b" cannot have a domain type
---INSERT INTO gtest24 (a) VALUES (4);  -- ok
---INSERT INTO gtest24 (a) VALUES (6);  -- error
+CREATE DOMAIN gtestdomain1 AS int CHECK (VALUE < 10) DEFAULT 12;
+CREATE TABLE gtest24 (a int UNIQUE, b gtestdomain1 GENERATED ALWAYS AS (a * 2) VIRTUAL);
+INSERT INTO gtest24 (a, b) VALUES (4, default);  -- ok
+INSERT INTO gtest24 (a) VALUES (3), (NULL);  -- ok
+INSERT INTO gtest24 (a) VALUES (6);  -- error
+ERROR:  value for domain gtestdomain1 violates check constraint "gtestdomain1_check"
+UPDATE gtest24 SET a = 6;            -- error
+ERROR:  value for domain gtestdomain1 violates check constraint "gtestdomain1_check"
+COPY gtest24 FROM stdin;  --error
+ERROR:  value for domain gtestdomain1 violates check constraint "gtestdomain1_check"
+CONTEXT:  COPY gtest24, line 1: "6"
+SELECT * FROM gtest24;
+ a | b 
+---+---
+ 4 | 8
+ 3 | 6
+   |  
+(3 rows)
+
+--ALTER TABLE ADD COLUM variant.
+ALTER TABLE gtest24 ADD COLUMN c gtestdomain1 GENERATED ALWAYS AS (a * 2) virtual not null; --error
+ERROR:  column "c" of relation "gtest24" contains null values
+ALTER TABLE gtest24 ADD COLUMN c gtestdomain1 GENERATED ALWAYS AS (a * 3) virtual check (c < 10); --error
+ERROR:  value for domain gtestdomain1 violates check constraint "gtestdomain1_check"
+ALTER TABLE gtest24 ADD COLUMN c gtestdomain1 GENERATED ALWAYS AS (a * 4) virtual; --table rewrite then error.
+ERROR:  value for domain gtestdomain1 violates check constraint "gtestdomain1_check"
+ALTER TABLE gtest24 ADD COLUMN c gtestdomain1 GENERATED ALWAYS AS (a * 2) virtual; --ok
+--alter domain add constraint variant.
+ALTER DOMAIN gtestdomain1 ADD CHECK (value IS NULL);  --error
+ERROR:  column "b" of table "gtest24" contains values that violate the new constraint
+ALTER DOMAIN gtestdomain1 ADD CHECK (value IS NOT NULL); --error
+ERROR:  column "b" of table "gtest24" contains values that violate the new constraint
+ALTER DOMAIN gtestdomain1 ADD NOT NULL; --error
+ERROR:  column "b" of table "gtest24" contains null values
+DELETE FROM gtest24 WHERE a is NULL;
+ALTER DOMAIN gtestdomain1 ADD NOT NULL; --ok
+ALTER DOMAIN gtestdomain1 ADD CHECK (value IS NOT NULL);  --ok
+ALTER DOMAIN gtestdomain1 ADD CHECK (VALUE < 7); --error
+ERROR:  column "b" of table "gtest24" contains values that violate the new constraint
+ALTER DOMAIN gtestdomain1 ADD CHECK (VALUE < 9); --ok
+INSERT INTO gtest24 (a) VALUES (NULL);  -- error
+ERROR:  domain gtestdomain1 does not allow null values
 CREATE TYPE gtestdomain1range AS range (subtype = gtestdomain1);
-CREATE TABLE gtest24r (a int PRIMARY KEY, b gtestdomain1range GENERATED ALWAYS AS (gtestdomain1range(a, a + 5)) VIRTUAL);
-ERROR:  virtual generated column "b" cannot have a domain type
---INSERT INTO gtest24r (a) VALUES (4);  -- ok
---INSERT INTO gtest24r (a) VALUES (6);  -- error
+CREATE TABLE gtest24r (a int PRIMARY KEY, b gtestdomain1range GENERATED ALWAYS AS (gtestdomain1range(a, a + 4)) VIRTUAL);
+INSERT INTO gtest24r (a) VALUES (4);  -- ok
+INSERT INTO gtest24r (a) VALUES (6);  -- error
+ERROR:  value for domain gtestdomain1 violates check constraint "gtestdomain1_check"
+INSERT INTO gtest24r (a) VALUES (5);  -- error
+ERROR:  value for domain gtestdomain1 violates check constraint "gtestdomain1_check2"
+CREATE TABLE gtest24v (
+  a jsonb,
+  b gtestdomain1[] GENERATED ALWAYS AS (JSON_QUERY(a, '$.a' returning gtestdomain1[] error on error)) VIRTUAL,
+  c gtestdomain1[] GENERATED ALWAYS AS (JSON_QUERY(a, '$.b' returning gtestdomain1[])) VIRTUAL not null);
+insert into gtest24v select jsonb '{"a":[6,10], "b":[6,2]}'; --error
+ERROR:  value for domain gtestdomain1 violates check constraint "gtestdomain1_check"
+insert into gtest24v select jsonb '{"a":[6,-1], "b":[6,10]}'; --error
+ERROR:  null value in column "c" of relation "gtest24v" violates not-null constraint
+DETAIL:  Failing row contains ({"a": [6, -1], "b": [6, 10]}, virtual, virtual).
+insert into gtest24v select jsonb '{"a":[6,-1], "b":[6,2]}'; --ok
 -- typed tables (currently not supported)
 CREATE TYPE gtest_type AS (f1 integer, f2 text, f3 bigint);
 CREATE TABLE gtest28 OF gtest_type (f1 WITH OPTIONS GENERATED ALWAYS AS (f2 *2) VIRTUAL);
diff --git a/src/test/regress/sql/generated_virtual.sql b/src/test/regress/sql/generated_virtual.sql
index f436e8f2313..0b3a981f225 100644
--- a/src/test/regress/sql/generated_virtual.sql
+++ b/src/test/regress/sql/generated_virtual.sql
@@ -429,14 +429,49 @@ CREATE TABLE gtest23q (a int PRIMARY KEY, b int REFERENCES gtest23p (y));
 --INSERT INTO gtest23q VALUES (2, 5);  -- error
 
 -- domains
-CREATE DOMAIN gtestdomain1 AS int CHECK (VALUE < 10);
-CREATE TABLE gtest24 (a int PRIMARY KEY, b gtestdomain1 GENERATED ALWAYS AS (a * 2) VIRTUAL);
---INSERT INTO gtest24 (a) VALUES (4);  -- ok
---INSERT INTO gtest24 (a) VALUES (6);  -- error
+CREATE DOMAIN gtestdomain1 AS int CHECK (VALUE < 10) DEFAULT 12;
+CREATE TABLE gtest24 (a int UNIQUE, b gtestdomain1 GENERATED ALWAYS AS (a * 2) VIRTUAL);
+INSERT INTO gtest24 (a, b) VALUES (4, default);  -- ok
+INSERT INTO gtest24 (a) VALUES (3), (NULL);  -- ok
+INSERT INTO gtest24 (a) VALUES (6);  -- error
+UPDATE gtest24 SET a = 6;            -- error
+COPY gtest24 FROM stdin;  --error
+6
+\.
+
+SELECT * FROM gtest24;
+
+--ALTER TABLE ADD COLUM variant.
+ALTER TABLE gtest24 ADD COLUMN c gtestdomain1 GENERATED ALWAYS AS (a * 2) virtual not null; --error
+ALTER TABLE gtest24 ADD COLUMN c gtestdomain1 GENERATED ALWAYS AS (a * 3) virtual check (c < 10); --error
+ALTER TABLE gtest24 ADD COLUMN c gtestdomain1 GENERATED ALWAYS AS (a * 4) virtual; --table rewrite then error.
+ALTER TABLE gtest24 ADD COLUMN c gtestdomain1 GENERATED ALWAYS AS (a * 2) virtual; --ok
+
+--alter domain add constraint variant.
+ALTER DOMAIN gtestdomain1 ADD CHECK (value IS NULL);  --error
+ALTER DOMAIN gtestdomain1 ADD CHECK (value IS NOT NULL); --error
+ALTER DOMAIN gtestdomain1 ADD NOT NULL; --error
+DELETE FROM gtest24 WHERE a is NULL;
+ALTER DOMAIN gtestdomain1 ADD NOT NULL; --ok
+ALTER DOMAIN gtestdomain1 ADD CHECK (value IS NOT NULL);  --ok
+ALTER DOMAIN gtestdomain1 ADD CHECK (VALUE < 7); --error
+ALTER DOMAIN gtestdomain1 ADD CHECK (VALUE < 9); --ok
+
+INSERT INTO gtest24 (a) VALUES (NULL);  -- error
+
 CREATE TYPE gtestdomain1range AS range (subtype = gtestdomain1);
-CREATE TABLE gtest24r (a int PRIMARY KEY, b gtestdomain1range GENERATED ALWAYS AS (gtestdomain1range(a, a + 5)) VIRTUAL);
---INSERT INTO gtest24r (a) VALUES (4);  -- ok
---INSERT INTO gtest24r (a) VALUES (6);  -- error
+CREATE TABLE gtest24r (a int PRIMARY KEY, b gtestdomain1range GENERATED ALWAYS AS (gtestdomain1range(a, a + 4)) VIRTUAL);
+INSERT INTO gtest24r (a) VALUES (4);  -- ok
+INSERT INTO gtest24r (a) VALUES (6);  -- error
+INSERT INTO gtest24r (a) VALUES (5);  -- error
+
+CREATE TABLE gtest24v (
+  a jsonb,
+  b gtestdomain1[] GENERATED ALWAYS AS (JSON_QUERY(a, '$.a' returning gtestdomain1[] error on error)) VIRTUAL,
+  c gtestdomain1[] GENERATED ALWAYS AS (JSON_QUERY(a, '$.b' returning gtestdomain1[])) VIRTUAL not null);
+insert into gtest24v select jsonb '{"a":[6,10], "b":[6,2]}'; --error
+insert into gtest24v select jsonb '{"a":[6,-1], "b":[6,10]}'; --error
+insert into gtest24v select jsonb '{"a":[6,-1], "b":[6,2]}'; --ok
 
 -- typed tables (currently not supported)
 CREATE TYPE gtest_type AS (f1 integer, f2 text, f3 bigint);
-- 
2.34.1

From ea676b927f0d74c1dde6237cd2f5b613af44053d Mon Sep 17 00:00:00 2001
From: jian he <[email-masked]>
Date: Thu, 6 Mar 2025 12:01:31 +0800
Subject: [PATCH v2 1/3] not null for virtual generated column

now we can add not null constraint on virtual generated column.
not null constraint on virtual generated column make sure evaulation of the
expanded generated expression does not yield null.
the virtual generated columns does not store  value, the storage is null.

discussion: https://postgr.es/m/[email-masked]
---
 src/backend/catalog/heap.c                    |  10 -
 src/backend/commands/indexcmds.c              |  10 +-
 src/backend/commands/tablecmds.c              |   8 -
 src/backend/executor/execMain.c               | 234 ++++++++++++++----
 src/backend/parser/parse_utilcmd.c            |  14 --
 src/include/nodes/execnodes.h                 |   2 +
 .../regress/expected/generated_virtual.out    |  54 ++--
 src/test/regress/sql/generated_virtual.sql    |  31 ++-
 8 files changed, 247 insertions(+), 116 deletions(-)

index bd3554c0bfd..b807ab66668 100644
@@ -2615,11 +2615,6 @@ AddRelationNewConstraints(Relation rel,
 
 			/*
@@ -2935,11 +2930,6 @@ AddRelationNotNullConstraints(Relation rel, List *constraints,
 
 		/*
 		 * A column can only have one not-null constraint, so discard any
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index 32ff3ca9a28..e690487eb3e 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -1126,10 +1126,12 @@ DefineIndex(Oid tableId,
 
 		if (TupleDescAttr(RelationGetDescr(rel), attno - 1)->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
 			ereport(ERROR,
-					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-					 stmt->isconstraint ?
-					 errmsg("unique constraints on virtual generated columns are not supported") :
-					 errmsg("indexes on virtual generated columns are not supported")));
+					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					stmt->primary ?
+					errmsg("primary key on virtual generated columns are not supported") :
+					stmt->isconstraint ?
+					errmsg("unique constraints on virtual generated columns are not supported") :
+					errmsg("indexes on virtual generated columns are not supported"));
 	}
 
 	/*
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 59156a1c1f6..c89f1cb9936 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -7824,14 +7824,6 @@ ATExecSetNotNull(List **wqueue, Relation rel, char *conName, char *colName,
 				 errmsg("cannot alter system column \"%s\"",
 						colName)));
 
index 0493b7d5365..5f0b0cc3b8e 100644
@@ -1372,6 +1373,7 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
+	resultRelInfo->ri_GeneratedNotNullExprs = NULL;
@@ -1841,6 +1843,78 @@ ExecutePlan(QueryDesc *queryDesc,
 }

+/*
+ * we warp "col IS NOT NULL" to a NullTest node. NullTest->arg is the virtual
+ * generated expression.
+ * A return value of -1 means virtual generated not null constraint satisfied,
+ * return value > 0 means not null violation happend in that attribute number.
+ *
+ * gen_virtual_cols is the attribute numbers of virtual generated column.
+*/
+static int
+ExecRelCheckGenVirtualNotNull(ResultRelInfo *resultRelInfo, TupleTableSlot *slot,
+							  EState *estate, Bitmapset *gen_virtual_cols)
+{
+	Relation	rel = resultRelInfo->ri_RelationDesc;
+	ExprContext *econtext;
+	MemoryContext oldContext;
+	int			i = 0;
+	int			attidx = -1;
+	int			*attnums;
+	int			cnt	= bms_num_members(gen_virtual_cols);
+
+	attnums = (int *) palloc0(cnt * sizeof(int));
+	if (resultRelInfo->ri_GeneratedNotNullExprs == NULL)
+	{
+		oldContext = MemoryContextSwitchTo(estate->es_query_cxt);
+		resultRelInfo->ri_GeneratedNotNullExprs =
+			(ExprState **) palloc0(cnt * sizeof(ExprState *));
+
+		while ((attidx = bms_next_member(gen_virtual_cols, attidx)) >= 0)
+		{
+			Expr	   *expr;
+			NullTest   *nnulltest;
+
+			nnulltest = makeNode(NullTest);
+			nnulltest->arg = (Expr *) build_generation_expression(rel, attidx);
+			nnulltest->nulltesttype = IS_NOT_NULL;
+			nnulltest->argisrow = false;
+			nnulltest->location = -1;
+
+			expr = (Expr *) nnulltest;
+			resultRelInfo->ri_GeneratedNotNullExprs[i] = ExecPrepareExpr(expr, estate);
+			attnums[i++] = attidx;
+		}
+		MemoryContextSwitchTo(oldContext);
+	}
+	else
+	{
+		while ((attidx = bms_next_member(gen_virtual_cols, attidx)) >= 0)
+			attnums[i++] = attidx;
+	}
+
+	/*
+	 * We will use the EState's per-tuple context for evaluating virtual
+	 * generated column not null constraint expressions (creating it if it's not
+	 * already there).
+	 */
+	econtext = GetPerTupleExprContext(estate);
+
+	/* Arrange for econtext's scan tuple to be the tuple under test */
+	econtext->ecxt_scantuple = slot;
+
+	/* And evaluate the check constraints for virtual generated column */
+	for (i = 0; i < cnt; i++)
+	{
+		ExprState  *exprstate = resultRelInfo->ri_GeneratedNotNullExprs[i];
+		if (exprstate && !ExecCheck(exprstate, econtext))
+			return attnums[i];
+	}
+
+	/* -1 result means no error */
+	return -1;
+}
+
 /*
  * ExecRelCheck --- check that tuple meets constraints for result relation
  *
@@ -2039,6 +2113,70 @@ ExecPartitionCheckEmitError(ResultRelInfo *resultRelInfo,
 			 errtable(resultRelInfo->ri_RelationDesc)));
 }
 
+/* not null constraint violation happened, now format the error message */
+static void
+NotNullViolationErrorReport(ResultRelInfo *resultRelInfo, TupleTableSlot *slot,
+							EState *estate, int attrChk)
+{
+	Bitmapset  *modifiedCols;
+	char	   *val_desc;
+
+	Relation	rel = resultRelInfo->ri_RelationDesc;
+	Relation	orig_rel = rel;
+
+	TupleDesc	tupdesc = RelationGetDescr(rel);
+	TupleDesc	orig_tupdesc = RelationGetDescr(rel);
+	Form_pg_attribute att = TupleDescAttr(tupdesc, attrChk - 1);
+
+	Assert(attrChk > 0);
+
+	/*
+	 * If the tuple has been routed, it's been converted to the
+	 * partition's rowtype, which might differ from the root
+	 * table's.  We must convert it back to the root table's
+	 * rowtype so that val_desc shown error message matches the
+	 * input tuple.
+	*/
+	if (resultRelInfo->ri_RootResultRelInfo)
+	{
+		ResultRelInfo *rootrel = resultRelInfo->ri_RootResultRelInfo;
+		AttrMap    *map;
+
+		tupdesc = RelationGetDescr(rootrel->ri_RelationDesc);
+		/* a reverse map */
+		map = build_attrmap_by_name_if_req(orig_tupdesc,
+										   tupdesc,
+										   false);
+
+		/*
+		 * Partition-specific slot's tupdesc can't be changed, so
+		 * allocate a new one.
+		*/
+		if (map != NULL)
+			slot = execute_attr_map_slot(map, slot,
+										 MakeTupleTableSlot(tupdesc, &TTSOpsVirtual));
+		modifiedCols = bms_union(ExecGetInsertedCols(rootrel, estate),
+								 ExecGetUpdatedCols(rootrel, estate));
+		rel = rootrel->ri_RelationDesc;
+	}
+	else
+		modifiedCols = bms_union(ExecGetInsertedCols(resultRelInfo, estate),
+								 ExecGetUpdatedCols(resultRelInfo, estate));
+
+	val_desc = ExecBuildSlotValueDescription(RelationGetRelid(rel),
+											 slot,
+											 tupdesc,
+											 modifiedCols,
+											 64);
+	ereport(ERROR,
+			errcode(ERRCODE_NOT_NULL_VIOLATION),
+			errmsg("null value in column \"%s\" of relation \"%s\" violates not-null constraint",
+					NameStr(att->attname),
+					RelationGetRelationName(orig_rel)),
+					val_desc ? errdetail("Failing row contains %s.", val_desc) : 0,
+					errtablecol(orig_rel, attrChk));
+}
+
 /*
  * ExecConstraints - check constraints of the tuple in 'slot'
  *
@@ -2058,73 +2196,63 @@ ExecConstraints(ResultRelInfo *resultRelInfo,
 	TupleDesc	tupdesc = RelationGetDescr(rel);
 	TupleConstr *constr = tupdesc->constr;
 	Bitmapset  *modifiedCols;
+	bool		virtual_notnull	= false;
+	Form_pg_attribute att;
+	int			natts;
+	int			attnum;
 
 	Assert(constr);				/* we should not be called otherwise */
+	natts	= tupdesc->natts;
 
 	if (constr->has_not_null)
 	{
-		int			natts = tupdesc->natts;
-		int			attrChk;
-
-		for (attrChk = 1; attrChk <= natts; attrChk++)
+		for (attnum = 1; attnum <= natts; attnum++)
 		{
-			Form_pg_attribute att = TupleDescAttr(tupdesc, attrChk - 1);
+			att = TupleDescAttr(tupdesc, attnum - 1);
 
-			if (att->attnotnull && slot_attisnull(slot, attrChk))
+			if (att->attnotnull && slot_attisnull(slot, attnum))
 			{
-				char	   *val_desc;
-				Relation	orig_rel = rel;
-				TupleDesc	orig_tupdesc = RelationGetDescr(rel);
-
-				/*
-				 * If the tuple has been routed, it's been converted to the
-				 * partition's rowtype, which might differ from the root
-				 * table's.  We must convert it back to the root table's
-				 * rowtype so that val_desc shown error message matches the
-				 * input tuple.
-				 */
-				if (resultRelInfo->ri_RootResultRelInfo)
+				/* virtual generated column not null constraint handled below */
+				if (att->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
 				{
-					ResultRelInfo *rootrel = resultRelInfo->ri_RootResultRelInfo;
-					AttrMap    *map;
-
-					tupdesc = RelationGetDescr(rootrel->ri_RelationDesc);
-					/* a reverse map */
-					map = build_attrmap_by_name_if_req(orig_tupdesc,
-													   tupdesc,
-													   false);
-
-					/*
-					 * Partition-specific slot's tupdesc can't be changed, so
-					 * allocate a new one.
-					 */
-					if (map != NULL)
-						slot = execute_attr_map_slot(map, slot,
-													 MakeTupleTableSlot(tupdesc, &TTSOpsVirtual));
-					modifiedCols = bms_union(ExecGetInsertedCols(rootrel, estate),
-											 ExecGetUpdatedCols(rootrel, estate));
-					rel = rootrel->ri_RelationDesc;
+					if (!virtual_notnull)
+						virtual_notnull = true;
+					continue;
 				}
-				else
-					modifiedCols = bms_union(ExecGetInsertedCols(resultRelInfo, estate),
-											 ExecGetUpdatedCols(resultRelInfo, estate));
-				val_desc = ExecBuildSlotValueDescription(RelationGetRelid(rel),
-														 slot,
-														 tupdesc,
-														 modifiedCols,
-														 64);
-
-				ereport(ERROR,
-						(errcode(ERRCODE_NOT_NULL_VIOLATION),
-						 errmsg("null value in column \"%s\" of relation \"%s\" violates not-null constraint",
-								NameStr(att->attname),
-								RelationGetRelationName(orig_rel)),
-						 val_desc ? errdetail("Failing row contains %s.", val_desc) : 0,
-						 errtablecol(orig_rel, attrChk)));
+				NotNullViolationErrorReport(resultRelInfo, slot, estate, attnum);
 			}
 		}
 	}
 
+	/* check virtual generated column not null constraint */
+	if (virtual_notnull)
+	{
+		Bitmapset  *vir_gencols = NULL;
+
+		Assert(constr->has_not_null);
+		Assert(constr->has_generated_virtual);
+
+		for (attnum = 1; attnum <= natts; attnum++)
+		{
+			att = TupleDescAttr(tupdesc, attnum - 1);
+			if (att->attnotnull && att->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+				vir_gencols = bms_add_member(vir_gencols, att->attnum);
+		}
+		attnum = -1;
+		Assert(vir_gencols != NULL);
+
+		attnum = ExecRelCheckGenVirtualNotNull(resultRelInfo, slot, estate, vir_gencols);
+
+		/* constraint evaulation return falsem do error report, also make an Assert */
+		if (attnum > 0)
+		{
+			att = TupleDescAttr(tupdesc, attnum - 1);
+			Assert(att->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL);
+
+			NotNullViolationErrorReport(resultRelInfo, slot, estate, attnum);
+		}
+	}
+
 	if (rel->rd_rel->relchecks > 0)
 	{
 		const char *failed;
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index abbe1bb45a3..7e9a5f7411b 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -988,20 +988,6 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column)
 							column->colname, cxt->relation->relname),
 					 parser_errposition(cxt->pstate,
 										constraint->location)));
-
-		/*
-		 * TODO: Straightforward not-null constraints won't work on virtual
-		 * generated columns, because there is no support for expanding the
-		 * column when the constraint is checked.  Maybe we could convert the
-		 * not-null constraint into a full check constraint, so that the
-		 * generation expression can be expanded at check time.
-		 */
-		if (column->is_not_null && column->generated == ATTRIBUTE_GENERATED_VIRTUAL)
-			ereport(ERROR,
-					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-					 errmsg("not-null constraints are not supported on virtual generated columns"),
-					 parser_errposition(cxt->pstate,
-										constraint->location)));
 	}
 
 	/*
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index a323fa98bbb..1c72243468c 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -549,6 +549,8 @@ typedef struct ResultRelInfo
 	/* array of constraint-checking expr states */
 	ExprState **ri_ConstraintExprs;
 
+	/* array of virtual generated not null constraint-checking expr states */
+	ExprState **ri_GeneratedNotNullExprs;
 	/*
 	 * Arrays of stored generated columns ExprStates for INSERT/UPDATE/MERGE.
 	 */
diff --git a/src/test/regress/expected/generated_virtual.out b/src/test/regress/expected/generated_virtual.out
index 7ef05f45be7..3bd5130587c 100644
--- a/src/test/regress/expected/generated_virtual.out
+++ b/src/test/regress/expected/generated_virtual.out
@@ -664,28 +664,46 @@ 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).
--- not-null constraints (currently not supported)
 CREATE TABLE gtest21a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (nullif(a, 0)) VIRTUAL NOT NULL);
-ERROR:  not-null constraints are not supported on virtual generated columns
-LINE 1: ... 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
+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).
 -- 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);  -- error
-ERROR:  not-null constraints are not supported on virtual generated columns
+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).
+INSERT INTO gtest21ax (a) VALUES (1);  --ok
+DROP TABLE gtest21ax;
 CREATE TABLE gtest21ax (a int PRIMARY KEY, b int GENERATED ALWAYS AS (nullif(a, 0)) VIRTUAL);
-ALTER TABLE gtest21ax ADD CONSTRAINT cc NOT NULL b;  -- error
-ERROR:  not-null constraints are not supported on virtual generated columns
+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).
 DROP TABLE gtest21ax;
-CREATE TABLE gtest21b (a int PRIMARY KEY, b int GENERATED ALWAYS AS (nullif(a, 0)) VIRTUAL);
+CREATE TABLE gtest21b (a int, b int GENERATED ALWAYS AS (nullif(a, 0)) VIRTUAL);
 ALTER TABLE gtest21b ALTER COLUMN b SET NOT NULL;
-ERROR:  not-null constraints are not supported on virtual generated columns
-DETAIL:  Column "b" of relation "gtest21b" is a virtual generated column.
---INSERT INTO gtest21b (a) VALUES (1);  -- ok
---INSERT INTO gtest21b (a) VALUES (0);  -- violates constraint
+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).
+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).
 ALTER TABLE gtest21b ALTER COLUMN b DROP NOT NULL;
---INSERT INTO gtest21b (a) VALUES (0);  -- ok now
+INSERT INTO gtest21b (a) VALUES (0);  -- ok now
+--not null constraint with partitioned table case.
+CREATE TABLE gtestnn_parent(f1 int, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 2) VIRTUAL not null) PARTITION BY RANGE (f1);
+CREATE TABLE gtestnn_child PARTITION OF gtestnn_parent FOR VALUES FROM (1) TO (5);
+CREATE TABLE gtestnn_childdef PARTITION OF gtestnn_parent default;
+--check the error message.
+INSERT INTO gtestnn_parent VALUES (1, NULL);
+ERROR:  null value in column "f3" of relation "gtestnn_child" violates not-null constraint
+DETAIL:  Failing row contains (1, null, virtual).
+INSERT INTO gtestnn_parent VALUES (5, NULL);
+ERROR:  null value in column "f3" of relation "gtestnn_childdef" violates not-null constraint
+DETAIL:  Failing row contains (5, null, virtual).
 -- index constraints
 CREATE TABLE gtest22a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a / 2) VIRTUAL UNIQUE);
 ERROR:  unique constraints on virtual generated columns are not supported
@@ -693,7 +711,7 @@ ERROR:  unique constraints on virtual generated columns are not supported
 --INSERT INTO gtest22a VALUES (3);
 --INSERT INTO gtest22a VALUES (4);
 CREATE TABLE gtest22b (a int, b int GENERATED ALWAYS AS (a / 2) VIRTUAL, PRIMARY KEY (a, b));
-ERROR:  not-null constraints are not supported on virtual generated columns
+ERROR:  primary key on virtual generated columns are not supported
 --INSERT INTO gtest22b VALUES (2);
 --INSERT INTO gtest22b VALUES (2);
 -- indexes
@@ -738,7 +756,7 @@ ERROR:  foreign key constraints on virtual generated columns are not supported
 --DROP TABLE gtest23b;
 --DROP TABLE gtest23a;
 CREATE TABLE gtest23p (x int, y int GENERATED ALWAYS AS (x * 2) VIRTUAL, PRIMARY KEY (y));
-ERROR:  not-null constraints are not supported on virtual generated columns
+ERROR:  primary key on virtual generated columns are not supported
 --INSERT INTO gtest23p VALUES (1), (2), (3);
 CREATE TABLE gtest23q (a int PRIMARY KEY, b int REFERENCES gtest23p (y));
 ERROR:  relation "gtest23p" does not exist
diff --git a/src/test/regress/sql/generated_virtual.sql b/src/test/regress/sql/generated_virtual.sql
index dab8c92ef99..f436e8f2313 100644
--- a/src/test/regress/sql/generated_virtual.sql
+++ b/src/test/regress/sql/generated_virtual.sql
@@ -335,23 +335,36 @@ 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

 -- 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);  -- error
+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
+INSERT INTO gtest21ax (a) VALUES (1);  --ok
+DROP TABLE gtest21ax;
 CREATE TABLE gtest21ax (a int PRIMARY KEY, b int GENERATED ALWAYS AS (nullif(a, 0)) VIRTUAL);
-ALTER TABLE gtest21ax ADD CONSTRAINT cc NOT NULL b;  -- error
+ALTER TABLE gtest21ax ADD CONSTRAINT cc NOT NULL b;
+INSERT INTO gtest21ax (a) VALUES (0);  -- violates constraint
 DROP TABLE gtest21ax;
 
-CREATE TABLE gtest21b (a int PRIMARY KEY, b int GENERATED ALWAYS AS (nullif(a, 0)) VIRTUAL);
+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 (0);  -- violates constraint
+INSERT INTO gtest21b (a) VALUES (1);  -- ok
+INSERT INTO gtest21b (a) VALUES (2), (0);  -- violates constraint
+INSERT INTO gtest21b (a) VALUES (NULL);  -- error
 ALTER TABLE gtest21b ALTER COLUMN b DROP NOT NULL;
---INSERT INTO gtest21b (a) VALUES (0);  -- ok now
+INSERT INTO gtest21b (a) VALUES (0);  -- ok now
+
+--not null constraint with partitioned table case.
+CREATE TABLE gtestnn_parent(f1 int, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 2) VIRTUAL not null) PARTITION BY RANGE (f1);
+CREATE TABLE gtestnn_child PARTITION OF gtestnn_parent FOR VALUES FROM (1) TO (5);
+CREATE TABLE gtestnn_childdef PARTITION OF gtestnn_parent default;
+--check the error message.
+INSERT INTO gtestnn_parent VALUES (1, NULL);
+INSERT INTO gtestnn_parent VALUES (5, NULL);
+
 
 -- index constraints

---

From: Navneet Kumar <[email-masked]>
Date: Fri, 7 Mar 2025 22:17:41 +0530

Hi

I encountered an issue when trying to add a virtual column to an existing
table using the ALTER command. The operation fails even though the existing
data ensures that the virtual column's value will never be NULL. However,
if I define the same virtual column while creating the table and then
insert the same data, it works as expected.

For example
This scenario fails
1. CREATE TABLE person (
    id INT GENERATED BY DEFAULT AS IDENTITY,
    first_name VARCHAR(50) NOT NULL,
    last_name VARCHAR(50) NOT NULL
);

2. INSERT INTO person (first_name, last_name)
VALUES ('first', 'last');

3. ALTER TABLE person
ADD COLUMN full_name VARCHAR(100) GENERATED ALWAYS AS (first_name || ' ' ||
last_name) VIRTUAL;

The above ALTER command works if I use STORED instead. Also if I define
virtual generated column while creating table it works with same data.

1. CREATE TABLE person (
    id INT GENERATED BY DEFAULT AS IDENTITY,
    first_name VARCHAR(50) NOT NULL,
    last_name VARCHAR(50) NOT NULL,
    full_name VARCHAR(100) NOT NULL GENERATED ALWAYS AS (first_name || '  '
||      last_name) VIRTUAL
);

2. INSERT INTO person(first_name, last_name)

    VALUES ('first', 'last');

On Thu, Mar 6, 2025 at 7:15 PM jian he <[email-masked]> wrote:

> > Hi, I’ve had a chance to review the patch.  As I am still getting
> familiar with executor part, questions and feedbacks could be relatively
> trivial.
> >

---

From: Navneet Kumar <[email-masked]>
Date: Fri, 7 Mar 2025 23:39:34 +0530

> ADD COLUMN full_name VARCHAR(100) GENERATED ALWAYS AS (first_name || ' '
> || last_name) VIRTUAL;
>

Forgot to mention NOT NULL constraint in above query.

3. ALTER TABLE person
ADD COLUMN full_name VARCHAR(100) NOT NULL GENERATED ALWAYS AS (first_name
|| ' ' || last_name) VIRTUAL;

ERROR:  column "full_name" of relation "person" contains null values

---

From: Xuneng Zhou <[email-masked]>
Date: Sat, 8 Mar 2025 12:23:02 +0800

Hi,

jian he <[email-masked]> 于2025年3月6日周四 21:44写道：

> Thank you for your explanation!  This name seems to be more clear for its
usage.

---

From: Xuneng Zhou <[email-masked]>
Date: Sat, 8 Mar 2025 12:30:11 +0800

Hi,

forget to add hackers to cc.

Xuneng Zhou <[email-masked]> 于2025年3月8日周六 12:10写道：

>
>
> Navneet Kumar <[email-masked]> 于2025年3月8日周六 02:09写道：
>
>>
>>
>>> This scenario fails
>>> 1. CREATE TABLE person (
>> ADD COLUMN full_name VARCHAR(100) NOT NULL GENERATED ALWAYS AS
>> (first_name || ' ' || last_name) VIRTUAL;
>>
> I did some debugging for this error.  It is reported in this function:
>
> */**
>
> * * ATRewriteTable: scan or rewrite one table*
>
> * **
>
> * * A rewrite is requested by passing a valid OIDNewHeap; in that case,
> caller*
>
> * * must already hold AccessExclusiveLock on it.*
>
> * */*
>
> static void
>
> *ATRewriteTable*(AlteredTableInfo **tab*, Oid *OIDNewHeap*)
>
> {
>
>
>
>             .......
>
>
> *            /* Now check any constraints on the possibly-changed tuple */*
>
>             econtext->ecxt_scantuple = insertslot;
>
>
>             foreach(l, notnull_attrs)
>
>             {
>
>                 int         attn = lfirst_int(l);
>
>
>                 if (*slot_attisnull*(insertslot, attn + 1))
>
>                 {
>
>                     Form_pg_attribute attr = *TupleDescAttr*(newTupDesc,
> attn);
>
>
>                     ereport(ERROR,
>
>                             (*errcode*(ERRCODE_NOT_NULL_VIOLATION),
>
>                              *errmsg*("column \"%s\" of relation \"%s\"
> contains null values",
>
>                                     NameStr(attr->attname),
>
>                                     RelationGetRelationName(oldrel)),
>
>                              *errtablecol*(oldrel, attn + 1)));
>
>                 }
>
>             }
>
>         .......
>
> }
>
>
> If this error is unexpected, I think the issue is that when adding a NOT
> NULL constraint to a regular column, pg scans the table to ensure no NULL
> values exist. But for virtual columns, there are no stored values to scan.
> Maybe we should add some condition like this? Then checking not null at
> runtime.
>
>
> * /* Skip NOT NULL validation for virtual generated columns during table
> rewrite */*
>
>     if (TupleDescAttr(newTupDesc, attn)->attgenerated ==
> ATTRIBUTE_GENERATED_VIRTUAL)
>
>         continue;
>

---

From: jian he <[email-masked]>
Date: Sat, 8 Mar 2025 18:10:56 +0800

hi.
patch attached to fix the above thread mentioned issue, related tests added.

also there are several compiler warnings at [0] for virtual generated
column domain patch,
i did some minor adjustment, let's see how it goes.

[0] https://cirrus-ci.com/task/4550600270020608
From 0985d7a779287feaa1a382726e566418bc56d381 Mon Sep 17 00:00:00 2001
From: jian he <[email-masked]>
Date: Sat, 8 Mar 2025 18:08:51 +0800
Subject: [PATCH v3 3/3] domain over virtual generated column.

discussion: https://postgr.es/m/[email-masked]
---
 src/backend/catalog/heap.c                    |  11 -
 src/backend/commands/copyfrom.c               |   5 +-
 src/backend/commands/tablecmds.c              |   6 +-
 src/backend/commands/typecmds.c               | 210 +++++++++++++++---
 src/backend/executor/nodeModifyTable.c        |  84 +++++--
 .../regress/expected/generated_virtual.out    |  67 +++++-
 src/test/regress/sql/generated_virtual.sql    |  49 +++-
 7 files changed, 352 insertions(+), 80 deletions(-)

index 41efda3d338..55411d3d5e1 100644
@@ -7510,10 +7510,14 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
index 3cb3ca1cca1..7ef84700619 100644

+static bool
 
+	/*
 
+		ExprState **ri_GeneratedExprs = NULL;
 
@@ -3146,8 +3212,8 @@ validateDomainNotNullConstraint(Oid domainoid)
 
 				if (slot_attisnull(slot, attnum))
 				{
@@ -3158,14 +3224,30 @@ validateDomainNotNullConstraint(Oid domainoid)
 					 * only executes in an ALTER DOMAIN command, the client
 					 * should already know which domain is in question.
 					 */
-					ereport(ERROR,
-							(errcode(ERRCODE_NOT_NULL_VIOLATION),
-							 errmsg("column \"%s\" of table \"%s\" contains null values",
-									NameStr(attr->attname),
-									RelationGetRelationName(testrel)),
-							 errtablecol(testrel, attnum)));
+					if (attr->attgenerated != ATTRIBUTE_GENERATED_VIRTUAL)
+						ereport(ERROR,
+								(errcode(ERRCODE_NOT_NULL_VIOLATION),
+								errmsg("column \"%s\" of table \"%s\" contains null values",
+										NameStr(attr->attname),
+										RelationGetRelationName(testrel)),
+								errtablecol(testrel, attnum)));
+					else
+					{
+						Assert(need_GeneratedExprs);
+						econtext->ecxt_scantuple = slot;
+						Assert(ri_GeneratedExprs[attnum - 1] != NULL);
+						if (!ExecCheck(ri_GeneratedExprs[attnum - 1] , econtext))
+							ereport(ERROR,
+									errcode(ERRCODE_NOT_NULL_VIOLATION),
+									errmsg("column \"%s\" of table \"%s\" contains null values",
+											NameStr(attr->attname),
+											RelationGetRelationName(testrel)),
+									errtablecol(testrel, attnum));
+					}
 				}
 			}
+
+			ResetExprContext(econtext);
 		}
 		ExecDropSingleTupleTableSlot(slot);
 		table_endscan(scan);
@@ -3174,6 +3256,8 @@ validateDomainNotNullConstraint(Oid domainoid)
 		/* Close each rel after processing, but keep lock */
 		table_close(testrel, NoLock);
 	}
+
+	FreeExecutorState(estate);
 }
 
 /*
@@ -3210,6 +3294,32 @@ validateDomainCheckConstraint(Oid domainoid, const char *ccbin)
 		TupleTableSlot *slot;
 		TableScanDesc scan;
 		Snapshot	snapshot;
+		ExprState **ri_GeneratedExprs = NULL;
 
@@ -3222,37 +3332,75 @@ validateDomainCheckConstraint(Oid domainoid, const char *ccbin)

+					Assert(need_GeneratedExprs);

 
 
 /*

 	/*

 

 
-			/*
 
-			values[i] = val;
-			nulls[i] = isnull;
+				values[i] = val;
+				nulls[i] = isnull;
+			}
+			else
+			{

 
 	/*
index 0eef3fcfff1..7c8bacf9232 100644
@@ -785,16 +785,65 @@ ERROR:  relation "gtest23p" does not exist
index 485e2a317fe..81f03cc4a92 100644
@@ -439,14 +439,49 @@ CREATE TABLE gtest23q (a int PRIMARY KEY, b int REFERENCES gtest23p (y));
 
 -- domains

From 7096ea7b53316117e8ac4a309e88c9f543a956df Mon Sep 17 00:00:00 2001
From: jian he <[email-masked]>
Date: Thu, 6 Mar 2025 12:35:19 +0800
Subject: [PATCH v3 2/3] rename ExecComputeGenerated to ExecComputeGenerated

 
 				/*

 
 /*
 
 		/*
 
 		/*
 
 /*

From f1f455eb8bcaab05c9af090bf9cb979cc7af7746 Mon Sep 17 00:00:00 2001
From: jian he <[email-masked]>
Date: Sat, 8 Mar 2025 17:38:25 +0800
Subject: [PATCH v3 1/3] not null for virtual generated column

we do support ALTER COLUMN SET EXPRESSION over not null virtual generated column.

discussion: https://postgr.es/m/[email-masked]
---
 src/backend/catalog/heap.c                    |  10 -
 src/backend/commands/indexcmds.c              |  10 +-
 src/backend/commands/tablecmds.c              |  68 +++++-
 src/backend/executor/execMain.c               | 220 +++++++++++++-----
 src/backend/parser/parse_utilcmd.c            |  14 --
 src/include/executor/executor.h               |   4 +
 src/include/nodes/execnodes.h                 |   2 +
 .../regress/expected/generated_virtual.out    |  89 +++++--
 src/test/regress/sql/generated_virtual.sql    |  50 +++-
 9 files changed, 348 insertions(+), 119 deletions(-)

 			/*
 
 		/*

 	/*
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 59156a1c1f6..41efda3d338 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6080,6 +6080,7 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap)
 	TupleDesc	newTupDesc;
 	bool		needscan = false;
 	List	   *notnull_attrs;
+	List	   *all_virtual_nns = NIL;
 	int			i;
 	ListCell   *l;
 	EState	   *estate;
@@ -6183,6 +6184,16 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap)
 			needscan = true;
 	}
 
+	foreach_int(attn, notnull_attrs)
+	{
+		Form_pg_attribute attr = TupleDescAttr(newTupDesc, attn);
+
+		Assert(attr->attnotnull);
+		Assert(!attr->attisdropped);
+		if (attr->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+			all_virtual_nns = lappend_int(all_virtual_nns, attr->attnum);
+	}
+
 	if (newrel || needscan)
 	{
 		ExprContext *econtext;
@@ -6193,6 +6204,22 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap)
 		List	   *dropped_attrs = NIL;
 		ListCell   *lc;
 		Snapshot	snapshot;
+		ResultRelInfo *rInfo;
+		MemoryContext oldcontext;
+
+		if (list_length(all_virtual_nns) > 0)
+		{
+			Assert(newTupDesc->constr->has_generated_virtual);
+			Assert(newTupDesc->constr->has_not_null);
+			oldcontext = MemoryContextSwitchTo(estate->es_query_cxt);
+			rInfo = makeNode(ResultRelInfo);
+			InitResultRelInfo(rInfo,
+							  oldrel,
+							  0,		/* dummy rangetable index */
+							  NULL,
+							  estate->es_instrument);
+			MemoryContextSwitchTo(oldcontext);
+		}
 
 		if (newrel)
 			ereport(DEBUG1,
@@ -6275,6 +6302,7 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap)
 		while (table_scan_getnextslot(scan, ForwardScanDirection, oldslot))
 		{
 			TupleTableSlot *insertslot;
+			bool	virtual_notnull_check = false;
 
 			if (tab->rewrite > 0)
 			{
@@ -6366,6 +6394,14 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap)
 				{
 					Form_pg_attribute attr = TupleDescAttr(newTupDesc, attn);
 
+					/* virtual generated column not null constraint handled below */
+
+					if (attr->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+					{
+						if (!virtual_notnull_check)
+							virtual_notnull_check = true;
+						continue;
+					}
 					ereport(ERROR,
 							(errcode(ERRCODE_NOT_NULL_VIOLATION),
 							 errmsg("column \"%s\" of relation \"%s\" contains null values",
@@ -6375,6 +6411,27 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap)
 				}
 			}
 
+			if (virtual_notnull_check)
+			{
+				int		attnum = -1;
+				Assert(list_length(all_virtual_nns) > 0);
+
+				attnum = ExecRelCheckGenVirtualNotNull(rInfo, insertslot,
+													   estate,
+													   all_virtual_nns);
+				if (attnum > 0)
+				{
+					Form_pg_attribute attr = TupleDescAttr(newTupDesc, attnum - 1);
+
+					ereport(ERROR,
+							errcode(ERRCODE_NOT_NULL_VIOLATION),
+							errmsg("column \"%s\" of relation \"%s\" contains null values",
+									NameStr(attr->attname),
+									RelationGetRelationName(oldrel)),
+							errtablecol(oldrel, attnum));
+				}
+			}
+
 			foreach(l, tab->constraints)
 			{
 				NewConstraint *con = lfirst(l);
@@ -7824,14 +7881,6 @@ ATExecSetNotNull(List **wqueue, Relation rel, char *conName, char *colName,
 				 errmsg("cannot alter system column \"%s\"",
 						colName)));
 
-	/* TODO: see transformColumnDefinition() */
-	if (TupleDescAttr(RelationGetDescr(rel), attnum - 1)->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
-		ereport(ERROR,
-				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-				 errmsg("not-null constraints are not supported on virtual generated columns"),
-				 errdetail("Column \"%s\" of relation \"%s\" is a virtual generated column.",
-						   colName, RelationGetRelationName(rel))));
-
 	/* See if there's already a constraint */
 	tuple = findNotNullConstraintAttnum(RelationGetRelid(rel), attnum);
 	if (HeapTupleIsValid(tuple))
@@ -8500,6 +8549,9 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,
 				 errdetail("Column \"%s\" of relation \"%s\" is a virtual generated column.",
 						   colName, RelationGetRelationName(rel))));
 
+	if (attgenerated == ATTRIBUTE_GENERATED_VIRTUAL && attTup->attnotnull)
+		tab->verify_new_notnull = true;
+
 	/*
 	 * We need to prevent this because a change of expression could affect a
 	 * row filter and inject expressions that are not permitted in a row
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 0493b7d5365..51fe8efc559 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -51,6 +51,7 @@
 #include "foreign/fdwapi.h"
 #include "mb/pg_wchar.h"
 #include "miscadmin.h"
+#include "nodes/makefuncs.h"
 #include "nodes/queryjumble.h"
 #include "parser/parse_relation.h"
 #include "pgstat.h"
@@ -1372,6 +1373,7 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
 	resultRelInfo->ri_FdwState = NULL;
 	resultRelInfo->ri_usesFdwDirectModify = false;
 	resultRelInfo->ri_ConstraintExprs = NULL;
+	resultRelInfo->ri_GeneratedNotNullExprs = NULL;
 	resultRelInfo->ri_GeneratedExprsI = NULL;
 	resultRelInfo->ri_GeneratedExprsU = NULL;
 	resultRelInfo->ri_projectReturning = NULL;
@@ -1841,6 +1843,70 @@ ExecutePlan(QueryDesc *queryDesc,
 }

+/*
+ * we warp "virtual_generated col IS NOT NULL" to a NullTest node.
+ * NullTest->arg is the virtual generated expression.  return value of -1 means
+ * ok.  return value > 0 means not null violation happended for that attribute
+ * number.
+ * all_virtual_nns is *all* the virtual generated column not-null attribute numbers.
+ *
+*/
+int
+ExecRelCheckGenVirtualNotNull(ResultRelInfo *resultRelInfo, TupleTableSlot *slot,
+							  EState *estate, List *all_virtual_nns)
+{
+	Relation	rel = resultRelInfo->ri_RelationDesc;
+	ExprContext *econtext;
+	MemoryContext oldContext;
+	int			i = 0;
+	int			cnt = list_length(all_virtual_nns);
+
+	if (resultRelInfo->ri_GeneratedNotNullExprs == NULL)
+	{
+		oldContext = MemoryContextSwitchTo(estate->es_query_cxt);
+		resultRelInfo->ri_GeneratedNotNullExprs =
+			(ExprState **) palloc0(cnt * sizeof(ExprState *));
+
+		foreach_int(attidx, all_virtual_nns)
+		{
+			Expr	   *expr;
+			NullTest   *nnulltest;
+
+			nnulltest = makeNode(NullTest);
+			nnulltest->arg = (Expr *) build_generation_expression(rel, attidx);
+			nnulltest->nulltesttype = IS_NOT_NULL;
+			nnulltest->argisrow = false;
+			nnulltest->location = -1;
+
+			expr = (Expr *) nnulltest;
+			resultRelInfo->ri_GeneratedNotNullExprs[i++] = ExecPrepareExpr(expr, estate);
+		}
+		MemoryContextSwitchTo(oldContext);
+	}
+
+	/*
+	 * We will use the EState's per-tuple context for evaluating virtual
+	 * generated column not null constraint expressions (creating it if it's not
+	 * already there).
+	 */
+	econtext = GetPerTupleExprContext(estate);
+
+	/* Arrange for econtext's scan tuple to be the tuple under test */
+	econtext->ecxt_scantuple = slot;
+
+	/* And evaluate the check constraints for virtual generated column */
+	i = 0;
+	foreach_int(attnum, all_virtual_nns)
+	{
+		ExprState  *exprstate = resultRelInfo->ri_GeneratedNotNullExprs[i++];
+		if (exprstate && !ExecCheck(exprstate, econtext))
+			return attnum;
+	}
+
+	/* -1 result means no error */
+	return -1;
+}
+
 /*
  * ExecRelCheck --- check that tuple meets constraints for result relation
  *
@@ -2039,6 +2105,70 @@ ExecPartitionCheckEmitError(ResultRelInfo *resultRelInfo,
 			 errtable(resultRelInfo->ri_RelationDesc)));
 }
 
@@ -2058,73 +2188,57 @@ ExecConstraints(ResultRelInfo *resultRelInfo,
+	List 		*all_virtual_nns = NIL;
+	bool		virtual_notnull_check = false;

 
+				all_virtual_nns = lappend_int(all_virtual_nns, att->attnum);
+					if (!virtual_notnull_check)
+						virtual_notnull_check = true;
 
+	if (virtual_notnull_check)
+		Assert(list_length(all_virtual_nns) > 0);
+		attnum = ExecRelCheckGenVirtualNotNull(resultRelInfo, slot, estate, all_virtual_nns);
+			NotNullViolationErrorReport(resultRelInfo, slot, estate, att->attnum);
 
 	/*
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index d12e3f451d2..30edc1cd644 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -220,6 +220,10 @@ extern ResultRelInfo *ExecGetTriggerResultRel(EState *estate, Oid relid,
 extern List *ExecGetAncestorResultRels(EState *estate, ResultRelInfo *resultRelInfo);
 extern void ExecConstraints(ResultRelInfo *resultRelInfo,
 							TupleTableSlot *slot, EState *estate);
+extern int ExecRelCheckGenVirtualNotNull(ResultRelInfo *resultRelInfo,
+										 TupleTableSlot *slot,
+										 EState *estate,
+										 List *all_virtual_nns);
 extern bool ExecPartitionCheck(ResultRelInfo *resultRelInfo,
 							   TupleTableSlot *slot, EState *estate, bool emitError);
 extern void ExecPartitionCheckEmitError(ResultRelInfo *resultRelInfo,
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index a323fa98bbb..1c72243468c 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -549,6 +549,8 @@ typedef struct ResultRelInfo
 	/* array of constraint-checking expr states */
 	ExprState **ri_ConstraintExprs;
 
+	/* array of virtual generated not null constraint-checking expr states */
+	ExprState **ri_GeneratedNotNullExprs;
 	/*
 	 * Arrays of stored generated columns ExprStates for INSERT/UPDATE/MERGE.
 	 */
diff --git a/src/test/regress/expected/generated_virtual.out b/src/test/regress/expected/generated_virtual.out
index 7ef05f45be7..0eef3fcfff1 100644
--- a/src/test/regress/expected/generated_virtual.out
+++ b/src/test/regress/expected/generated_virtual.out
@@ -664,28 +664,68 @@ 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).
--- not-null constraints (currently not supported)
 CREATE TABLE gtest21a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (nullif(a, 0)) VIRTUAL NOT NULL);
-ERROR:  not-null constraints are not supported on virtual generated columns
-LINE 1: ... 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
+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).
 -- 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);  -- error
-ERROR:  not-null constraints are not supported on virtual generated columns
+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).
+INSERT INTO gtest21ax (a) VALUES (1);  --ok
+-- SET EXPRESSION support not null constraint
+ALTER TABLE gtest21ax ALTER COLUMN b SET EXPRESSION AS (nullif(a, 1)); --error
+ERROR:  column "b" of relation "gtest21ax" contains null values
+DROP TABLE gtest21ax;
 CREATE TABLE gtest21ax (a int PRIMARY KEY, b int GENERATED ALWAYS AS (nullif(a, 0)) VIRTUAL);
-ALTER TABLE gtest21ax ADD CONSTRAINT cc NOT NULL b;  -- error
-ERROR:  not-null constraints are not supported on virtual generated columns
+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).
 DROP TABLE gtest21ax;
-CREATE TABLE gtest21b (a int PRIMARY KEY, b int GENERATED ALWAYS AS (nullif(a, 0)) VIRTUAL);
+CREATE TABLE gtest21b (a int, b int GENERATED ALWAYS AS (nullif(a, 0)) VIRTUAL);
 ALTER TABLE gtest21b ALTER COLUMN b SET NOT NULL;
-ERROR:  not-null constraints are not supported on virtual generated columns
-DETAIL:  Column "b" of relation "gtest21b" is a virtual generated column.
---INSERT INTO gtest21b (a) VALUES (1);  -- ok
---INSERT INTO gtest21b (a) VALUES (0);  -- violates constraint
+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).
+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).
 ALTER TABLE gtest21b ALTER COLUMN b DROP NOT NULL;
---INSERT INTO gtest21b (a) VALUES (0);  -- ok now
+INSERT INTO gtest21b (a) VALUES (0);  -- ok now
+--not null constraint with partitioned table case.
+CREATE TABLE gtestnn_parent(f1 int, f2 bigint, f3 bigint GENERATED ALWAYS AS (nullif(f1, 1) + nullif(f2, 10)) VIRTUAL not null) PARTITION BY RANGE (f1);
+CREATE TABLE gtestnn_child PARTITION OF gtestnn_parent FOR VALUES FROM (1) TO (5);
+CREATE TABLE gtestnn_childdef PARTITION OF gtestnn_parent default;
+--check the error message.
+INSERT INTO gtestnn_parent VALUES (2, 2, default), (3, 5, default), (14, 12, default);
+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).
+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).
+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); --error
+select * from gtestnn_parent;
+ f1 | f2 | f3 
+----+----+----
+  2 |  2 |  4
+  3 |  5 |  8
+ 14 | 12 | 26
+ 10 | 11 | 21
+(4 rows)
+
+--test ALTER TABLE ADD COLUMN.
+ALTER TABLE gtestnn_parent ADD COLUMN c int NOT NULL GENERATED ALWAYS AS (nullif(f1, 14) + nullif(f2, 10)) VIRTUAL; --error
+ERROR:  column "c" of relation "gtestnn_childdef" contains null values
+ALTER TABLE gtestnn_parent ADD COLUMN c int NOT NULL GENERATED ALWAYS AS (nullif(f1, 13) + nullif(f2, 5)) VIRTUAL; --error
+ERROR:  column "c" of relation "gtestnn_child" contains null values
+ALTER TABLE gtestnn_parent ADD COLUMN c int NOT NULL GENERATED ALWAYS AS (nullif(f1, 4) + nullif(f2, 6)) VIRTUAL; --ok
 -- index constraints
 CREATE TABLE gtest22a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a / 2) VIRTUAL UNIQUE);
 ERROR:  unique constraints on virtual generated columns are not supported
@@ -693,7 +733,7 @@ ERROR:  unique constraints on virtual generated columns are not supported
 --INSERT INTO gtest22a VALUES (3);
 --INSERT INTO gtest22a VALUES (4);
 CREATE TABLE gtest22b (a int, b int GENERATED ALWAYS AS (a / 2) VIRTUAL, PRIMARY KEY (a, b));
-ERROR:  not-null constraints are not supported on virtual generated columns
+ERROR:  primary key on virtual generated columns are not supported
 --INSERT INTO gtest22b VALUES (2);
 --INSERT INTO gtest22b VALUES (2);
 -- indexes
@@ -738,7 +778,7 @@ ERROR:  foreign key constraints on virtual generated columns are not supported
 --DROP TABLE gtest23b;
 --DROP TABLE gtest23a;
 CREATE TABLE gtest23p (x int, y int GENERATED ALWAYS AS (x * 2) VIRTUAL, PRIMARY KEY (y));
-ERROR:  not-null constraints are not supported on virtual generated columns
+ERROR:  primary key on virtual generated columns are not supported
 --INSERT INTO gtest23p VALUES (1), (2), (3);
 CREATE TABLE gtest23q (a int PRIMARY KEY, b int REFERENCES gtest23p (y));
 ERROR:  relation "gtest23p" does not exist
@@ -1029,7 +1069,7 @@ CREATE TABLE gtest27 (
     b int,
     x int GENERATED ALWAYS AS ((a + b) * 2) VIRTUAL
 );
-INSERT INTO gtest27 (a, b) VALUES (3, 7), (4, 11);
+INSERT INTO gtest27 (a, b) VALUES (3, 7), (4, 11), (NULL, NULL);
 ALTER TABLE gtest27 ALTER COLUMN a TYPE text;  -- error
 ERROR:  cannot alter type of a column used by a generated column
 DETAIL:  Column "a" is used by generated column "x".
@@ -1047,7 +1087,8 @@ SELECT * FROM gtest27;
 ---+----+----
  3 |  7 | 20
  4 | 11 | 30
-(2 rows)
+   |    |   
+(3 rows)
 
 ALTER TABLE gtest27 ALTER COLUMN x TYPE boolean USING x <> 0;  -- error
 ERROR:  cannot specify USING when altering type of generated column
@@ -1056,6 +1097,14 @@ LINE 1: ALTER TABLE gtest27 ALTER COLUMN x TYPE boolean USING x <> 0...
 DETAIL:  Column "x" is a generated column.
 ALTER TABLE gtest27 ALTER COLUMN x DROP DEFAULT;  -- error
 ERROR:  column "x" of relation "gtest27" is a generated column
+--not null error violation
+ALTER TABLE gtest27
+  DROP COLUMN x,
+  ALTER COLUMN a TYPE bigint,
+  ALTER COLUMN b TYPE bigint,
+  ADD COLUMN x bigint GENERATED ALWAYS AS ((a + b) * 2) VIRTUAL NOT NULL;
+ERROR:  column "x" of relation "gtest27" contains null values
+DELETE FROM gtest27 WHERE a IS NULL AND b IS NULL;
 -- It's possible to alter the column types this way:
 ALTER TABLE gtest27
   DROP COLUMN x,
diff --git a/src/test/regress/sql/generated_virtual.sql b/src/test/regress/sql/generated_virtual.sql
index dab8c92ef99..485e2a317fe 100644
--- a/src/test/regress/sql/generated_virtual.sql
+++ b/src/test/regress/sql/generated_virtual.sql
@@ -335,23 +335,46 @@ 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

+-- SET EXPRESSION support not null constraint
+ALTER TABLE gtest21ax ALTER COLUMN b SET EXPRESSION AS (nullif(a, 1)); --error
 
-CREATE TABLE gtest21b (a int PRIMARY KEY, b int GENERATED ALWAYS AS (nullif(a, 0)) VIRTUAL);
+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 (0);  -- violates constraint
+INSERT INTO gtest21b (a) VALUES (1);  -- ok
+INSERT INTO gtest21b (a) VALUES (2), (0);  -- violates constraint
+INSERT INTO gtest21b (a) VALUES (NULL);  -- error
 ALTER TABLE gtest21b ALTER COLUMN b DROP NOT NULL;
---INSERT INTO gtest21b (a) VALUES (0);  -- ok now
+INSERT INTO gtest21b (a) VALUES (0);  -- ok now
+
+--not null constraint with partitioned table case.
+CREATE TABLE gtestnn_parent(f1 int, f2 bigint, f3 bigint GENERATED ALWAYS AS (nullif(f1, 1) + nullif(f2, 10)) VIRTUAL not null) PARTITION BY RANGE (f1);
+CREATE TABLE gtestnn_child PARTITION OF gtestnn_parent FOR VALUES FROM (1) TO (5);
+CREATE TABLE gtestnn_childdef PARTITION OF gtestnn_parent default;
+--check the error message.
+INSERT INTO gtestnn_parent VALUES (2, 2, default), (3, 5, default), (14, 12, default);
+INSERT INTO gtestnn_parent VALUES (1, 2, default); --error
+INSERT INTO gtestnn_parent VALUES (2, 10, default); --error
+ALTER TABLE gtestnn_parent ALTER COLUMN f3 SET EXPRESSION AS (nullif(f1, 2) + nullif(f2, 11)); --error
+INSERT INTO gtestnn_parent VALUES (10, 11, default); --error
+select * from gtestnn_parent;
+--test ALTER TABLE ADD COLUMN.
+ALTER TABLE gtestnn_parent ADD COLUMN c int NOT NULL GENERATED ALWAYS AS (nullif(f1, 14) + nullif(f2, 10)) VIRTUAL; --error
+ALTER TABLE gtestnn_parent ADD COLUMN c int NOT NULL GENERATED ALWAYS AS (nullif(f1, 13) + nullif(f2, 5)) VIRTUAL; --error
+ALTER TABLE gtestnn_parent ADD COLUMN c int NOT NULL GENERATED ALWAYS AS (nullif(f1, 4) + nullif(f2, 6)) VIRTUAL; --ok
 
 -- index constraints
 CREATE TABLE gtest22a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a / 2) VIRTUAL UNIQUE);
@@ -524,13 +547,20 @@ CREATE TABLE gtest27 (
     b int,
     x int GENERATED ALWAYS AS ((a + b) * 2) VIRTUAL
 );
-INSERT INTO gtest27 (a, b) VALUES (3, 7), (4, 11);
+INSERT INTO gtest27 (a, b) VALUES (3, 7), (4, 11), (NULL, NULL);
 ALTER TABLE gtest27 ALTER COLUMN a TYPE text;  -- error
 ALTER TABLE gtest27 ALTER COLUMN x TYPE numeric;
 \d gtest27
 SELECT * FROM gtest27;
 ALTER TABLE gtest27 ALTER COLUMN x TYPE boolean USING x <> 0;  -- error
 ALTER TABLE gtest27 ALTER COLUMN x DROP DEFAULT;  -- error
+--not null error violation
+ALTER TABLE gtest27
+  DROP COLUMN x,
+  ALTER COLUMN a TYPE bigint,
+  ALTER COLUMN b TYPE bigint,
+  ADD COLUMN x bigint GENERATED ALWAYS AS ((a + b) * 2) VIRTUAL NOT NULL;
+DELETE FROM gtest27 WHERE a IS NULL AND b IS NULL;
 -- It's possible to alter the column types this way:
 ALTER TABLE gtest27
   DROP COLUMN x,
-- 
2.34.1

---

From: jian he <[email-masked]>
Date: Thu, 13 Mar 2025 20:22:11 +0800

hi.

new patch attached.

0001 for virtual generated columns not null.
minor change to fix the compiler warning.
0002-0004 is for domain over virtual generated columns.

0002: we need to compute the generation expression for the domain with
constraints,
thus rename ExecComputeStoredGenerated to ExecComputeGenerated.

0003. preparatory patch for 0004.
soft error variant of ExecPrepareExpr, ExecInitExpr.
for soft error processing of CoerceToDomain. see below description.

0004. no table rewrite for adding virtual generation column over
domain with constraints.
(syntax: ALTER TABLE xx ADD COLUMN X domain_type GENERATED ALWAYS AS
(expression) VIRTUAL
in phase3, ATRewriteTable: we already initialized AlteredTableInfo->newvals via
``ex->exprstate = ExecInitExpr((Expr *) ex->expr, NULL);``
we can easily evaluate it via ExecCheck. if fail, then error out.

--------------
reason for the 0003 patch:

ALTER DOMAIN ADD CONSTRAINT.
since the virtual generated column has no actual storage.
so, in validateDomainCheckConstraint we cannot use
```
                    d = slot_getattr(slot, attnum, &isNull);
                    econtext->domainValue_datum = d;
                    econtext->domainValue_isNull = isNull;
                    conResult = ExecEvalExprSwitchContext(exprstate,
                                                          econtext,
                                                          &isNull);

```
to check whether existing generation expression satisfy the newly
added domain constraint or not.

we also need to evaluate error softly,
because
ALTER DOMAIN ADD CONSTRAINT need check exists table domain value satisfy
the newly constraint or not.
if we not do soft error evaluation, the error message would be like:
``value for domain gtestdomain1 violates check constraint "gtestdomain1_check"``
but we want error message like:
ERROR:  column "b" of table "gtest24" contains values that violate the
new constraint
--------------
From 30485418e6cd2dd993094b351b6ad10b4d885da3 Mon Sep 17 00:00:00 2001
From: jian he <[email-masked]>
Date: Thu, 13 Mar 2025 20:15:46 +0800
Subject: [PATCH v4 2/4] rename ExecComputeStoredGenerated to
 ExecComputeGenerated

 
 				/*
index 0a9b880d250..05931bb3391 100644

 
 /*
 
 		/*
 
 		/*
 
 /*

From 9e9b4cbf32bb5599a34803b7d12bad1dcb1ad03a Mon Sep 17 00:00:00 2001
From: jian he <[email-masked]>
Date: Thu, 13 Mar 2025 20:18:32 +0800
Subject: [PATCH v4 4/4] domain over virtual generated column.

domains that don't have constraints work just fine.
domain constraints check happen within ExecComputeGenerated.
we compute the virtual generated column in ExecComputeGenerated and discarded the value.

domain_with_constaint can be element type of range, multirange, array, composite.
to be bulletproof, if this column type is not type of TYPTYPE_BASE or it's an array type,
then we do actually compute the generated expression.
This can be have negative performance for INSERTs

The following two query shows in pg_catalog, most of the system type is TYPTYPE_BASE.
So I guess this compromise is fine.

ALTER TABLE ADD VIRTUAL GENERATED COLUMN.
need table scan to verify new column generation expression value
satisfied the domain constraints.
but no need table rewrite!

discussion: https://postgr.es/m/[email-masked]
---
 src/backend/catalog/heap.c                    |  11 -
 src/backend/commands/copyfrom.c               |   5 +-
 src/backend/commands/tablecmds.c              |  41 +++-
 src/backend/commands/typecmds.c               | 197 +++++++++++++++---
 src/backend/executor/nodeModifyTable.c        |  90 ++++++--
 src/test/regress/expected/fast_default.out    |   4 +
 .../regress/expected/generated_virtual.out    |  65 +++++-
 src/test/regress/sql/fast_default.sql         |   4 +
 src/test/regress/sql/generated_virtual.sql    |  47 ++++-
 9 files changed, 382 insertions(+), 82 deletions(-)

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index b8d82ba2809..5fe6f09e248 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -5980,7 +5980,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * rebuild data.
 			 */
 			if (tab->constraints != NIL || tab->verify_new_notnull ||
-				tab->partition_constraint != NULL)
+				tab->newvals || tab->partition_constraint != NULL)
 				ATRewriteTable(tab, InvalidOid);
 
 			/*
@@ -6100,6 +6100,7 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap)
 	BulkInsertState bistate;
 	int			ti_options;
 	ExprState  *partqualstate = NULL;
+	List		*virtual_check = NIL;
 
 	/*
 	 * Open the relation(s).  We have surely already locked the existing
@@ -6175,6 +6176,16 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap)
 
 		/* expr already planned */
 		ex->exprstate = ExecInitExpr((Expr *) ex->expr, NULL);
+		if (tab->rewrite == 0 && ex->is_generated)
+		{
+			Form_pg_attribute attr = TupleDescAttr(newTupDesc, ex->attnum - 1);
+			if (attr->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL &&
+				DomainHasConstraints(attr->atttypid))
+			{
+				Assert(!attr->attisdropped);
+				virtual_check = lappend_int(virtual_check, attr->attnum);
+			}
+		}
 	}
 
 	notnull_attrs = NIL;
@@ -6206,6 +6217,13 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap)
 			all_virtual_nns = lappend_int(all_virtual_nns, attr->attnum);
 	}
 
+	/*
+	 * We need to verify that domain constraints on the virtual generated column
+	 * are satisfied, which requires table scan.
+	*/
+	if (virtual_check != NIL)
+		needscan = true;
+
 	if (newrel || needscan)
 	{
 		ExprContext *econtext;
@@ -6469,6 +6487,27 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap)
 				}
 			}
 
+			if (virtual_check != NIL)
+			{
+				foreach(l, tab->newvals)
+				{
+					NewColumnValue *ex = lfirst(l);
+
+					if (!ex->is_generated)
+						continue;
+
+					if(list_member_int(virtual_check, ex->attnum) &&
+					   !ExecCheck(ex->exprstate, econtext))
+					{
+						Form_pg_attribute attr = TupleDescAttr(newTupDesc, ex->attnum - 1);
+						ereport(ERROR,
+								errcode(ERRCODE_CHECK_VIOLATION),
+								errmsg("value for domain %s is violated by some row",
+								format_type_be(attr->atttypid)));
+					}
+				}
+			}
+
 			if (partqualstate && !ExecCheck(partqualstate, econtext))
 			{
 				if (tab->validate_default)
diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c
index 3cb3ca1cca1..8f2888fe0e1 100644
--- a/src/backend/commands/typecmds.c
+++ b/src/backend/commands/typecmds.c
@@ -65,6 +65,7 @@
 #include "parser/parse_expr.h"
 #include "parser/parse_func.h"
 #include "parser/parse_type.h"
+#include "rewrite/rewriteHandler.h"
 #include "utils/builtins.h"
 #include "utils/fmgroids.h"
 #include "utils/inval.h"
@@ -2772,6 +2773,8 @@ AlterDomainNotNull(List *names, bool notNull)
 								   typTup->typbasetype, typTup->typtypmod,
 								   constr, NameStr(typTup->typname), NULL);
 
+		CommandCounterIncrement();
+
 		validateDomainNotNullConstraint(domainoid);
 	}
 	else
@@ -2974,7 +2977,15 @@ AlterDomainAddConstraint(List *names, Node *newConstraint,
 		 * to.
 		 */
 		if (!constr->skip_validation)
+		{
+			/*
+			 * validate virtual generated column over domain check constraint
+			 * need see pg_constraint row
+			 */
+			CommandCounterIncrement();
+
 			validateDomainCheckConstraint(domainoid, ccbin);
+		}
 
 		/*
 		 * We must send out an sinval message for the domain, to ensure that
@@ -3121,6 +3132,15 @@ validateDomainNotNullConstraint(Oid domainoid)
 	List	   *rels;
 	ListCell   *rt;
 
+	/*
+	 * EState is for domain not null constraint over virtual generated column.
+	 * it maybe needed in below.
+	*/
+	EState	   *estate;
+	ExprContext *econtext;
+	estate = CreateExecutorState();
+	econtext = GetPerTupleExprContext(estate);
+
 	/* Fetch relation list with attributes based on this domain */
 	/* ShareLock is sufficient to prevent concurrent data changes */
 
@@ -3134,6 +3154,44 @@ validateDomainNotNullConstraint(Oid domainoid)
 		TupleTableSlot *slot;
 		TableScanDesc scan;
 		Snapshot	snapshot;
+		Form_pg_attribute attr;
+		ExprState **ri_GeneratedExprs = NULL;
+		int			attnum;
+		int			attr_virtual_generated = 0;
+
+		/* cacahe virtual generated columns handling */
+		if (tupdesc->constr && tupdesc->constr->has_generated_virtual)
+		{
+			ri_GeneratedExprs = (ExprState **) palloc0(tupdesc->natts * sizeof(ExprState *));
+
+			for (int i = 0; i < rtc->natts; i++)
+			{
+				attnum = rtc->atts[i];
+				attr = TupleDescAttr(tupdesc, attnum - 1);
+
+				if (attr->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+				{
+					NullTest   *nnulltest;
+					Expr	   *expr;
+
+					nnulltest = makeNode(NullTest);
+					nnulltest->arg = (Expr *) build_column_default(testrel, attnum);
+					nnulltest->nulltesttype = IS_NOT_NULL;
+					nnulltest->argisrow = false;
+					nnulltest->location = -1;
+
+					expr = (Expr *) nnulltest;
+					ri_GeneratedExprs[attnum - 1] = ExecPrepareExpr(expr, estate);
+					attr_virtual_generated++;
+				}
+			}
+		}
+		if (attr_virtual_generated == 0 && ri_GeneratedExprs != NULL)
+		{
+			pfree(ri_GeneratedExprs);
+			ri_GeneratedExprs = NULL;
+		}
+
 
@@ -3146,8 +3204,8 @@ validateDomainNotNullConstraint(Oid domainoid)
 
@@ -3158,14 +3216,30 @@ validateDomainNotNullConstraint(Oid domainoid)
@@ -3174,6 +3248,8 @@ validateDomainNotNullConstraint(Oid domainoid)
 
 /*
@@ -3210,6 +3286,39 @@ validateDomainCheckConstraint(Oid domainoid, const char *ccbin)
 		TupleTableSlot *slot;
 		TableScanDesc scan;
 		Snapshot	snapshot;
+		ExprState **ri_GeneratedExprs = NULL;
+		Form_pg_attribute attr;
+		int			attnum;
+		int			attr_virtual_generated = 0;
+
+		/* cacahe virtual generated columns handling */
+		if (tupdesc->constr && tupdesc->constr->has_generated_virtual)
+		{
+			ri_GeneratedExprs = (ExprState **) palloc0(tupdesc->natts * sizeof(ExprState *));
+			for (int i = 0; i < rtc->natts; i++)
+			{
+				Expr	   *gen_defexpr;
+
+				attnum = rtc->atts[i];
+				attr = TupleDescAttr(tupdesc, attnum - 1);
+				if (attr->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+				{
+					gen_defexpr = (Expr *) build_column_default(testrel, attnum);
+					if (gen_defexpr == NULL)
+						elog(ERROR, "no generation expression found for column number %d of table \"%s\"",
+							attnum, RelationGetRelationName(testrel));
+
+					ri_GeneratedExprs[attnum - 1] = ExecPrepareExprSafe(gen_defexpr, estate);
+					attr_virtual_generated++;
+				}
+			}
+		}
+		if (attr_virtual_generated == 0 && ri_GeneratedExprs != NULL)
+		{
+			pfree(ri_GeneratedExprs);
+			ri_GeneratedExprs = NULL;
+		}
+
 
@@ -3222,37 +3331,63 @@ validateDomainCheckConstraint(Oid domainoid, const char *ccbin)

 
-				conResult = ExecEvalExprSwitchContext(exprstate,
-													  econtext,
-													  &isNull);
+					Assert(ri_GeneratedExprs[attnum - 1]->escontext != NULL);
+					Assert(ri_GeneratedExprs != NULL);
 
+					conResult = ExecEvalExprSwitchContext(ri_GeneratedExprs[attnum - 1],
+					if (SOFT_ERROR_OCCURRED(ri_GeneratedExprs[attnum - 1]->escontext))
+						isNull = true;
 
index 53755ccdbad..152e2d7e357 100644

 
 /*
@@ -534,7 +564,8 @@ ExecComputeGenerated(ResultRelInfo *resultRelInfo, EState *estate,
 	bool	   *nulls;

 	/*
 	 * Initialize the expressions if we didn't already, and check whether we
@@ -550,13 +581,20 @@ ExecComputeGenerated(ResultRelInfo *resultRelInfo, EState *estate,
 	}
 	else
 	{
+		/*
+		 * we can exit earlier if the virtual generated column data type is not
+		 * doamin with constraints.
+		 */
 		if (resultRelInfo->ri_GeneratedExprsI == NULL)
 			ExecInitGenerated(resultRelInfo, estate, cmdtype);
-		/* Early exit is impossible given the prior Assert */
-		Assert(resultRelInfo->ri_NumGeneratedNeededI > 0);
+		if (resultRelInfo->ri_NumGeneratedNeededI == 0)
+			return;
 		ri_GeneratedExprs = resultRelInfo->ri_GeneratedExprsI;
 	}
 
+	if (ri_GeneratedExprs != NULL)
+		Assert(resultRelInfo->ri_NumGeneratedNeededU > 0 || resultRelInfo->ri_NumGeneratedNeededI > 0);
+
 	oldContext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
 
 	values = palloc(sizeof(*values) * natts);
@@ -574,21 +612,28 @@ ExecComputeGenerated(ResultRelInfo *resultRelInfo, EState *estate,
 			Datum		val;
 			bool		isnull;

 
-			/*
 
-			values[i] = val;
-			nulls[i] = isnull;
+				values[i] = val;
+				nulls[i] = isnull;
+			}
+			else
+			{
+				Assert(TupleDescAttr(tupdesc, i)->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL);
+				values[i] = (Datum) 0;
+				nulls[i] = true;
+			}
 		}
 		else
 		{
@@ -907,10 +952,11 @@ ExecInsert(ModifyTableContext *context,
 		slot->tts_tableOid = RelationGetRelid(resultRelInfo->ri_RelationDesc);
 
 		/*
 
@@ -1034,10 +1080,11 @@ ExecInsert(ModifyTableContext *context,
 		slot->tts_tableOid = RelationGetRelid(resultRelationDesc);
 
 		/*
 
@@ -2122,10 +2169,11 @@ ExecUpdatePrepareSlot(ResultRelInfo *resultRelInfo,
 	slot->tts_tableOid = RelationGetRelid(resultRelationDesc);
 
 	/*
-	 * Compute stored generated columns
+	 * Compute generated columns
 	 */
 	if (resultRelationDesc->rd_att->constr &&
-		resultRelationDesc->rd_att->constr->has_generated_stored)
+		(resultRelationDesc->rd_att->constr->has_generated_stored ||
+		 resultRelationDesc->rd_att->constr->has_generated_virtual))
 		ExecComputeGenerated(resultRelInfo, estate, slot,
 							 CMD_UPDATE);
 }
diff --git a/src/test/regress/expected/fast_default.out b/src/test/regress/expected/fast_default.out
index ccbcdf8403f..e1353560aba 100644
--- a/src/test/regress/expected/fast_default.out
+++ b/src/test/regress/expected/fast_default.out
@@ -70,6 +70,9 @@ NOTICE:  rewriting table has_volatile for reason 4
 -- stored generated columns need a rewrite
 ALTER TABLE has_volatile ADD col7 int GENERATED ALWAYS AS (55) stored;
 NOTICE:  rewriting table has_volatile for reason 2
+-- virtual generated columns over volatile domain constraint don't need a rewrite
+CREATE DOMAIN d1 as INT CHECK(value < random(min=>12::int, max=>21));
+ALTER TABLE has_volatile ADD col8 int GENERATED ALWAYS AS (1 + id) VIRTUAL;
 -- Test a large sample of different datatypes
 CREATE TABLE T(pk INT NOT NULL PRIMARY KEY, c_int INT DEFAULT 1);
 SELECT set('t');
@@ -922,6 +925,7 @@ DROP FUNCTION set(name);
 DROP FUNCTION comp();
 DROP TABLE m;
 DROP TABLE has_volatile;
+DROP DOMAIN d1;
 DROP EVENT TRIGGER has_volatile_rewrite;
 DROP FUNCTION log_rewrite;
 DROP SCHEMA fast_default;
diff --git a/src/test/regress/expected/generated_virtual.out b/src/test/regress/expected/generated_virtual.out
index 3b5723599f1..3ddcd1aba72 100644
--- a/src/test/regress/expected/generated_virtual.out
+++ b/src/test/regress/expected/generated_virtual.out
@@ -785,16 +785,63 @@ ERROR:  relation "gtest23p" does not exist
 --INSERT INTO gtest23q VALUES (1, 2);  -- ok
 --INSERT INTO gtest23q VALUES (2, 5);  -- error
 -- domains
-CREATE DOMAIN gtestdomain1 AS int CHECK (VALUE < 10);
-CREATE TABLE gtest24 (a int PRIMARY KEY, b gtestdomain1 GENERATED ALWAYS AS (a * 2) VIRTUAL);
-ERROR:  virtual generated column "b" cannot have a domain type
---INSERT INTO gtest24 (a) VALUES (4);  -- ok
---INSERT INTO gtest24 (a) VALUES (6);  -- error
+CREATE DOMAIN gtestdomain1 AS int CHECK (VALUE < 10) DEFAULT 12;
+CREATE TABLE gtest24 (a int UNIQUE, b gtestdomain1 GENERATED ALWAYS AS (a * 2) VIRTUAL);
+INSERT INTO gtest24 (a, b) VALUES (4, default);  -- ok
+INSERT INTO gtest24 (a) VALUES (3), (NULL);  -- ok
+INSERT INTO gtest24 (a) VALUES (6);  -- error
+ERROR:  value for domain gtestdomain1 violates check constraint "gtestdomain1_check"
+UPDATE gtest24 SET a = 6;            -- error
+ERROR:  value for domain gtestdomain1 violates check constraint "gtestdomain1_check"
+COPY gtest24 FROM stdin;  --error
+ERROR:  value for domain gtestdomain1 violates check constraint "gtestdomain1_check"
+CONTEXT:  COPY gtest24, line 1: "6"
+SELECT * FROM gtest24;
+ a | b 
+---+---
+ 4 | 8
+ 3 | 6
+   |  
+(3 rows)
+
+--ALTER TABLE ADD COLUM variant.
+ALTER TABLE gtest24 ADD COLUMN c gtestdomain1 GENERATED ALWAYS AS (a * 2) virtual not null; --error
+ERROR:  column "c" of relation "gtest24" contains null values
+ALTER TABLE gtest24 ADD COLUMN c gtestdomain1 GENERATED ALWAYS AS (a * 3) virtual check (c < 10); --error
+ERROR:  value for domain gtestdomain1 violates check constraint "gtestdomain1_check"
+ALTER TABLE gtest24 ADD COLUMN c gtestdomain1 GENERATED ALWAYS AS (a * 4) virtual; --table rewrite then error.
+ERROR:  value for domain gtestdomain1 violates check constraint "gtestdomain1_check"
+ALTER TABLE gtest24 ADD COLUMN c gtestdomain1 GENERATED ALWAYS AS (a * 2) virtual; --ok
+--alter domain add constraint variant.
+ALTER DOMAIN gtestdomain1 ADD CHECK (value IS NULL);  --error
+ERROR:  column "b" of table "gtest24" contains values that violate the new constraint
+ALTER DOMAIN gtestdomain1 ADD CHECK (value IS NOT NULL); --error
+ERROR:  column "b" of table "gtest24" contains values that violate the new constraint
+ALTER DOMAIN gtestdomain1 ADD NOT NULL; --error
+ERROR:  column "b" of table "gtest24" contains null values
+DELETE FROM gtest24 WHERE a is NULL;
+ALTER DOMAIN gtestdomain1 ADD NOT NULL; --ok
+ALTER DOMAIN gtestdomain1 ADD CHECK (value IS NOT NULL);  --ok
+ALTER DOMAIN gtestdomain1 ADD CHECK (VALUE < 7); --error
+ERROR:  column "b" of table "gtest24" contains values that violate the new constraint
+ALTER DOMAIN gtestdomain1 ADD CHECK (VALUE < 9); --ok
 CREATE TYPE gtestdomain1range AS range (subtype = gtestdomain1);
-CREATE TABLE gtest24r (a int PRIMARY KEY, b gtestdomain1range GENERATED ALWAYS AS (gtestdomain1range(a, a + 5)) VIRTUAL);
-ERROR:  virtual generated column "b" cannot have a domain type
---INSERT INTO gtest24r (a) VALUES (4);  -- ok
---INSERT INTO gtest24r (a) VALUES (6);  -- error
+CREATE TABLE gtest24r (a int PRIMARY KEY, b gtestdomain1range GENERATED ALWAYS AS (gtestdomain1range(a, a + 4)) VIRTUAL);
+INSERT INTO gtest24r (a) VALUES (4);  -- ok
+INSERT INTO gtest24r (a) VALUES (6);  -- error
+ERROR:  value for domain gtestdomain1 violates check constraint "gtestdomain1_check"
+INSERT INTO gtest24r (a) VALUES (5);  -- error
+ERROR:  value for domain gtestdomain1 violates check constraint "gtestdomain1_check2"
+CREATE TABLE gtest24v (
+  a jsonb,
+  b gtestdomain1[] GENERATED ALWAYS AS (JSON_QUERY(a, '$.a' returning gtestdomain1[] error on error)) VIRTUAL,
+  c gtestdomain1[] GENERATED ALWAYS AS (JSON_QUERY(a, '$.b' returning gtestdomain1[])) VIRTUAL not null);
+insert into gtest24v select jsonb '{"a":[6,10], "b":[6,2]}'; --error
+ERROR:  value for domain gtestdomain1 violates check constraint "gtestdomain1_check"
+insert into gtest24v select jsonb '{"a":[6,-1], "b":[6,10]}'; --error
+ERROR:  null value in column "c" of relation "gtest24v" violates not-null constraint
+DETAIL:  Failing row contains ({"a": [6, -1], "b": [6, 10]}, virtual, virtual).
+insert into gtest24v select jsonb '{"a":[6,-1], "b":[6,2]}'; --ok
 -- typed tables (currently not supported)
 CREATE TYPE gtest_type AS (f1 integer, f2 text, f3 bigint);
 CREATE TABLE gtest28 OF gtest_type (f1 WITH OPTIONS GENERATED ALWAYS AS (f2 *2) VIRTUAL);
diff --git a/src/test/regress/sql/fast_default.sql b/src/test/regress/sql/fast_default.sql
index 068dd0bc8aa..ffec1e11011 100644
--- a/src/test/regress/sql/fast_default.sql
+++ b/src/test/regress/sql/fast_default.sql
@@ -77,6 +77,9 @@ ALTER TABLE has_volatile ALTER COLUMN col1 SET DATA TYPE float8,
 -- stored generated columns need a rewrite
 ALTER TABLE has_volatile ADD col7 int GENERATED ALWAYS AS (55) stored;
 
+-- virtual generated columns over volatile domain constraint don't need a rewrite
+CREATE DOMAIN d1 as INT CHECK(value < random(min=>12::int, max=>21));
+ALTER TABLE has_volatile ADD col8 int GENERATED ALWAYS AS (1 + id) VIRTUAL;

 -- Test a large sample of different datatypes
@@ -616,6 +619,7 @@ DROP FUNCTION set(name);
 DROP FUNCTION comp();
 DROP TABLE m;
 DROP TABLE has_volatile;
+DROP DOMAIN d1;
 DROP EVENT TRIGGER has_volatile_rewrite;
 DROP FUNCTION log_rewrite;
 DROP SCHEMA fast_default;
diff --git a/src/test/regress/sql/generated_virtual.sql b/src/test/regress/sql/generated_virtual.sql
index 485e2a317fe..73e507ae8e6 100644
--- a/src/test/regress/sql/generated_virtual.sql
+++ b/src/test/regress/sql/generated_virtual.sql
@@ -439,14 +439,47 @@ CREATE TABLE gtest23q (a int PRIMARY KEY, b int REFERENCES gtest23p (y));
 --INSERT INTO gtest23q VALUES (2, 5);  -- error
 
 -- domains

From 079526f201136c32be42774b8eec5c829ab728b2 Mon Sep 17 00:00:00 2001
From: jian he <[email-masked]>
Date: Thu, 13 Mar 2025 15:05:41 +0800
Subject: [PATCH v4 3/4] soft error variant of ExecPrepareExpr, ExecInitExpr

ExecPrepareExprSafe and ExecInitExprSafe.
ExecPrepareExprSafe initialize for expression execution with soft error support.
not all expression node support it. some like CoerceToDomain do support it.

XXX more comments.

discussion: https://postgr.es/m/[email-masked]
---
 src/backend/executor/execExpr.c | 63 +++++++++++++++++++++++++++++++++
 src/include/executor/executor.h |  2 ++
 2 files changed, 65 insertions(+)

diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c
index f1569879b52..b8f5f647281 100644
--- a/src/backend/executor/execExpr.c
+++ b/src/backend/executor/execExpr.c
@@ -170,6 +170,46 @@ ExecInitExpr(Expr *node, PlanState *parent)
 	return state;
 }
 
+/*
+ * ExecInitExpr: soft error variant of ExecInitExpr.
+ * use it only for expression nodes support soft errors, not all expression
+ * nodes support it.
+*/
+ExprState *
+ExecInitExprSafe(Expr *node, PlanState *parent)
+{
+	ExprState  *state;
+	ExprEvalStep scratch = {0};
+
+	/* Special case: NULL expression produces a NULL ExprState pointer */
+	if (node == NULL)
+		return NULL;
+
+	/* Initialize ExprState with empty step list */
+	state = makeNode(ExprState);
+	state->expr = node;
+	state->parent = parent;
+	state->ext_params = NULL;
+	state->escontext = makeNode(ErrorSaveContext);
+	state->escontext->type = T_ErrorSaveContext;
+	state->escontext->error_occurred = false;
+	state->escontext->details_wanted = true;
+
+	/* Insert setup steps as needed */
+	ExecCreateExprSetupSteps(state, (Node *) node);
+
+	/* Compile the expression proper */
+	ExecInitExprRec(node, state, &state->resvalue, &state->resnull);
+
+	/* Finally, append a DONE step */
+	scratch.opcode = EEOP_DONE_RETURN;
+	ExprEvalPushStep(state, &scratch);
+
+	ExecReadyExpr(state);
+
+	return state;
+}
+
 /*
  * ExecInitExprWithParams: prepare a standalone expression tree for execution
  *
@@ -778,6 +818,29 @@ ExecPrepareExpr(Expr *node, EState *estate)
 	return result;
 }
 
+/*
+ * ExecPrepareExprSafe: soft error variant of ExecPrepareExpr.
+ *
+ * use it when expression node *support* soft error expression execution.
+ * ExecPrepareExpr comments apply to here too.
+ */
+ExprState *
+ExecPrepareExprSafe(Expr *node, EState *estate)
+{
+	ExprState  *result;
+	MemoryContext oldcontext;
+
+	oldcontext = MemoryContextSwitchTo(estate->es_query_cxt);
+
+	node = expression_planner(node);
+
+	result = ExecInitExprSafe(node, NULL);
+
+	MemoryContextSwitchTo(oldcontext);
+
+	return result;
+}
+
 /*
  * ExecPrepareQual --- initialize for qual execution outside a normal
  * Plan tree context.
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 4063ed68f5f..28e7df4f336 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -309,6 +309,7 @@ ExecProcNode(PlanState *node)
  * prototypes from functions in execExpr.c
  */
 extern ExprState *ExecInitExpr(Expr *node, PlanState *parent);
+extern ExprState *ExecInitExprSafe(Expr *node, PlanState *parent);
 extern ExprState *ExecInitExprWithParams(Expr *node, ParamListInfo ext_params);
 extern ExprState *ExecInitQual(List *qual, PlanState *parent);
 extern ExprState *ExecInitCheck(List *qual, PlanState *parent);
@@ -357,6 +358,7 @@ extern ProjectionInfo *ExecBuildUpdateProjection(List *targetList,
 												 TupleTableSlot *slot,
 												 PlanState *parent);
 extern ExprState *ExecPrepareExpr(Expr *node, EState *estate);
+extern ExprState *ExecPrepareExprSafe(Expr *node, EState *estate);
 extern ExprState *ExecPrepareQual(List *qual, EState *estate);
 extern ExprState *ExecPrepareCheck(List *qual, EState *estate);
 extern List *ExecPrepareExprList(List *nodes, EState *estate);
-- 
2.34.1

From 3541711e18717dd5d2c93c7e2521249b01d6db7e Mon Sep 17 00:00:00 2001
From: jian he <[email-masked]>
Date: Wed, 12 Mar 2025 22:07:57 +0800
Subject: [PATCH v4 1/4] not null for virtual generated column

 
 			/*
 
 		/*

 	/*
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 18ff8956577..b8d82ba2809 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6092,6 +6092,7 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap)
 	TupleDesc	newTupDesc;
 	bool		needscan = false;
 	List	   *notnull_attrs;
+	List	   *all_virtual_nns = NIL;
 	int			i;
 	ListCell   *l;
 	EState	   *estate;
@@ -6195,6 +6196,16 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap)
 			needscan = true;
 	}
 
@@ -6205,6 +6216,22 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap)
+		ResultRelInfo *rInfo	= NULL;
 
 		if (newrel)
 			ereport(DEBUG1,
@@ -6287,6 +6314,7 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap)
 		while (table_scan_getnextslot(scan, ForwardScanDirection, oldslot))
 		{
 			TupleTableSlot *insertslot;
+			bool	virtual_notnull_check = false;
 
 			if (tab->rewrite > 0)
 			{
@@ -6378,6 +6406,14 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap)
 				{
 					Form_pg_attribute attr = TupleDescAttr(newTupDesc, attn);
 
@@ -6387,6 +6423,27 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap)
 				}
 			}
 
@@ -7836,14 +7893,6 @@ ATExecSetNotNull(List **wqueue, Relation rel, char *conName, char *colName,
 
@@ -8512,6 +8561,9 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,

+/*

 

 
 	/*
index 0d2ffabda68..4063ed68f5f 100644
index 575b0b1bd24..36b4b497a2d 100644
 
index dc09c85938e..3b5723599f1 100644

 

 -- index constraints

---

From: Álvaro Herrera <[email-masked]>
Date: Wed, 19 Mar 2025 17:19:24 +0100

> minor change to fix the compiler warning.

I gave a look at 0001, and I propose some fixes.  0001 is just a typo in
an error message.  0002 is pgindent.  Then 0003 contain some more
interesting bits:

- in ExecRelCheckGenVirtualNotNull() it seems more natural to an
  AttrNumber rather than int, and use the InvalidAttrNumber value rather
  than -1 to indicate that all columns are ok.  Also, use
  foreach_current_index instead of manually counting the loop.

- ATRewriteTable can be simplified if we no longer add the virtual
  generated columns with not nulls to the notnulls_attrs list, but instead 
  we store the attnums in a separate list.  That way, the machinery
  around ExecRelCheckGenVirtualNotNull made less convoluted.

- in ExecConstraints, you removed a comment (which ended up in
  NotNullViolationErrorReport), which was OK as far as that went; but
  there was another comment a few lines below which said "See the
  comment above", which no longer made sense.  To fix it, I duplicated
  the original comment in the place where "See the..." comment was.

- renamed NotNullViolationErrorReport to ReportNotNullViolationError.
  Perhaps a better name is still possible -- something in line with
  ExecPartitionCheckEmitError?  (However, that function is exported,
  while the new one is not. So we probably don't need an "Exec" prefix
  for it.  I don't particularly like that name very much anyway.)

- Reduce scope of variables all over the place.

- Added a bunch more comments.

Other comments:

* The block in ATRewriteTable that creates a resultRelInfo for
  ExecRelCheckGenVirtualNotNull needs an explanation.

* I suspect the locations for the new functions were selected with
  the help of a dartboard.  ExecRelCheckGenVirtualNotNull() in
  particular looks like it could use a better location.  Maybe it's
  better right after ExecConstraints, and ReportNotNullViolationError
  (or whatever we name it) can go after it.

* I don't find the name all_virtual_nns particularly appropriate.  Maybe
  virtual_notnull_attrs?

* There are some funny rules around nulls on rowtypes.  I think allowing
  this tuple is wrong (and I left an XXX comment near where the 'argisrow'
  flag is set):

create type twoints as (a int, b int);
create table foo (a int, b int, c twoints generated always as (row(a,b)::twoints) not null);
insert into foo values (null, null);

I don't remember exactly what the rules are though so I may be wrong.

-- 
Álvaro Herrera               48°01'N 7°57'E  —  https://www.EnterpriseDB.com/
"Having your biases confirmed independently is how scientific progress is
made, and hence made our great society what it is today" (Mary Gardiner)

---

From: jian he <[email-masked]>
Date: Thu, 20 Mar 2025 13:18:09 +0800

>   ExecRelCheckGenVirtualNotNull needs an explanation.
I tried my best.
here are the comments above the line ``if (notnull_virtual_attrs != NIL)``

        /*
         * Whether change an existing generation expression or adding a new
         * not-null virtual generated column, we need to evaluate whether the
         * generation expression is null.
         * In ExecConstraints, we use ExecRelCheckGenVirtualNotNull do the job.
         * Here, we can also utilize ExecRelCheckGenVirtualNotNull.
         * To achieve this, we need create a dummy ResultRelInfo.  Ensure that
         * the resultRelationIndex of the dummy ResultRelInfo is set to 0.
        */

+ /*
+ * XXX this deserves an explanation. Also, is rInfo a good variable
+ * name?
+ */
there are other places using this variable name: "rInfo", so i use
these names....

ATRewriteTable:
        for (i = 0; i < newTupDesc->natts; i++)
        {
            Form_pg_attribute attr = TupleDescAttr(newTupDesc, i);
            if (attr->attnotnull && !attr->attisdropped)
            {
                if (attr->attgenerated != ATTRIBUTE_GENERATED_VIRTUAL)
                    notnull_attrs = lappend_int(notnull_attrs, i);
                else
                    notnull_virtual_attrs = lappend_int(notnull_virtual_attrs,
                                                        attr->attnum);
            }
        }
this is kind of ugly? notnull_virtual_attrs is 1 based, notnull_attrs
is 0 based.
I want to change it all to 1 based. see v5-0002

>   (or whatever we name it) can go after it.

I thought ExecRelCheckGenVirtualNotNull is very similar to ExecRelCheck,
that's why I put it close to ExecRelCheck.
putting it right after ExecConstraints is ok for me.

> * I don't find the name all_virtual_nns particularly appropriate.  Maybe
in ATRewriteTable we already have "notnull_virtual_attrs"
we can rename it to notnull_virtual_attrs

> I don't remember exactly what the rules are though so I may be wrong.
argisrow should set to false.
i think you mean the special value ``'(,)'``

create table t(a twoints not null);
insert into t select '(,)' returning a is null; --return true;
create table t1(a twoints not null generated always as ('(,)'::twoints));
insert into t1 default values returning a is null; --should return true.

i think you mean this thread:
https://postgr.es/m/[email-masked]
should i put a test into generated_virtual.sql?

+ * We implement this by consing up a NullTest node for each virtual
trivial question.
 I googled, and still found any explanation of the word "consing up".

---

From: Xuneng Zhou <[email-masked]>
Date: Fri, 21 Mar 2025 12:33:52 +0800

Hi,

response from ChatGPT, seems correct:

"Consing up" is an informal term derived from Lisp terminology. In this
context, it means dynamically creating (allocating and constructing) a new
NullTest node. Instead of reusing an existing node, the code allocates a
fresh node—using PostgreSQL’s memory allocation (palloc) functions—and
fills in its fields (like the generation expression, null test type, etc.).
This new node is then prepared (compiled) for execution with
ExecPrepareExpr and used to evaluate whether the generated expression
returns a non-NULL value, thus enforcing the NOT NULL constraint on virtual
generated columns.

---

From: Álvaro Herrera <[email-masked]>
Date: Fri, 21 Mar 2025 11:18:00 +0100

> I want to change it all to 1 based. see v5-0002

Yeah, this inconsistency bothered me too.  I have pushed your 0002 now,
which means your 0001 needs a small rebase.  I'm going to leave 0001 for
Peter to commit, as it's mostly his turf.

>  I googled, and still found any explanation of the word "consing up".

https://www.pc-freak.net/files/the-hacker-disctionary.html

You could change "cons up" to "manufacture" in that comment and get
about the same meaning.  Maybe have a look at
  git grep -C3 -E 'cons(|ing|ed) up'

I think the ChatGPT answer quoted by Zhou is roughly on point.

-- 
Álvaro Herrera         PostgreSQL Developer  —  https://www.EnterpriseDB.com/
"El que vive para el futuro es un iluso, y el que vive para el pasado,
un imbécil" (Luis Adler, "Los tripulantes de la noche")

---

From: jian he <[email-masked]>
Date: Mon, 24 Mar 2025 11:26:50 +0800

hi.

rebase, and some minor code comments change.
From 0af096b3959cc6f146bbe8a54a018c0e69beff2e Mon Sep 17 00:00:00 2001
From: jian he <[email-masked]>
Date: Mon, 24 Mar 2025 11:22:54 +0800
Subject: [PATCH v6 1/1] not null for virtual generated column
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

now we can add not null constraint on virtual generated column.
virtual generated colunm the storage is still null.
but not null constraint on virtual generated column make sure evaulation of the
expanded generated expression does not yield null.

reviewed by: Xuneng Zhou <[email-masked]>,
reviewed by: Navneet Kumar <[email-masked]>,
reviewed by: Álvaro Herrera <[email-masked]>

discussion: https://postgr.es/m/[email-masked]
---
 src/backend/catalog/heap.c                    |  10 -
 src/backend/commands/indexcmds.c              |  10 +-
 src/backend/commands/tablecmds.c              |  74 +++++-
 src/backend/executor/execMain.c               | 245 ++++++++++++++----
 src/backend/parser/parse_utilcmd.c            |  14 -
 src/include/executor/executor.h               |   4 +
 src/include/nodes/execnodes.h                 |   3 +
 .../regress/expected/generated_virtual.out    |  93 +++++--
 src/test/regress/sql/generated_virtual.sql    |  54 +++-
 9 files changed, 384 insertions(+), 123 deletions(-)

 			/*
 
 		/*
 		 * A column can only have one not-null constraint, so discard any
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index 89cc83e8843..33c2106c17c 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -1118,10 +1118,12 @@ DefineIndex(Oid tableId,
 
+					errmsg("primary keys on virtual generated columns are not supported") :
 
 	/*
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 1202544ebd0..503be7e0f7e 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6092,6 +6092,7 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap)
 	TupleDesc	newTupDesc;
 	bool		needscan = false;
 	List	   *notnull_attrs;
+	List	   *notnull_virtual_attrs;
 	int			i;
 	ListCell   *l;
 	EState	   *estate;
@@ -6176,22 +6177,32 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap)
 		ex->exprstate = ExecInitExpr((Expr *) ex->expr, NULL);
 	}
 
-	notnull_attrs = NIL;
+	notnull_attrs = notnull_virtual_attrs = NIL;
 	if (newrel || tab->verify_new_notnull)
 	{
 		/*
 		 * If we are rebuilding the tuples OR if we added any new but not
 		 * verified not-null constraints, check all not-null constraints. This
 		 * is a bit of overkill but it minimizes risk of bugs.
+		 *
+		 * notnull_attrs does *not* collect attribute numbers for not-null
+		 * constraints over virtual generated columns; instead, they are
+		 * collected in notnull_virtual_attrs.
 		 */
 		for (i = 0; i < newTupDesc->natts; i++)
 		{
 			Form_pg_attribute attr = TupleDescAttr(newTupDesc, i);
 
 			if (attr->attnotnull && !attr->attisdropped)
-				notnull_attrs = lappend_int(notnull_attrs, attr->attnum);
+			{
+				if (attr->attgenerated != ATTRIBUTE_GENERATED_VIRTUAL)
+					notnull_attrs = lappend_int(notnull_attrs, attr->attnum);
+				else
+					notnull_virtual_attrs = lappend_int(notnull_virtual_attrs,
+														attr->attnum);
+			}
 		}
-		if (notnull_attrs)
+		if (notnull_attrs || notnull_virtual_attrs)
 			needscan = true;
 	}
 
@@ -6205,6 +6216,32 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap)
 		List	   *dropped_attrs = NIL;
 		ListCell   *lc;
 		Snapshot	snapshot;
+		ResultRelInfo *rInfo = NULL;
+
+		/*
+		 * Whether change an existing generation expression or adding a new
+		 * not-null virtual generated column, we need to evaluate whether the
+		 * generation expression is null.
+		 * In ExecConstraints, we use ExecRelCheckGenVirtualNotNull do the job.
+		 * Here, we can also utilize ExecRelCheckGenVirtualNotNull too.
+		 * To achieve this, we need create a dummy ResultRelInfo.  Ensure that
+		 * the resultRelationIndex of the dummy ResultRelInfo is set to 0.
+		*/
+		if (notnull_virtual_attrs != NIL)
+		{
+			MemoryContext oldcontext;
+
+			Assert(newTupDesc->constr->has_generated_virtual);
+			Assert(newTupDesc->constr->has_not_null);
+			oldcontext = MemoryContextSwitchTo(estate->es_query_cxt);
+			rInfo = makeNode(ResultRelInfo);
+			InitResultRelInfo(rInfo,
+							  oldrel,
+							  0,	/* dummy rangetable index */
+							  NULL,
+							  estate->es_instrument);
+			MemoryContextSwitchTo(oldcontext);
+		}
 
 		if (newrel)
 			ereport(DEBUG1,
@@ -6385,6 +6422,26 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap)
 				}
 			}
 
+			if (notnull_virtual_attrs != NIL)
+			{
+				AttrNumber	attnum;
+
+				attnum = ExecRelCheckGenVirtualNotNull(rInfo, insertslot,
+													   estate,
+													   notnull_virtual_attrs);
+				if (attnum != InvalidAttrNumber)
+				{
+					Form_pg_attribute attr = TupleDescAttr(newTupDesc, attnum - 1);
+
+					ereport(ERROR,
+							errcode(ERRCODE_NOT_NULL_VIOLATION),
+							errmsg("column \"%s\" of relation \"%s\" contains null values",
+								   NameStr(attr->attname),
+								   RelationGetRelationName(oldrel)),
+							errtablecol(oldrel, attnum));
+				}
+			}
+
 			foreach(l, tab->constraints)
 			{
 				NewConstraint *con = lfirst(l);
@@ -7834,14 +7891,6 @@ ATExecSetNotNull(List **wqueue, Relation rel, char *conName, char *colName,
 				 errmsg("cannot alter system column \"%s\"",
 						colName)));
 
@@ -8510,6 +8559,9 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,
 
+	if (attgenerated == ATTRIBUTE_GENERATED_VIRTUAL && attTup->attnotnull)
+		tab->verify_new_notnull = true;
+
 	/*
 	 * We need to prevent this because a change of expression could affect a
 	 * row filter and inject expressions that are not permitted in a row
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index e9bd98c7738..f90fc001185 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -51,6 +51,7 @@
 #include "foreign/fdwapi.h"
 #include "mb/pg_wchar.h"
 #include "miscadmin.h"
+#include "nodes/makefuncs.h"
 #include "nodes/queryjumble.h"
 #include "parser/parse_relation.h"
 #include "pgstat.h"
@@ -92,7 +93,9 @@ static bool ExecCheckPermissionsModified(Oid relOid, Oid userid,
 										 AclMode requiredPerms);
 static void ExecCheckXactReadOnly(PlannedStmt *plannedstmt);
 static void EvalPlanQualStart(EPQState *epqstate, Plan *planTree);
-
+static void ReportNotNullViolationError(ResultRelInfo *resultRelInfo,
+										TupleTableSlot *slot,
+										EState *estate, int attrChk);
 /* end of local decls */

@@ -1372,6 +1375,7 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
 	resultRelInfo->ri_FdwState = NULL;
 	resultRelInfo->ri_usesFdwDirectModify = false;
 	resultRelInfo->ri_ConstraintExprs = NULL;
+	resultRelInfo->ri_GeneratedNotNullExprs = NULL;
 	resultRelInfo->ri_GeneratedExprsI = NULL;
 	resultRelInfo->ri_GeneratedExprsU = NULL;
 	resultRelInfo->ri_projectReturning = NULL;
@@ -2058,73 +2062,61 @@ ExecConstraints(ResultRelInfo *resultRelInfo,
 	TupleDesc	tupdesc = RelationGetDescr(rel);
 	TupleConstr *constr = tupdesc->constr;
 	Bitmapset  *modifiedCols;
+	List	   *notnull_virtual_attrs = NIL;
+	bool		virtual_notnull_check = false;

+	/*
+	 * First, go over all the not-null constraints and verify and possibly
+	 * throw errors for all of those that aren't on virtual generated columns.
+	 * (The latter need executor support and a precise list of columns to
+	 * handle, so we accumulate that here and initialize separately.)
+	 */
 	if (constr->has_not_null)
 	{
-		int			natts = tupdesc->natts;
-		int			attrChk;
-
-		for (attrChk = 1; attrChk <= natts; attrChk++)
+		for (AttrNumber attnum = 1; attnum <= tupdesc->natts; attnum++)
 		{
-			Form_pg_attribute att = TupleDescAttr(tupdesc, attrChk - 1);
+			Form_pg_attribute att = TupleDescAttr(tupdesc, attnum - 1);
 
+				notnull_virtual_attrs = lappend_int(notnull_virtual_attrs, att->attnum);
 
-				ereport(ERROR,
+				ReportNotNullViolationError(resultRelInfo, slot, estate, attnum);
 			}
 		}
 	}
 
+	/* Check not-null constraints on virtual generated column, if any */
+	if (virtual_notnull_check)
+	{
+		AttrNumber	attnum;
+
+		Assert(constr->has_not_null);
+		Assert(constr->has_generated_virtual);
+		Assert(notnull_virtual_attrs != NIL);
+
+		attnum = ExecRelCheckGenVirtualNotNull(resultRelInfo, slot, estate,
+											   notnull_virtual_attrs);
+		if (attnum != InvalidAttrNumber)
+		{
+			Form_pg_attribute att = TupleDescAttr(tupdesc, attnum - 1);
+
+			Assert(att->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL);
+			ReportNotNullViolationError(resultRelInfo, slot, estate, att->attnum);
+		}
+	}
+
+	/* Last, verify any CHECK constraints */
 	if (rel->rd_rel->relchecks > 0)
 	{
 		const char *failed;
@@ -2134,7 +2126,12 @@ ExecConstraints(ResultRelInfo *resultRelInfo,
 			char	   *val_desc;
 			Relation	orig_rel = rel;
 
-			/* See the comment above. */
+			/*
+			 * If the tuple has been routed, it's been converted to the
+			 * partition's rowtype, which might differ from the root table's.
+			 * We must convert it back to the root table's rowtype so that
+			 * val_desc shown error message matches the input tuple.
+			 */
 			if (resultRelInfo->ri_RootResultRelInfo)
 			{
 				ResultRelInfo *rootrel = resultRelInfo->ri_RootResultRelInfo;
@@ -2176,6 +2173,146 @@ ExecConstraints(ResultRelInfo *resultRelInfo,
 	}
 }
 
+/*
+ * Verify not-null constraints on virtual generated columns of the given
+ * tuple slot.
+ *
+ * Return value of InvalidAttrNumber means all not-null constraints on virtual
+ * generated columns are satisfied.  A return value > 0 means a not-null
+ * violation happened for that attribute.
+ *
+ * notnull_virtual_attrs is the list of the attnums of virtual generated column with
+ * not-null constraints.
+ */
+AttrNumber
+ExecRelCheckGenVirtualNotNull(ResultRelInfo *resultRelInfo, TupleTableSlot *slot,
+							  EState *estate, List *notnull_virtual_attrs)
+{
+	Relation	rel = resultRelInfo->ri_RelationDesc;
+	ExprContext *econtext;
+	MemoryContext oldContext;
+
+	/*
+	 * We implement this by consing up a NullTest node for each virtual
+	 * generated column, which we cache in resultRelInfo, and running those
+	 * through ExecCheck.
+	 */
+	if (resultRelInfo->ri_GeneratedNotNullExprs == NULL)
+	{
+		int			cnt = list_length(notnull_virtual_attrs);
+
+		oldContext = MemoryContextSwitchTo(estate->es_query_cxt);
+		resultRelInfo->ri_GeneratedNotNullExprs =
+			(ExprState **) palloc0(cnt * sizeof(ExprState *));
+
+		foreach_int(attnum, notnull_virtual_attrs)
+		{
+			int			i = foreach_current_index(attnum);
+			NullTest   *nnulltest;
+
+			/* "generated_expression IS NOT NULL" check. */
+			nnulltest = makeNode(NullTest);
+			nnulltest->arg = (Expr *) build_generation_expression(rel, attnum);
+			nnulltest->nulltesttype = IS_NOT_NULL;
+			nnulltest->argisrow = false;
+			nnulltest->location = -1;
+
+			resultRelInfo->ri_GeneratedNotNullExprs[i] =
+				ExecPrepareExpr((Expr *) nnulltest, estate);
+		}
+		MemoryContextSwitchTo(oldContext);
+	}
+
+	/*
+	 * We will use the EState's per-tuple context for evaluating virtual
+	 * generated column not null constraint expressions (creating it if it's
+	 * not already there).
+	 */
+	econtext = GetPerTupleExprContext(estate);
+
+	/* Arrange for econtext's scan tuple to be the tuple under test */
+	econtext->ecxt_scantuple = slot;
+
+	/* And evaluate the check constraints for virtual generated column */
+	foreach_int(attnum, notnull_virtual_attrs)
+	{
+		int			i = foreach_current_index(attnum);
+		ExprState  *exprstate = resultRelInfo->ri_GeneratedNotNullExprs[i];
+
+		Assert(exprstate != NULL);
+		if (!ExecCheck(exprstate, econtext))
+			return attnum;
+	}
+
+	/* InvalidAttrNumber result means no error */
+	return InvalidAttrNumber;
+}
+
+/*
+ * Report a violation of a not-null constraint that was already detected.
+ */
+static void
+ReportNotNullViolationError(ResultRelInfo *resultRelInfo, TupleTableSlot *slot,
+							EState *estate, int attrChk)
+{
+	Bitmapset  *modifiedCols;
+	char	   *val_desc;
+
+	Relation	rel = resultRelInfo->ri_RelationDesc;
+	Relation	orig_rel = rel;
+
+	TupleDesc	tupdesc = RelationGetDescr(rel);
+	TupleDesc	orig_tupdesc = RelationGetDescr(rel);
+	Form_pg_attribute att = TupleDescAttr(tupdesc, attrChk - 1);
+
+	Assert(attrChk > 0);
+
+	/*
+	 * If the tuple has been routed, it's been converted to the partition's
+	 * rowtype, which might differ from the root table's.  We must convert it
+	 * back to the root table's rowtype so that val_desc shown error message
+	 * matches the input tuple.
+	 */
+	if (resultRelInfo->ri_RootResultRelInfo)
+	{
+		ResultRelInfo *rootrel = resultRelInfo->ri_RootResultRelInfo;
+		AttrMap    *map;
+
+		tupdesc = RelationGetDescr(rootrel->ri_RelationDesc);
+		/* a reverse map */
+		map = build_attrmap_by_name_if_req(orig_tupdesc,
+										   tupdesc,
+										   false);
+
+		/*
+		 * Partition-specific slot's tupdesc can't be changed, so allocate a
+		 * new one.
+		 */
+		if (map != NULL)
+			slot = execute_attr_map_slot(map, slot,
+										 MakeTupleTableSlot(tupdesc, &TTSOpsVirtual));
+		modifiedCols = bms_union(ExecGetInsertedCols(rootrel, estate),
+								 ExecGetUpdatedCols(rootrel, estate));
+		rel = rootrel->ri_RelationDesc;
+	}
+	else
+		modifiedCols = bms_union(ExecGetInsertedCols(resultRelInfo, estate),
+								 ExecGetUpdatedCols(resultRelInfo, estate));
+
+	val_desc = ExecBuildSlotValueDescription(RelationGetRelid(rel),
+											 slot,
+											 tupdesc,
+											 modifiedCols,
+											 64);
+	ereport(ERROR,
+			errcode(ERRCODE_NOT_NULL_VIOLATION),
+			errmsg("null value in column \"%s\" of relation \"%s\" violates not-null constraint",
+				   NameStr(att->attname),
+				   RelationGetRelationName(orig_rel)),
+			val_desc ? errdetail("Failing row contains %s.", val_desc) : 0,
+			errtablecol(orig_rel, attrChk));
+}
+
 /*
  * ExecWithCheckOptions -- check that tuple satisfies any WITH CHECK OPTIONs
  * of the specified kind.
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 896a7f2c59b..9c1541e1fea 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -988,20 +988,6 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column)
 							column->colname, cxt->relation->relname),
 					 parser_errposition(cxt->pstate,
 										constraint->location)));
-
-		/*
-		 * TODO: Straightforward not-null constraints won't work on virtual
-		 * generated columns, because there is no support for expanding the
-		 * column when the constraint is checked.  Maybe we could convert the
-		 * not-null constraint into a full check constraint, so that the
-		 * generation expression can be expanded at check time.
-		 */
-		if (column->is_not_null && column->generated == ATTRIBUTE_GENERATED_VIRTUAL)
-			ereport(ERROR,
-					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-					 errmsg("not-null constraints are not supported on virtual generated columns"),
-					 parser_errposition(cxt->pstate,
-										constraint->location)));
 	}
 
 	/*
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 0db5d18ba22..7a2525e8ce1 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -220,6 +220,10 @@ extern ResultRelInfo *ExecGetTriggerResultRel(EState *estate, Oid relid,
 extern List *ExecGetAncestorResultRels(EState *estate, ResultRelInfo *resultRelInfo);
 extern void ExecConstraints(ResultRelInfo *resultRelInfo,
 							TupleTableSlot *slot, EState *estate);
+extern AttrNumber ExecRelCheckGenVirtualNotNull(ResultRelInfo *resultRelInfo,
+												TupleTableSlot *slot,
+												EState *estate,
+												List *notnull_virtual_attrs);
 extern bool ExecPartitionCheck(ResultRelInfo *resultRelInfo,
 							   TupleTableSlot *slot, EState *estate, bool emitError);
 extern void ExecPartitionCheckEmitError(ResultRelInfo *resultRelInfo,
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index d4d4e655180..6159b8a3c07 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -549,6 +549,9 @@ typedef struct ResultRelInfo
 	/* array of constraint-checking expr states */
 	ExprState **ri_ConstraintExprs;
 
index dc09c85938e..98108191be1 100644
@@ -664,28 +664,72 @@ INSERT INTO gtest20c VALUES (1);  -- ok
+CREATE TABLE gtestnn_parent(
+            f1 int,
+            f2 bigint,
+            f3 bigint GENERATED ALWAYS AS (nullif(f1, 1) + nullif(f2, 10)) VIRTUAL not null)
+            PARTITION BY RANGE (f1);
+INSERT INTO gtestnn_parent VALUES (10, 11, default); --ok
+SELECT * FROM gtestnn_parent ORDER BY f1, f2, f3;
@@ -693,7 +737,7 @@ ERROR:  unique constraints on virtual generated columns are not supported
+ERROR:  primary keys on virtual generated columns are not supported
@@ -738,7 +782,7 @@ ERROR:  foreign key constraints on virtual generated columns are not supported
+ERROR:  primary keys on virtual generated columns are not supported
@@ -1029,7 +1073,7 @@ CREATE TABLE gtest27 (
@@ -1047,7 +1091,8 @@ SELECT * FROM gtest27;
 ---+----+----
  3 |  7 | 20
  4 | 11 | 30
-(2 rows)
+   |    |   
+(3 rows)
 
@@ -1056,6 +1101,14 @@ LINE 1: ALTER TABLE gtest27 ALTER COLUMN x TYPE boolean USING x <> 0...
index dab8c92ef99..6624d0b22eb 100644
@@ -335,23 +335,50 @@ ALTER TABLE gtest20c ADD CONSTRAINT whole_row_check CHECK (gtest20c IS NOT NULL)

 
+CREATE TABLE gtestnn_parent(
+            f1 int,
+            f2 bigint,
+            f3 bigint GENERATED ALWAYS AS (nullif(f1, 1) + nullif(f2, 10)) VIRTUAL not null)
+            PARTITION BY RANGE (f1);
+INSERT INTO gtestnn_parent VALUES (10, 11, default); --ok
+SELECT * FROM gtestnn_parent ORDER BY f1, f2, f3;
 
 -- index constraints
@@ -524,13 +551,20 @@ CREATE TABLE gtest27 (

---

From: Peter Eisentraut <[email-masked]>
Date: Fri, 28 Mar 2025 15:06:16 +0100

> rebase, and some minor code comments change.

I have committed this.

I did a bit more editing on the naming of various things.  In terms of 
functionality, in ExecConstraints() notnull_virtual_attrs and 
virtual_notnull_check were redundant, so I simplified that.  I also cut 
back a little bit on the assertions.  They were all ok, but I felt in 
some places they bloated the code quite a bit for no real gain.

---

From: jian he <[email-masked]>
Date: Mon, 31 Mar 2025 15:26:10 +0800

> > rebase, and some minor code comments change.
In an earlier thread, I also posted a patch for supporting virtual
generated columns over domain type.
The code is somehow similar,
so I post the remaining patch to this thread.

v7-0001
we need to compute the generation expression for the domain with constraints,
thus rename ExecComputeStoredGenerated to ExecComputeGenerated.

v7-0002
soft error variant of ExecPrepareExpr, ExecInitExpr.
for soft error processing of CoerceToDomain.
we don't want error messages like
"value for domain d2 violates check constraint "d2_check""
while validating existing domain data,
we want something like:
+ERROR:  column "b" of table "gtest24" contains values that violate
the new constraint

v7-0003
supports virtual generation columns over domain type.
If the domain has constraints then we need to compute the virtual
generation expression,
that happens mainly within ExecComputeGenerated.

if a virtual generation column type is domain_with_constraint, then
ALTER DOMAIN ADD CONSTRAINTS need to revalidate these virtual
generation expressions again.
so in validateDomainCheckConstraint, validateDomainNotNullConstraint
We need to fetch the generation expression (build_generation_expression),
compile the generation expression (ExecPrepareExprSafe),
and evaluate it (ExecEvalExprSwitchContext).

I also posted patch summary earlier at [1]
[1] https://postgr.es/m/[email-masked]
From 346dbce94c008a1ec9447ca65c7b7fcd0f8428d6 Mon Sep 17 00:00:00 2001
From: jian he <[email-masked]>
Date: Mon, 31 Mar 2025 15:02:46 +0800
Subject: [PATCH v7 3/3] domain over virtual generated column.

domains that don't have constraints work just fine.
domain constraints check happen within ExecComputeGenerated.
we compute the virtual generated column in ExecComputeGenerated and discarded the value.
if value cannot coerce to domain then we will error out.

discussion: https://postgr.es/m/[email-masked]
---
 src/backend/catalog/heap.c                    |  13 +-
 src/backend/commands/copyfrom.c               |   5 +-
 src/backend/commands/tablecmds.c              |  48 +++-
 src/backend/commands/typecmds.c               | 209 +++++++++++++++---
 src/backend/executor/nodeModifyTable.c        |  88 ++++++--
 src/include/catalog/heap.h                    |   1 -
 src/test/regress/expected/fast_default.out    |   4 +
 .../regress/expected/generated_virtual.out    |  65 +++++-
 src/test/regress/sql/fast_default.sql         |   4 +
 src/test/regress/sql/generated_virtual.sql    |  47 +++-
 10 files changed, 398 insertions(+), 86 deletions(-)

diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index b807ab66668..5519690d85c 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -508,7 +508,7 @@ CheckAttributeNamesTypes(TupleDesc tupdesc, char relkind,
 						   TupleDescAttr(tupdesc, i)->atttypid,
 						   TupleDescAttr(tupdesc, i)->attcollation,
 						   NIL, /* assume we're creating a new rowtype */
-						   flags | (TupleDescAttr(tupdesc, i)->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL ? CHKATYPE_IS_VIRTUAL : 0));
+						   flags);
 	}
 }
 
index 906b6581e11..0086e4b2936 100644
 
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 10624353b0a..4acae8d5568 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -5989,7 +5989,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * rebuild data.
 			 */
 			if (tab->constraints != NIL || tab->verify_new_notnull ||
-				tab->partition_constraint != NULL)
+				tab->newvals || tab->partition_constraint != NULL)
 				ATRewriteTable(tab, InvalidOid);
 
 			/*
@@ -6109,6 +6109,7 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap)
 	BulkInsertState bistate;
 	int			ti_options;
 	ExprState  *partqualstate = NULL;
+	List		*domain_virtual_attrs = NIL;
 
 	/*
 	 * Open the relation(s).  We have surely already locked the existing
@@ -6184,6 +6185,16 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap)
 
+				domain_virtual_attrs = lappend_int(domain_virtual_attrs, attr->attnum);
+			}
+		}
 	}
 
 	notnull_attrs = notnull_virtual_attrs = NIL;
@@ -6215,6 +6226,13 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap)
 			needscan = true;
 	}
 
+	/*
+	 * We need verify domain constraints on the virtual generated column are
+	 * satisfied, which requires table scan.
+	*/
+	if (domain_virtual_attrs != NIL)
+		needscan = true;
+
 	if (newrel || needscan)
 	{
 		ExprContext *econtext;
@@ -6473,6 +6491,34 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap)
 				}
 			}
 
+			/*
+			 * domain_virtual_attrs is list of newly added columns that is virtual
+			 * generated column over domain with constraints. we need to check
+			 * whether these domain constraints are satisfied.
+			*/
+			if (domain_virtual_attrs != NIL)
+			{
+				Assert(tab->rewrite == 0);
+
+				foreach(l, tab->newvals)
+				{
+					NewColumnValue *ex = lfirst(l);
+
+					if (!ex->is_generated)
+						continue;
+
+					if(list_member_int(domain_virtual_attrs, ex->attnum) &&
+					   !ExecCheck(ex->exprstate, econtext))
+					{
+						Form_pg_attribute attr = TupleDescAttr(newTupDesc, ex->attnum - 1);
+						ereport(ERROR,
+								errcode(ERRCODE_CHECK_VIOLATION),
+								errmsg("value for domain %s is violated by some row",
+								format_type_be(attr->atttypid)));
+					}
+				}
+			}
+
 			if (partqualstate && !ExecCheck(partqualstate, econtext))
 			{
 				if (tab->validate_default)
diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c
index 3cb3ca1cca1..59c1dda5618 100644
--- a/src/backend/commands/typecmds.c
+++ b/src/backend/commands/typecmds.c
@@ -65,6 +65,7 @@
 #include "parser/parse_expr.h"
 #include "parser/parse_func.h"
 #include "parser/parse_type.h"
+#include "rewrite/rewriteHandler.h"
 #include "utils/builtins.h"
 #include "utils/fmgroids.h"
 #include "utils/inval.h"
@@ -2772,6 +2773,8 @@ AlterDomainNotNull(List *names, bool notNull)
 								   typTup->typbasetype, typTup->typtypmod,
 								   constr, NameStr(typTup->typname), NULL);
 
+		CommandCounterIncrement();
+
 		validateDomainNotNullConstraint(domainoid);
 	}
 	else
@@ -2974,7 +2977,12 @@ AlterDomainAddConstraint(List *names, Node *newConstraint,
 		 * to.
 		 */
 		if (!constr->skip_validation)
+		{
+			/* need CCI, so we have fresh domain constraint info */
+			CommandCounterIncrement();
+
 			validateDomainCheckConstraint(domainoid, ccbin);
+		}
 
 		/*
 		 * We must send out an sinval message for the domain, to ensure that
@@ -2997,7 +3005,12 @@ AlterDomainAddConstraint(List *names, Node *newConstraint,
 								   constr, NameStr(typTup->typname), constrAddr);
 
 		if (!constr->skip_validation)
+		{
+			/* need CCI, so we have fresh domain constraint info */
+			CommandCounterIncrement();
+
 			validateDomainNotNullConstraint(domainoid);
+		}
 
 		typTup->typnotnull = true;
 		CatalogTupleUpdate(typrel, &tup->t_self, tup);
@@ -3121,6 +3134,13 @@ validateDomainNotNullConstraint(Oid domainoid)
 	List	   *rels;
 	ListCell   *rt;
 
+	EState	   *estate;
+	ExprContext *econtext;
+
+	/* we need EState for domain over virtual generated column */
 
@@ -3134,6 +3154,47 @@ validateDomainNotNullConstraint(Oid domainoid)
 		TupleTableSlot *slot;
 		TableScanDesc scan;
 		Snapshot	snapshot;
+		Form_pg_attribute attr;
+		ExprState **ri_GeneratedExprs = NULL;
+		int			attnum;
+		int			count = 0;
+
+		/* cacahe virtual generated columns handling */
+		if (tupdesc->constr && tupdesc->constr->has_generated_virtual)
+		{
+			ri_GeneratedExprs = (ExprState **) palloc0(rtc->natts * sizeof(ExprState *));
+
+			/*
+			 * We implement this by building a NullTest node for each virtual
+			 * generated column, which we cache in ri_GeneratedExprs, and
+			 * running those through ExecCheck()
+			*/
+			for (int i = 0; i < rtc->natts; i++)
+			{
+				attnum = rtc->atts[i];
+				attr = TupleDescAttr(tupdesc, attnum - 1);
+
+				if (attr->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+				{
+					NullTest   *nnulltest;
+
+					nnulltest = makeNode(NullTest);
+					nnulltest->arg = (Expr *) build_generation_expression(testrel, attnum);;
+					nnulltest->nulltesttype = IS_NOT_NULL;
+					nnulltest->argisrow = false;
+					nnulltest->location = -1;
+
+					ri_GeneratedExprs[i] = ExecPrepareExpr((Expr *) nnulltest, estate);
+					count++;
+				}
+			}
+		}
+		if (count == 0 && ri_GeneratedExprs != NULL)
+		{
+			pfree(ri_GeneratedExprs);
+			ri_GeneratedExprs = NULL;
+		}
+
 
@@ -3146,8 +3207,8 @@ validateDomainNotNullConstraint(Oid domainoid)
 
@@ -3158,14 +3219,30 @@ validateDomainNotNullConstraint(Oid domainoid)
+						Assert(ri_GeneratedExprs[i] != NULL);
+						if (!ExecCheck(ri_GeneratedExprs[i] , econtext))
@@ -3174,6 +3251,8 @@ validateDomainNotNullConstraint(Oid domainoid)
 
 /*
@@ -3210,6 +3289,41 @@ validateDomainCheckConstraint(Oid domainoid, const char *ccbin)
 		TupleTableSlot *slot;
 		TableScanDesc scan;
 		Snapshot	snapshot;
+		ExprState **ri_GeneratedExprs = NULL;
+		Form_pg_attribute attr;
+		int			attnum;
+		int			count = 0;
+
+		/* cacahe virtual generated columns handling */
+		if (tupdesc->constr && tupdesc->constr->has_generated_virtual)
+		{
+			ri_GeneratedExprs = (ExprState **) palloc0(rtc->natts * sizeof(ExprState *));
+			for (int i = 0; i < rtc->natts; i++)
+			{
+				Expr	   *defexpr;
+
+				attnum = rtc->atts[i];
+				attr = TupleDescAttr(tupdesc, attnum - 1);
+				if (attr->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+				{
+					defexpr = (Expr *) build_generation_expression(testrel, attnum);
+
+					/*
+					 * we need evaluate defexpr error soft way, otherwise
+					 * the evaulation failure error would be "value for domain
+					 * violate check constraints", which is not helpful error
+					 * message while validating domain check constraint.
+					*/
+					ri_GeneratedExprs[i] = ExecPrepareExprSafe(defexpr, estate);
+					count++;
+				}
+			}
+		}
+		if (count == 0 && ri_GeneratedExprs != NULL)
+		{
+			pfree(ri_GeneratedExprs);
+			ri_GeneratedExprs = NULL;
+		}
 
@@ -3222,37 +3336,68 @@ validateDomainCheckConstraint(Oid domainoid, const char *ccbin)

+					 * we'll use the EState's per-tuple context for evaluating
+					 * domain check constraint associated with virtual generated
+					 * column expressions (creating it if it's not already
+					 * there).
+					conResult = ExecEvalExprSwitchContext(ri_GeneratedExprs[i],
+					if (SOFT_ERROR_OCCURRED(ri_GeneratedExprs[i]->escontext))
 
index 5a581446c7d..7424dcf2742 100644
 
@@ -395,7 +396,7 @@ ExecCheckTIDVisible(EState *estate,
  *
  * This fills the resultRelInfo's ri_GeneratedExprsI/ri_NumGeneratedNeededI or
  * ri_GeneratedExprsU/ri_NumGeneratedNeededU fields, depending on cmdtype.
- * This is used only for stored generated columns.
+ * This is used for stored and virtual generated columns.
  *
  * If cmdType == CMD_UPDATE, the ri_extraUpdatedCols field is filled too.
  * This is used by both stored and virtual generated columns.
@@ -477,6 +478,33 @@ ExecInitGenerated(ResultRelInfo *resultRelInfo,
 				ri_GeneratedExprs[i] = ExecPrepareExpr(expr, estate);
 				ri_NumGeneratedNeeded++;
 			}
+			else
+			{
+				/*
+				* Virtual generated column only need to be computed when the
+				* type is domain_with_constraint.  However,
+				* domain_with_constraint can be the element type of range,
+				* composite, or array.
+				*
+				* To determine whether the true element type is
+				* domain_with_constraint, multiple lookup_type_cache calls seems
+				* doable. However, this would add a lot of code.  So simplify
+				* it: only when the type is TYPTYPE_BASE and not an array type,
+				* computation is not needed.
+				*/
+				Oid		typelem	= InvalidOid;
+				Oid		atttypid = TupleDescAttr(tupdesc, i)->atttypid;
+				char	att_typtype = get_typtype(atttypid);
+
+				if (att_typtype == TYPTYPE_BASE)
+					typelem = get_element_type(atttypid);
+
+				if (att_typtype != TYPTYPE_BASE || OidIsValid(typelem))
+				{
+					ri_GeneratedExprs[i] = ExecPrepareExpr(expr, estate);
+					ri_NumGeneratedNeeded++;
+				}
+			}

 /*
  * Compute generated columns for a tuple.
- * we might support virtual generated column in future, currently not.
+ * Generally, we don't need to compute virtual generated column, except for
+ * column type is domain with constraint. Since virtual generated column don't
+ * have storage, we don't need to store it.
  */
 void
 ExecComputeGenerated(ResultRelInfo *resultRelInfo, EState *estate,
@@ -534,7 +564,8 @@ ExecComputeGenerated(ResultRelInfo *resultRelInfo, EState *estate,
 	bool	   *nulls;

 	/*
@@ -552,11 +583,14 @@ ExecComputeGenerated(ResultRelInfo *resultRelInfo, EState *estate,

 	values = palloc(sizeof(*values) * natts);
@@ -567,28 +601,35 @@ ExecComputeGenerated(ResultRelInfo *resultRelInfo, EState *estate,
 
 	for (int i = 0; i < natts; i++)
 	{
-		CompactAttribute *attr = TupleDescCompactAttr(tupdesc, i);
+		Form_pg_attribute attr = TupleDescAttr(tupdesc, i);
 
 		if (ri_GeneratedExprs[i])
 		{
 			Datum		val;
 			bool		isnull;

 
-			/*
+			if (attr->attgenerated == ATTRIBUTE_GENERATED_STORED)
 
-			values[i] = val;
-			nulls[i] = isnull;
+				values[i] = val;
+				nulls[i] = isnull;
+			}
+			else
+			{
+				Assert(attr->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL);
+				values[i] = (Datum) 0;
+				nulls[i] = true;
+			}
 		}
 		else
 		{
@@ -907,10 +948,11 @@ ExecInsert(ModifyTableContext *context,
 		slot->tts_tableOid = RelationGetRelid(resultRelInfo->ri_RelationDesc);
 
 		/*
 
@@ -1034,10 +1076,11 @@ ExecInsert(ModifyTableContext *context,
 		slot->tts_tableOid = RelationGetRelid(resultRelationDesc);
 
 		/*
 
@@ -2122,10 +2165,11 @@ ExecUpdatePrepareSlot(ResultRelInfo *resultRelInfo,
 	slot->tts_tableOid = RelationGetRelid(resultRelationDesc);
 
 	/*
-	 * Compute stored generated columns
+	 * Compute generated columns
 	 */
 	if (resultRelationDesc->rd_att->constr &&
-		resultRelationDesc->rd_att->constr->has_generated_stored)
+		(resultRelationDesc->rd_att->constr->has_generated_stored ||
+		 resultRelationDesc->rd_att->constr->has_generated_virtual))
 		ExecComputeGenerated(resultRelInfo, estate, slot,
 							 CMD_UPDATE);
 }
diff --git a/src/include/catalog/heap.h b/src/include/catalog/heap.h
index dbd339e9df4..11c7f7afbfd 100644
--- a/src/include/catalog/heap.h
+++ b/src/include/catalog/heap.h
@@ -23,7 +23,6 @@
 #define CHKATYPE_ANYARRAY		0x01	/* allow ANYARRAY */
 #define CHKATYPE_ANYRECORD		0x02	/* allow RECORD and RECORD[] */
 #define CHKATYPE_IS_PARTKEY		0x04	/* attname is part key # not column */
-#define CHKATYPE_IS_VIRTUAL		0x08	/* is virtual generated column */
 
 typedef struct RawColumnDefault
 {
index 26bbe1e9c31..925c15caf5b 100644
@@ -790,16 +790,63 @@ ERROR:  relation "gtest23p" does not exist

index 13cfbd76859..383e20f67df 100644
@@ -444,14 +444,47 @@ CREATE TABLE gtest23q (a int PRIMARY KEY, b int REFERENCES gtest23p (y));
 
 -- domains

From c93dddb0c9e744fd4e8a329a6d28413137c6dd83 Mon Sep 17 00:00:00 2001
From: jian he <[email-masked]>
Date: Thu, 13 Mar 2025 15:05:41 +0800
Subject: [PATCH v7 2/3] soft error variant of ExecPrepareExpr, ExecInitExpr

XXX more comments.

 
+/*
 
+/*
index ae99407db89..a26160042ee 100644
@@ -346,6 +346,7 @@ ExecProcNode(PlanState *node)
@@ -394,6 +395,7 @@ extern ProjectionInfo *ExecBuildUpdateProjection(List *targetList,

From 28a05de321046b578686cc4e4802bcd4243d5343 Mon Sep 17 00:00:00 2001
From: jian he <[email-masked]>
Date: Thu, 13 Mar 2025 20:15:46 +0800
Subject: [PATCH v7 1/3] rename ExecComputeStoredGenerated to
 ExecComputeGenerated

index fbbbc09a97b..906b6581e11 100644
 
 				/*
index ede89ea3cf9..40c078638ab 100644
@@ -557,8 +557,8 @@ ExecSimpleRelationInsert(ResultRelInfo *resultRelInfo,
 
@@ -654,8 +654,8 @@ ExecSimpleRelationUpdate(ResultRelInfo *resultRelInfo,
 
index 309e27f8b5f..5a581446c7d 100644
 
 /*
 
 		/*
 
 		/*
 
 /*