From fc0e4763fd44c1e1571387c39b9cb5a0c8167730 Mon Sep 17 00:00:00 2001
From: "Anton A. Melnikov" <a.melnikov@postgrespro.ru>
Date: Fri, 27 Mar 2026 11:32:25 +0300
Subject: [PATCH] Avoid repeated parsing of generated column expressions.

After commit 83ea6c54, execution paths were introduced
where ExecInitGenerated() can be invoked per row
for the same ResultRelInfo during a single query.
Because the parsed expression trees are allocated
in es_query_cxt, each call results in additional
allocations that are not freed until query end.

Fix it by caching the parsed expression trees
(Node *) in ResultRelInfo,
ensuring that build_column_default() and stringToNode()
are executed only once per attribute per query
and subsequent calls reuse the cached parse trees.

This restores the old behavior where expression parsing
is done once per query and prevents unbounded memory growth.
---
 src/backend/executor/execMain.c        |  1 +
 src/backend/executor/nodeModifyTable.c | 14 +++++++++++++-
 src/include/nodes/execnodes.h          |  3 +++
 3 files changed, 17 insertions(+), 1 deletion(-)

diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 58b84955c2b..b3d2304624e 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1311,6 +1311,7 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
 	resultRelInfo->ri_GenVirtualNotNullConstraintExprs = NULL;
 	resultRelInfo->ri_GeneratedExprsI = NULL;
 	resultRelInfo->ri_GeneratedExprsU = NULL;
+	resultRelInfo->ri_GeneratedExprsParsed = NULL;
 	resultRelInfo->ri_projectReturning = NULL;
 	resultRelInfo->ri_onConflictArbiterIndexes = NIL;
 	resultRelInfo->ri_onConflict = NULL;
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 4cd5e262e0f..451a38bc7c9 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -485,7 +485,19 @@ ExecInitGenerated(ResultRelInfo *resultRelInfo,
 			Expr	   *expr;
 
 			/* Fetch the GENERATED AS expression tree */
-			expr = (Expr *) build_column_default(rel, i + 1);
+			if (resultRelInfo->ri_GeneratedExprsParsed == NULL)
+				resultRelInfo->ri_GeneratedExprsParsed = palloc0(natts * sizeof(Node *));
+
+			if (resultRelInfo->ri_GeneratedExprsParsed[i] == NULL)
+			{
+				expr = (Expr *) build_column_default(rel, i + 1);
+				resultRelInfo->ri_GeneratedExprsParsed[i] = (Node *) expr;
+			}
+			else
+			{
+				expr = (Expr *) resultRelInfo->ri_GeneratedExprsParsed[i];
+			}
+
 			if (expr == NULL)
 				elog(ERROR, "no generation expression found for column number %d of table \"%s\"",
 					 i + 1, RelationGetRelationName(rel));
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 684e398f824..ca30137a308 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -582,6 +582,9 @@ typedef struct ResultRelInfo
 	int			ri_NumGeneratedNeededI;
 	int			ri_NumGeneratedNeededU;
 
+	/* Cached parsed GENERATED expressions */
+	Node      **ri_GeneratedExprsParsed;
+
 	/* list of RETURNING expressions */
 	List	   *ri_returningList;
 
-- 
2.50.1

