From 7dce02a462eff7492befcaa9fa27062816780e7d Mon Sep 17 00:00:00 2001
From: Andrey Rachitskiy <pl0h0yp1@gmail.com>
Date: Fri, 28 Aug 2026 12:33:47 +0500
Subject: [PATCH v3] Fold immutable XmlExpr forms in eval_const_expressions

IS DOCUMENT was left as XmlExpr even with a Const argument, so CASE
still simplified unused WHEN results and could execute xpath() during
planning.  That started failing for SQL-language functions once they
began using custom plans.

Fold XmlExpr in eval_const_expressions when all inputs are Const and
the op is immutable.  Treat XMLELEMENT and XMLFOREST as non-immutable.
Other XmlExprOps only manipulate xml/text.

Bug: #19487
Author: Andrey Rachitskiy <pl0h0yp1@gmail.com>
Reported-by: Ilya Portnov <i.portnov@compassplus.com>
Reviewed-by: Tom Lane <tgl@sss.pgh.pa.us>
Discussion: https://www.postgresql.org/message-id/19487-367258bc497b923a%40postgresql.org
Backpatch-through: 18
---
 src/backend/optimizer/util/clauses.c | 37 ++++++++++++++++++++++++++++
 src/test/regress/expected/xml.out    | 10 ++++++++
 src/test/regress/sql/xml.sql         |  5 ++++
 3 files changed, 52 insertions(+)

diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c
index 8da4ed617b5..96153179668 100644
--- a/src/backend/optimizer/util/clauses.c
+++ b/src/backend/optimizer/util/clauses.c
@@ -115,6 +115,7 @@ static bool contain_agg_clause_walker(Node *node, void *context);
 static bool find_window_functions_walker(Node *node, WindowFuncLists *lists);
 static bool contain_subplans_walker(Node *node, void *context);
 static bool contain_mutable_functions_walker(Node *node, void *context);
+static bool xmlexpr_is_immutable(XmlExpr *xexpr);
 static bool contain_volatile_functions_walker(Node *node, void *context);
 static bool contain_volatile_functions_not_nextval_walker(Node *node, void *context);
 static bool max_parallel_hazard_walker(Node *node,
@@ -496,6 +497,34 @@ contain_mutable_functions_walker(Node *node, void *context)
 								  context);
 }
 
+/*
+ * xmlexpr_is_immutable
+ *	  True if this XmlExpr is immutable.
+ *
+ * XMLELEMENT and XMLFOREST are treated as non-immutable.  Other XmlExprOps
+ * only manipulate xml/text.
+ */
+static bool
+xmlexpr_is_immutable(XmlExpr *xexpr)
+{
+	switch (xexpr->op)
+	{
+		case IS_XMLCONCAT:
+		case IS_XMLPARSE:
+		case IS_XMLPI:
+		case IS_XMLROOT:
+		case IS_XMLSERIALIZE:
+		case IS_DOCUMENT:
+			return true;
+
+		case IS_XMLELEMENT:
+		case IS_XMLFOREST:
+			return false;
+	}
+
+	return false;
+}
+
 /*
  * contain_mutable_functions_after_planning
  *	  Test whether given expression contains mutable functions.
@@ -3702,6 +3731,14 @@ eval_const_expressions_mutator(Node *node,
 					return ece_evaluate_expr(node);
 				return node;
 			}
+		case T_XmlExpr:
+			{
+				node = ece_generic_processing(node);
+				if (ece_all_arguments_const(node) &&
+					xmlexpr_is_immutable((XmlExpr *) node))
+					return ece_evaluate_expr(node);
+				return node;
+			}
 		case T_CoalesceExpr:
 			{
 				CoalesceExpr *coalesceexpr = (CoalesceExpr *) node;
diff --git a/src/test/regress/expected/xml.out b/src/test/regress/expected/xml.out
index fb3e0ec41b2..d60e1c4efb7 100644
--- a/src/test/regress/expected/xml.out
+++ b/src/test/regress/expected/xml.out
@@ -713,6 +713,16 @@ LINE 1: SELECT '<>' IS NOT DOCUMENT;
 DETAIL:  line 1: StartTag: invalid element name
 <>
  ^
+-- Fold IS DOCUMENT so CASE does not simplify unused xpath() at plan time.
+SELECT CASE WHEN ('2019-12-16T00:00:00.000'::xml) IS DOCUMENT
+	THEN (xpath('/*/text()', '2019-12-16T00:00:00.000'::xml))[1]::text
+	ELSE ('2019-12-16T00:00:00.000'::xml)::text
+END;
+          case           
+-------------------------
+ 2019-12-16T00:00:00.000
+(1 row)
+
 SELECT xmlagg(data) FROM xmltest;
                 xmlagg                
 --------------------------------------
diff --git a/src/test/regress/sql/xml.sql b/src/test/regress/sql/xml.sql
index aafd39433a6..52e39187447 100644
--- a/src/test/regress/sql/xml.sql
+++ b/src/test/regress/sql/xml.sql
@@ -185,6 +185,11 @@ SELECT xml '<abc/>' IS NOT DOCUMENT;
 SELECT xml 'abc' IS NOT DOCUMENT;
 SELECT '<>' IS NOT DOCUMENT;
 
+-- Fold IS DOCUMENT so CASE does not simplify unused xpath() at plan time.
+SELECT CASE WHEN ('2019-12-16T00:00:00.000'::xml) IS DOCUMENT
+	THEN (xpath('/*/text()', '2019-12-16T00:00:00.000'::xml))[1]::text
+	ELSE ('2019-12-16T00:00:00.000'::xml)::text
+END;
 
 SELECT xmlagg(data) FROM xmltest;
 SELECT xmlagg(data) FROM xmltest WHERE id > 10;
-- 
2.53.0

