From 847defcce012d89d944e82168ae6eb59ee286e40 Mon Sep 17 00:00:00 2001
From: Jim Jones <jim.jones@uni-muenster.de>
Date: Tue, 8 Sep 2026 09:06:04 +0200
Subject: [PATCH v24] Add XMLCAST (SQL/XML feature X025)

This patch introduces support for XMLCAST, as specified in SQL/XML:2023
(ISO/IEC 9075-14:2023), Subclause 6.7 "<XML cast specification>". It
enables standards-compliant conversion between SQL data types and XML,
following the lexical rules defined by W3C XML Schema Part 2.

XMLCAST provides an alternative to CAST when converting SQL values into
XML content, ensuring the output uses standard XML Schema lexical forms.
For example, timestamp and interval values are rendered as xs:dateTime
and xs:duration (e.g. "2024-01-01T12:00:00" or "P1Y2M").

Conversely, XMLCAST converts XML content back into SQL types, requiring
the value to lie in the lexical space of the corresponding XML Schema
type: XMLCAST('yes'::xml AS boolean) and XMLCAST('3 days'::xml AS
interval) are errors. A useful consequence is that the result does not
depend on DateStyle. This validation uses libxml2's XML Schema support
and is therefore only active when libxml2 was built with it.

In that direction the XML value is atomized, so markup is stripped and
XMLCAST('<a>x</a>'::xml AS text) is 'x'; a value that atomizes to more
than one item is rejected rather than concatenated.

Supported casts include:
* SQL -> XML: boolean, numeric, character, date/time, interval, binary
* XML -> SQL: the inverse of the above, with lexical validation

The BY REF and BY VALUE clauses are accepted for SQL/XML compatibility
when both the operand and the target are of type xml; which one is
written makes no difference.

Author: Jim Jones <jim.jones@uni-muenster.de>
Reviewed-by: Robert Haas <robertmhaas@gmail.com>
Reviewed-by: Marcos Pegoraro <marcos@f10.com.br>
Discussion: https://www.postgresql.org/message-id/flat/7b99d466-985f-4d27-8c93-9b98c6945ebb%40uni-muenster.de
---
 doc/src/sgml/config.sgml              |    4 +-
 doc/src/sgml/datatype.sgml            |  180 +++-
 doc/src/sgml/func/func-xml.sgml       |    7 +-
 src/backend/catalog/sql_features.txt  |    2 +-
 src/backend/executor/execExprInterp.c |   22 +-
 src/backend/nodes/nodeFuncs.c         |   30 +-
 src/backend/optimizer/util/clauses.c  |    1 +
 src/backend/parser/gram.y             |   26 +-
 src/backend/parser/parse_expr.c       |  137 +++
 src/backend/parser/parse_target.c     |    7 +
 src/backend/utils/adt/ruleutils.c     |   13 +
 src/backend/utils/adt/xml.c           |  859 ++++++++++++++++++
 src/include/nodes/parsenodes.h        |   16 +
 src/include/nodes/primnodes.h         |   12 +-
 src/include/parser/kwlist.h           |    1 +
 src/include/utils/xml.h               |    5 +
 src/test/regress/expected/xml.out     | 1168 +++++++++++++++++++++++++
 src/test/regress/sql/xml.sql          |  602 +++++++++++++
 src/tools/pgindent/typedefs.list      |    1 +
 19 files changed, 3073 insertions(+), 20 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 0165eb9ec02..3749d100964 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -10963,7 +10963,9 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
         Sets how binary values are to be encoded in XML.  This applies
         for example when <type>bytea</type> values are converted to
         XML by the functions <function>xmlelement</function> or
-        <function>xmlforest</function>.  Possible values are
+        <function>xmlforest</function>.  It also selects the encoding
+        that <function>xmlcast</function> expects when converting XML
+        back to <type>bytea</type>.  Possible values are
         <literal>base64</literal> and <literal>hex</literal>, which
         are both defined in the XML Schema standard.  The default is
         <literal>base64</literal>.  For further information about
diff --git a/doc/src/sgml/datatype.sgml b/doc/src/sgml/datatype.sgml
index 89985ab7b16..0d9e488b4f6 100644
--- a/doc/src/sgml/datatype.sgml
+++ b/doc/src/sgml/datatype.sgml
@@ -4511,14 +4511,186 @@ XMLPARSE ( { DOCUMENT | CONTENT } <replaceable>value</replaceable>)
 XMLPARSE (DOCUMENT '<?xml version="1.0"?><book><title>Manual</title><chapter>...</chapter></book>')
 XMLPARSE (CONTENT 'abc<foo>bar</foo><bar>foo</bar>')
 ]]></programlisting>
-    While this is the only way to convert character strings into XML
-    values according to the SQL standard, the PostgreSQL-specific
-    syntaxes:
+
+    Another option for converting values to or from <type>xml</type> is
+    <function>xmlcast</function>:<indexterm><primary>xmlcast</primary></indexterm>
+<synopsis>
+XMLCAST ( <replaceable>expression</replaceable> AS <replaceable>type</replaceable> [ BY REF | BY VALUE ] )
+</synopsis>
+    Like <literal>CAST</literal>, it converts <replaceable>expression</replaceable> to
+    <replaceable>type</replaceable>; unlike <literal>CAST</literal>, the XML side uses the
+    lexical form of the corresponding XML Schema type, in both directions.  An
+    <type>interval</type> is written as <literal>P1Y2M</literal> (<type>xs:duration</type>) and a
+    <type>timestamp</type> as <literal>2024-05-19T14:30:00</literal> (<type>xs:dateTime</type>);
+    reading back, an XML value outside that lexical space is an error, so
+    <literal>XMLCAST('yes'::xml AS boolean)</literal> and
+    <literal>XMLCAST('3 days'::xml AS interval)</literal> both fail.  A useful consequence is
+    that the result does not depend on <xref linkend="guc-datestyle"/>.
+
+    In detail:
+
+     <itemizedlist>
+      <listitem>
+        <para>
+          Either <replaceable>expression</replaceable> or <replaceable>type</replaceable> must be of type <type>xml</type>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          Casting is supported between <type>xml</type> and <link linkend="datatype-character-table">character</link>,
+          <link linkend="datatype-numeric">numeric</link>, <link linkend="datatype-datetime">date/time</link>,
+          <link linkend="datatype-boolean">boolean</link> and <link linkend="datatype-binary">binary</link> data types.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          Converting from <type>xml</type> to a type other than a character
+          string checks the value against an XML Schema lexical space, which
+          requires that <application>libxml2</application> was built with XML
+          Schema support.  Where it was not, such a cast is rejected.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          Domains are handled according to their base type, and the result is
+          of the declared domain type, so any constraints on it are enforced.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          A character string target is not checked, since the lexical space of
+          <type>xs:string</type> admits anything.  Converting from XML
+          <emphasis>atomizes</emphasis> the value: the result is its
+          string value, so element markup is stripped, entity
+          and character references of every spelling are resolved,
+          and <literal>CDATA</literal> sections are unwrapped.
+          <literal>XMLCAST('&lt;a&gt;x&lt;/a&gt;'::xml AS text)</literal>
+          is therefore <literal>x</literal>, not <literal>&lt;a&gt;x&lt;/a&gt;</literal>;
+          use <function>xmlserialize</function> to obtain the markup instead.
+          An XML value with no content at all converts to the null value,
+          which is distinct from one whose string value happens to be empty.
+          Leading and trailing whitespace is ignored, as the XML Schema
+          <literal>whiteSpace</literal> facet requires, except for a character
+          string target, where <type>xs:string</type> preserves it.
+        </para>
+        <para>
+          The value must atomize to a single item.  One holding several, such
+          as <literal>'&lt;a&gt;1&lt;/a&gt;&lt;b&gt;2&lt;/b&gt;'</literal>, is
+          rejected rather than concatenated.  A single element whose content
+          spans several nodes is still one item, so
+          <literal>XMLCAST('&lt;x&gt;&lt;y&gt;bar&lt;/y&gt;foo&lt;/x&gt;'::xml AS text)</literal>
+          is <literal>barfoo</literal>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          Time zones are normalized rather than ignored.  Converting to a type
+          <literal>WITHOUT TIME ZONE</literal>, a value carrying a zone is
+          first adjusted to <acronym>UTC</acronym> and only then stripped of
+          it, so <literal>XMLCAST('2024-01-01T12:00:00+06:00'::xml AS timestamp)</literal>
+          is <literal>2024-01-01 06:00:00</literal>.  Converting to a type
+          <literal>WITH TIME ZONE</literal>, a value carrying no zone is taken
+          to be in <acronym>UTC</acronym> rather than in the session's
+          <xref linkend="guc-timezone"/>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          A <type>double precision</type> maps to <type>xs:double</type>,
+          which writes the infinities and not-a-number as
+          <literal>INF</literal>, <literal>-INF</literal> and
+          <literal>NaN</literal>; converting back rejects all three, so those
+          values do not survive a round trip through <type>xml</type>.  A <type>numeric</type> maps to
+          <type>xs:decimal</type>, which has no infinities and no
+          not-a-number at all, so those values cannot be converted to
+          <type>xml</type> in the first place.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          An <type>interval</type> maps to <type>xs:duration</type>, which
+          carries a single sign in front of the value, so
+          <literal>-P1Y2M</literal> rather than <literal>P-1Y-2M</literal>.
+          An interval whose fields differ in sign, such as
+          <literal>1 year -1 day</literal>, has no
+          <type>xs:duration</type> representation at all and is rejected.
+          Note also that <type>xs:duration</type> has no week designator,
+          so <literal>P3W</literal> is not accepted even though the
+          <type>interval</type> input syntax allows it.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          Values of type <type>bytea</type> are represented in XML as
+          <type>xs:hexBinary</type> or <type>xs:base64Binary</type>, as
+          selected by the <xref linkend="guc-xmlbinary"/> configuration
+          parameter.  That setting governs both directions, so casting a
+          <type>bytea</type> value to <type>xml</type> and back reproduces
+          the original value as long as it does not change in between.  The
+          one exception is a zero-length value, which produces empty XML
+          content and therefore comes back as the null value; the same is
+          true of the empty character string.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          Converting a character string to <type>xml</type> escapes the
+          characters that XML reserves, as <function>xmltext</function> does:
+          <literal>&amp;</literal>, <literal>&lt;</literal>,
+          <literal>&gt;</literal>, <literal>"</literal> and carriage return.
+          Note that <literal>'</literal> is not escaped (see examples below).
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>BY REF</literal> and <literal>BY VALUE</literal> may only be
+          written when both <replaceable>expression</replaceable> and
+          <replaceable>type</replaceable> are of type <type>xml</type>, and
+          which one is written makes no difference, as discussed in
+          <xref linkend="functions-xml-limits-postgresql"/>.
+        </para>
+      </listitem>
+    </itemizedlist>
+
+     Examples:
+<screen><![CDATA[
+SELECT xmlcast('<foo&bar>'::text AS xml);
+       xmlcast
+---------------------
+ &lt;foo&amp;bar&gt;
+(1 row)
+
+SELECT xmlcast('&lt;foo&amp;bar&gt;'::xml AS text);
+  xmlcast
+-----------
+ <foo&bar>
+(1 row)
+
+SELECT xmlcast('2024-05-19 14:30:00'::timestamp AS xml);
+       xmlcast
+---------------------
+ 2024-05-19T14:30:00
+(1 row)
+
+SELECT xmlcast('P1Y2M25DT5H6M7S'::xml AS interval);
+            xmlcast
+--------------------------------
+ 1 year 2 mons 25 days 05:06:07
+(1 row)
+
+SELECT xmlcast('1 year 2 months 3 weeks 4 days 5 hours 6 minutes 7 seconds'::interval AS xml);
+     xmlcast
+-----------------
+ P1Y2M25DT5H6M7S
+(1 row)
+]]></screen>
+
+    Character strings can also be converted to XML with the
+    <productname>PostgreSQL</productname>-specific cast syntaxes:
 <programlisting><![CDATA[
 xml '<foo>bar</foo>'
 '<foo>bar</foo>'::xml
 ]]></programlisting>
-    can also be used.
    </para>
 
    <para>
diff --git a/doc/src/sgml/func/func-xml.sgml b/doc/src/sgml/func/func-xml.sgml
index 511bc90852a..4da07985fe0 100644
--- a/doc/src/sgml/func/func-xml.sgml
+++ b/doc/src/sgml/func/func-xml.sgml
@@ -10,9 +10,10 @@
    The functions and function-like expressions described in this
    section operate on values of type <type>xml</type>.  See <xref
    linkend="datatype-xml"/> for information about the <type>xml</type>
-   type.  The function-like expressions <function>xmlparse</function>
-   and <function>xmlserialize</function> for converting to and from
-   type <type>xml</type> are documented there, not in this section.
+   type.  The function-like expressions <function>xmlparse</function>,
+   <function>xmlcast</function>, and <function>xmlserialize</function>
+   for converting to and from type <type>xml</type> are documented
+   there, not in this section.
   </para>
 
   <para>
diff --git a/src/backend/catalog/sql_features.txt b/src/backend/catalog/sql_features.txt
index 55f073e2262..be17b8dfcbe 100644
--- a/src/backend/catalog/sql_features.txt
+++ b/src/backend/catalog/sql_features.txt
@@ -724,7 +724,7 @@ X014	Attributes of XML type			YES
 X015	Fields of XML type			NO	
 X016	Persistent XML values			YES	
 X020	XMLConcat			YES	
-X025	XMLCast			NO	
+X025	XMLCast			YES	supported for character, numeric, boolean, date/time, interval and binary types
 X030	XMLDocument			NO	
 X031	XMLElement			YES	
 X032	XMLForest			YES	
diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
index 397219f7a3a..b90ca74d60b 100644
--- a/src/backend/executor/execExprInterp.c
+++ b/src/backend/executor/execExprInterp.c
@@ -4719,11 +4719,27 @@ ExecEvalXmlExpr(ExprState *state, ExprEvalStep *op)
 				*op->resnull = false;
 			}
 			break;
+			case IS_XMLCAST:
+			{
+				Datum	   *argvalue = op->d.xmlexpr.argvalue;
+				bool	   *argnull = op->d.xmlexpr.argnull;
 
-		default:
-			elog(ERROR, "unrecognized XML operation");
+				Assert(list_length(xexpr->args) == 1);
+
+				if (argnull[0])
+					return;
+
+				*op->resnull = false;
+				*op->resvalue = exec_xmlcast(argvalue[0],
+											 xexpr->sourceType,
+											 xexpr->targetType,
+											 op->resnull);
+			}
 			break;
-	}
+			default:
+				elog(ERROR, "unrecognized XML operation");
+				break;
+			}
 }
 
 /*
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index 2a2e00b372e..7f647e3fcec 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -220,6 +220,8 @@ exprType(const Node *expr)
 				type = BOOLOID;
 			else if (((const XmlExpr *) expr)->op == IS_XMLSERIALIZE)
 				type = TEXTOID;
+			else if (((const XmlExpr *) expr)->op == IS_XMLCAST)
+				type = ((const XmlExpr *) expr)->type;
 			else
 				type = XMLOID;
 			break;
@@ -986,11 +988,14 @@ exprCollation(const Node *expr)
 		case T_XmlExpr:
 
 			/*
-			 * XMLSERIALIZE returns text from non-collatable inputs, so its
-			 * collation is always default.  The other cases return boolean or
-			 * XML, which are non-collatable.
+			 * XMLSERIALIZE, and XMLCAST to a target it reaches by way of
+			 * text, return text, so their collation is always default.  The
+			 * other cases return boolean, XML, bytea or a date/time type,
+			 * none of which are collatable.
 			 */
-			if (((const XmlExpr *) expr)->op == IS_XMLSERIALIZE)
+			if (((const XmlExpr *) expr)->op == IS_XMLSERIALIZE ||
+				(((const XmlExpr *) expr)->op == IS_XMLCAST &&
+				 ((const XmlExpr *) expr)->type == TEXTOID))
 				coll = DEFAULT_COLLATION_OID;
 			else
 				coll = InvalidOid;
@@ -1260,7 +1265,9 @@ exprSetCollation(Node *expr, Oid collation)
 				   (collation == InvalidOid));
 			break;
 		case T_XmlExpr:
-			Assert((((XmlExpr *) expr)->op == IS_XMLSERIALIZE) ?
+			Assert((((XmlExpr *) expr)->op == IS_XMLSERIALIZE ||
+					(((XmlExpr *) expr)->op == IS_XMLCAST &&
+					 ((XmlExpr *) expr)->type == TEXTOID)) ?
 				   (collation == DEFAULT_COLLATION_OID) :
 				   (collation == InvalidOid));
 			break;
@@ -1754,6 +1761,9 @@ exprLocation(const Node *expr)
 		case T_FunctionParameter:
 			loc = ((const FunctionParameter *) expr)->location;
 			break;
+		case T_XmlCast:
+			loc = ((const XmlCast *) expr)->location;
+			break;
 		case T_XmlSerialize:
 			/* XMLSERIALIZE keyword should always be the first thing */
 			loc = ((const XmlSerialize *) expr)->location;
@@ -4583,6 +4593,16 @@ raw_expression_tree_walker_impl(Node *node,
 					return true;
 			}
 			break;
+		case T_XmlCast:
+			{
+				XmlCast   *xc = (XmlCast *) node;
+
+				if (WALK(xc->expr))
+					return true;
+				if (WALK(xc->typeName))
+					return true;
+			}
+			break;
 		case T_CollateClause:
 			return WALK(((CollateClause *) node)->arg);
 		case T_SortBy:
diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c
index 55cebe4a74b..dc52a900961 100644
--- a/src/backend/optimizer/util/clauses.c
+++ b/src/backend/optimizer/util/clauses.c
@@ -527,6 +527,7 @@ xmlexpr_is_immutable(XmlExpr *xexpr)
 
 		case IS_XMLELEMENT:
 		case IS_XMLFOREST:
+		case IS_XMLCAST:
 
 			/*
 			 * These variants invoke I/O conversion functions for a wide range
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index c025eaaaa4e..b49fdec0b45 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -323,7 +323,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
 %type <str>			opt_single_name
 %type <list>		opt_qualified_name
-%type <boolean>		opt_concurrently opt_usingindex
+%type <boolean>		opt_concurrently opt_usingindex opt_xml_passing_mech
 %type <dbehavior>	opt_drop_behavior
 %type <list>		opt_utility_option_list
 %type <list>		opt_wait_with_clause
@@ -840,7 +840,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
 	WAIT WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE
 
-	XML_P XMLATTRIBUTES XMLCONCAT XMLELEMENT XMLEXISTS XMLFOREST XMLNAMESPACES
+	XML_P XMLATTRIBUTES XMLCAST XMLCONCAT XMLELEMENT XMLEXISTS XMLFOREST XMLNAMESPACES
 	XMLPARSE XMLPI XMLROOT XMLSERIALIZE XMLTABLE
 
 	YEAR_P YES_P
@@ -16970,6 +16970,16 @@ func_expr_common_subexpr:
 					v->location = @1;
 					$$ = (Node *) v;
 				}
+			| XMLCAST '(' a_expr AS Typename opt_xml_passing_mech ')'
+				{
+					XmlCast *n = makeNode(XmlCast);
+
+					n->expr = $3;
+					n->typeName = $5;
+					n->passing_mech = $6;
+					n->location = @1;
+					$$ = (Node *) n;
+				}
 			| XMLCONCAT '(' expr_list ')'
 				{
 					$$ = makeXmlExpr(IS_XMLCONCAT, NULL, NIL, $3, @1);
@@ -17288,6 +17298,16 @@ xml_passing_mech:
 			| BY VALUE_P
 		;
 
+/*
+ * Whether a passing mechanism was written at all.  Which one it was makes no
+ * difference, but per SQL/XML Subclause 6.7 Syntax Rule 9 it may only appear
+ * when both the XMLCAST operand and target are of type xml.
+ */
+opt_xml_passing_mech:
+			xml_passing_mech						{ $$ = true; }
+			| /*EMPTY*/								{ $$ = false; }
+		;
+
 /*****************************************************************************
  *
  * WAIT FOR LSN
@@ -19264,6 +19284,7 @@ col_name_keyword:
 			| VALUES
 			| VARCHAR
 			| XMLATTRIBUTES
+			| XMLCAST
 			| XMLCONCAT
 			| XMLELEMENT
 			| XMLEXISTS
@@ -19865,6 +19886,7 @@ bare_label_keyword:
 			| WRITE
 			| XML_P
 			| XMLATTRIBUTES
+			| XMLCAST
 			| XMLCONCAT
 			| XMLELEMENT
 			| XMLEXISTS
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 30c889f505f..ea898e21aa8 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -69,6 +69,7 @@ static Node *transformMinMaxExpr(ParseState *pstate, MinMaxExpr *m);
 static Node *transformSQLValueFunction(ParseState *pstate,
 									   SQLValueFunction *svf);
 static Node *transformXmlExpr(ParseState *pstate, XmlExpr *x);
+static Node *transformXmlCast(ParseState *pstate, XmlCast *xc);
 static Node *transformXmlSerialize(ParseState *pstate, XmlSerialize *xs);
 static Node *transformBooleanTest(ParseState *pstate, BooleanTest *b);
 static Node *transformCurrentOfExpr(ParseState *pstate, CurrentOfExpr *cexpr);
@@ -277,6 +278,10 @@ transformExprRecurse(ParseState *pstate, Node *expr)
 											   (SQLValueFunction *) expr);
 			break;
 
+		case T_XmlCast:
+			result = transformXmlCast(pstate, (XmlCast *) expr);
+			break;
+
 		case T_XmlExpr:
 			result = transformXmlExpr(pstate, (XmlExpr *) expr);
 			break;
@@ -2506,6 +2511,10 @@ transformXmlExpr(ParseState *pstate, XmlExpr *x)
 				newe = coerce_to_specific_type(pstate, newe, XMLOID,
 											   "IS DOCUMENT");
 				break;
+			case IS_XMLCAST:
+				/* not handled here */
+				Assert(false);
+				break;
 		}
 		newx->args = lappend(newx->args, newe);
 		i++;
@@ -2514,6 +2523,134 @@ transformXmlExpr(ParseState *pstate, XmlExpr *x)
 	return (Node *) newx;
 }
 
+/*
+ * transformXmlCast -
+ *	  transform an XMLCAST expression
+ *
+ * XMLCAST converts a SQL value to xml, or an xml value to a SQL type, using
+ * the lexical form of the corresponding XML Schema type on the XML side.  One
+ * of the two must be xml; domains on either side are flattened to their base
+ * type.
+ *
+ * The XmlExpr built here does whatever exec_xmlcast() can do directly, and is
+ * wrapped in an ordinary cast node that converts its result to the type the
+ * user declared.  That wrapper is what applies a typmod and enforces the
+ * constraints of a domain target, and is General Rule 4.j's "CAST (A AS
+ * SQLT)".
+ */
+static Node *
+transformXmlCast(ParseState *pstate, XmlCast *xc)
+{
+	Node *result;
+	Node *expr;
+	XmlExpr *xexpr;
+	int32 targetTypmod;
+	Oid targetType;
+	Oid targetBaseType;
+	Oid inputType;
+
+	/* Transform the input expression */
+	expr = transformExprRecurse(pstate, xc->expr);
+
+	typenameTypeIdAndMod(pstate, xc->typeName, &targetType, &targetTypmod);
+
+	/*
+	 * Flatten domains on both sides.  What governs the conversion is the
+	 * underlying type: a domain over xml is still XML, and a domain over a
+	 * supported SQL type still has the same XML Schema lexical form.  The
+	 * XmlExpr below therefore deals only in base types, and the coercion
+	 * added at the end converts its result to the declared target type,
+	 * applying any domain constraints on the way.  map_sql_value_to_xml_value()
+	 * flattens domains for the same reason.
+	 */
+	inputType = getBaseType(exprType(expr));
+	targetBaseType = getBaseType(targetType);
+
+	/*
+	 * Ensure that either the cast operand or the data type is an XML, and
+	 * that both sides are types XMLCAST knows.  Both are matched against the
+	 * same exact set of type OIDs: a type category is too coarse a test,
+	 * since it says nothing about a type's physical representation, and
+	 * exec_xmlcast() would end up handing, say, a pass-by-value type to
+	 * xmltext() as though it were a varlena.  An untyped literal is the one
+	 * exception, and is resolved to text just below.
+	 */
+	if ((inputType != XMLOID && targetBaseType != XMLOID) ||
+		(inputType != UNKNOWNOID && !xmlcast_target_type_supported(inputType)) ||
+		!xmlcast_target_type_supported(targetBaseType))
+		ereport(ERROR,
+				(errcode(ERRCODE_CANNOT_COERCE),
+				 errmsg("cannot cast type %s to %s",
+						format_type_be(exprType(expr)),
+						format_type_be(targetType)),
+				 parser_errposition(pstate, xc->location)));
+
+	/*
+	 * A conversion from XML has to check the value against the lexical space
+	 * of its XML Schema type, which needs a libxml2 built with XML Schema
+	 * support.  Refuse here rather than accept whatever the target type's
+	 * input function happens to take.
+	 */
+	if (inputType == XMLOID && !xmlcast_can_validate(targetBaseType))
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("XMLCAST to type %s is not supported by this build",
+						format_type_be(targetType)),
+				 errdetail("Checking the XML Schema lexical form requires libxml2 with XML Schema support."),
+				 parser_errposition(pstate, xc->location)));
+
+	/*
+	 * Syntax Rule 9: an <XML passing mechanism> may only be written when both
+	 * the operand and the target are XML types.  We ignore which one was
+	 * asked for, but where it may appear is still part of the syntax.
+	 */
+	if (xc->passing_mech && (inputType != XMLOID || targetBaseType != XMLOID))
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg("BY REF and BY VALUE are only allowed when both the XMLCAST operand and target are of type xml"),
+				 parser_errposition(pstate, xc->location)));
+
+	/*
+	 * exec_xmlcast() has no mapping of its own for these, and their output
+	 * form is already the XML Schema one, so let them go through as text.
+	 */
+	if (inputType == INT2OID || inputType == INT4OID || inputType == INT8OID ||
+		inputType == NAMEOID || inputType == UNKNOWNOID)
+		inputType = TEXTOID;
+
+	xexpr = makeNode(XmlExpr);
+	xexpr->op = IS_XMLCAST;
+	xexpr->location = xc->location;
+	xexpr->type = xmlcast_result_type(targetBaseType);
+	xexpr->typmod = -1;
+	xexpr->targetType = targetBaseType;
+	xexpr->targetTypmod = targetTypmod;
+	xexpr->sourceType = inputType;
+	xexpr->args = list_make1(coerce_to_specific_type(pstate,
+													 expr,
+													 inputType,
+													 "XMLCAST"));
+
+	/*
+	 * Add a cast from whatever exec_xmlcast() actually produces to the type
+	 * the user asked for -- the declared one, so that a domain target picks
+	 * up its constraints here.  For a non-domain target that XMLCAST handles
+	 * natively this is a no-op.
+	 */
+	result = coerce_to_target_type(pstate, (Node *) xexpr,
+								   xexpr->type,
+								   targetType, targetTypmod,
+								   COERCION_EXPLICIT,
+								   COERCE_EXPLICIT_CAST,
+								   -1);
+
+	/* the target was vetted above, so this really should not happen */
+	if (result == NULL)
+		elog(ERROR, "could not coerce XMLCAST result to type %u", targetType);
+
+	return result;
+}
+
 static Node *
 transformXmlSerialize(ParseState *pstate, XmlSerialize *xs)
 {
diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c
index 0ea10f8e882..7347595315e 100644
--- a/src/backend/parser/parse_target.c
+++ b/src/backend/parser/parse_target.c
@@ -1970,8 +1970,15 @@ FigureColnameInternal(Node *node, char **name)
 				case IS_DOCUMENT:
 					/* nothing */
 					break;
+				case IS_XMLCAST:
+					*name = "xmlcast";
+					return 2;
 			}
 			break;
+		case T_XmlCast:
+			/* make XMLCAST act like a regular function */
+			*name = "xmlcast";
+			return 2;
 		case T_XmlSerialize:
 			/* make XMLSERIALIZE act like a regular function */
 			*name = "xmlserialize";
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 6506bf12e3c..765965b5c84 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -10673,6 +10673,9 @@ get_rule_expr(Node *node, deparse_context *context,
 						break;
 					case IS_DOCUMENT:
 						break;
+					case IS_XMLCAST:
+						appendStringInfoString(buf, "XMLCAST(");
+						break;
 				}
 				if (xexpr->op == IS_XMLPARSE || xexpr->op == IS_XMLSERIALIZE)
 				{
@@ -10722,6 +10725,7 @@ get_rule_expr(Node *node, deparse_context *context,
 						case IS_XMLFOREST:
 						case IS_XMLPI:
 						case IS_XMLSERIALIZE:
+						case IS_XMLCAST:
 							/* no extra decoration needed */
 							get_rule_expr((Node *) xexpr->args, context, true);
 							break;
@@ -10794,6 +10798,15 @@ get_rule_expr(Node *node, deparse_context *context,
 						appendStringInfoString(buf, " NO INDENT");
 				}
 
+				/*
+				 * BY REF / BY VALUE is not emitted: which one was written
+				 * makes no difference, and the node does not record it.
+				 */
+				if (xexpr->op == IS_XMLCAST)
+					appendStringInfo(buf, " AS %s",
+									 format_type_with_typemod(xexpr->targetType,
+															  xexpr->targetTypmod));
+
 				if (xexpr->op == IS_DOCUMENT)
 					appendStringInfoString(buf, " IS DOCUMENT");
 				else
diff --git a/src/backend/utils/adt/xml.c b/src/backend/utils/adt/xml.c
index 1f75ffcfd9d..56ab711f0df 100644
--- a/src/backend/utils/adt/xml.c
+++ b/src/backend/utils/adt/xml.c
@@ -45,6 +45,8 @@
 
 #include "postgres.h"
 
+#include <math.h>
+
 #ifdef USE_LIBXML
 #include <libxml/chvalid.h>
 #include <libxml/entities.h>
@@ -54,6 +56,9 @@
 #include <libxml/uri.h>
 #include <libxml/xmlerror.h>
 #include <libxml/xmlsave.h>
+#ifdef LIBXML_SCHEMAS_ENABLED
+#include <libxml/xmlschemastypes.h>
+#endif
 #include <libxml/xmlversion.h>
 #include <libxml/xmlwriter.h>
 #include <libxml/xpath.h>
@@ -99,6 +104,7 @@
 #include "utils/date.h"
 #include "utils/datetime.h"
 #include "utils/lsyscache.h"
+#include "utils/numeric.h"
 #include "utils/rel.h"
 #include "utils/syscache.h"
 #include "utils/xml.h"
@@ -1027,6 +1033,859 @@ xmlelement(XmlExpr *xexpr,
 #endif
 }
 
+/*
+ * Everything XMLCAST needs to know about a SQL target type, per SQL/XML:2023
+ * (ISO/IEC 9075-14:2023), Subclause 6.7 "<XML cast specification>", Syntax
+ * Rule 15.  A type absent from this table cannot be an XMLCAST target at all.
+ *
+ * "native" marks the targets exec_xmlcast() builds itself, because their XML
+ * Schema lexical form is not what the target type's input function would make
+ * of it; everything else it produces as text for an outer cast node to
+ * finish, which is General Rule 4.j's "CAST (A AS SQLT)".
+ *
+ * "collapse_ws" is the XML Schema whiteSpace facet: every type XMLCAST uses
+ * collapses, except xs:string, which preserves.  The atomized value has the
+ * facet applied before it is validated or converted, so that a padded
+ * "  -P1Y2M  " reaches interval_in() as "-P1Y2M".
+ *
+ * xsdtype is the lexical space an XML value must lie in, or XSD(UNKNOWN) --
+ * that is, 0 -- where no check applies: xs:string admits anything, and
+ * bytea's XSD type depends on xmlbinary, so xmlcast_validate_lexical()
+ * resolves that one itself.  It is stored as int rather than
+ * xmlSchemaValType so that this table, and the accessors below it, stay
+ * available in a build without libxml.
+ *
+ * Syntax Rules 15.c.ii and 15.d map every integer type to unbounded
+ * xs:integer and every approximate numeric to xs:double, so a value that is
+ * lexically fine but out of the SQL type's range is caught by the cast in
+ * General Rule 4.j rather than by validation.
+ */
+#ifdef LIBXML_SCHEMAS_ENABLED
+#define XSD(x) XML_SCHEMAS_##x
+#else
+#define XSD(x) 0
+#endif
+
+static const struct
+{
+	Oid			sqltype;
+	bool		native;
+	bool		collapse_ws;
+	int			xsdtype;
+	const char *xsdname;
+}			xmlcast_types[] =
+{
+	{XMLOID, true, false, XSD(UNKNOWN), NULL},
+	{TEXTOID, false, false, XSD(UNKNOWN), NULL},
+	{VARCHAROID, false, false, XSD(UNKNOWN), NULL},
+	{NAMEOID, false, false, XSD(UNKNOWN), NULL},
+	{BPCHAROID, false, false, XSD(UNKNOWN), NULL},
+	{BYTEAOID, true, true, XSD(UNKNOWN), NULL},
+	{BOOLOID, false, true, XSD(BOOLEAN), "xs:boolean"},
+	{INT2OID, false, true, XSD(INTEGER), "xs:integer"},
+	{INT4OID, false, true, XSD(INTEGER), "xs:integer"},
+	{INT8OID, false, true, XSD(INTEGER), "xs:integer"},
+	{NUMERICOID, false, true, XSD(DECIMAL), "xs:decimal"},
+	{FLOAT4OID, false, true, XSD(DOUBLE), "xs:double"},
+	{FLOAT8OID, false, true, XSD(DOUBLE), "xs:double"},
+	{DATEOID, true, true, XSD(DATE), "xs:date"},
+	{TIMEOID, true, true, XSD(TIME), "xs:time"},
+	{TIMETZOID, true, true, XSD(TIME), "xs:time"},
+	{TIMESTAMPOID, true, true, XSD(DATETIME), "xs:dateTime"},
+	{TIMESTAMPTZOID, true, true, XSD(DATETIME), "xs:dateTime"},
+	{INTERVALOID, true, true, XSD(DURATION), "xs:duration"},
+};
+
+/*
+ * Look up targetType in xmlcast_types[], or return -1 if it is not there.
+ */
+static int
+xmlcast_type_index(Oid targetType)
+{
+	int			i;
+
+	for (i = 0; i < lengthof(xmlcast_types); i++)
+	{
+		if (xmlcast_types[i].sqltype == targetType)
+			return i;
+	}
+
+	return -1;
+}
+
+/*
+ * Can exec_xmlcast() produce a value of the given type?
+ *
+ * The parser uses this to reject an unsupported target before execution.  The
+ * type category would be too coarse a test, since categories admit types we
+ * have no XML Schema mapping for -- oid and money are both
+ * TYPCATEGORY_NUMERIC -- and a domain inherits its base type's category.
+ */
+bool
+xmlcast_target_type_supported(Oid targetType)
+{
+	return xmlcast_type_index(targetType) >= 0;
+}
+
+/*
+ * Can the lexical form of a value converted to targetType be checked?
+ *
+ * Only false when libxml2 is present but was built without XML Schema
+ * support, and then only for the targets that need checking.
+ * transformXmlCast() consults this so that such a cast is refused once,
+ * during parse analysis, rather than once per row from inside
+ * exec_xmlcast().
+ *
+ * With no libxml2 at all there is nothing specific to say: report success
+ * here and let exec_xmlcast() raise the usual "unsupported XML feature", so
+ * that every XMLCAST fails the same way as the rest of the XML code.
+ */
+bool
+xmlcast_can_validate(Oid targetType)
+{
+#ifndef USE_LIBXML
+	return true;
+#elif defined(LIBXML_SCHEMAS_ENABLED)
+	return true;
+#else
+	int			i = xmlcast_type_index(targetType);
+
+	Assert(i >= 0);
+
+	/* nothing to check means nothing is missing */
+	return (targetType != BYTEAOID && xmlcast_types[i].xsdname == NULL);
+#endif
+}
+
+/*
+ * What type of value does exec_xmlcast() hand back for the given target?
+ *
+ * transformXmlCast() records this as the XmlExpr's result type, so that the
+ * outer cast node it adds converts from here to the declared target.
+ */
+Oid
+xmlcast_result_type(Oid targetType)
+{
+	int			i = xmlcast_type_index(targetType);
+
+	Assert(i >= 0);
+
+	return xmlcast_types[i].native ? targetType : TEXTOID;
+}
+
+#ifdef USE_LIBXML
+
+/*
+ * Is this node character data?  Text and CDATA sections are the same thing in
+ * the XQuery data model, so a run of them is a single text node.
+ */
+static bool
+xmlcast_node_is_text(xmlNodePtr node)
+{
+	return (node->type == XML_TEXT_NODE ||
+			node->type == XML_CDATA_SECTION_NODE);
+}
+
+/*
+ * Apply the XML Schema whiteSpace "collapse" facet: tabs, newlines and
+ * carriage returns become spaces, runs of spaces become one, and leading and
+ * trailing spaces are dropped.
+ */
+static char *
+xsd_collapse_whitespace(const char *str)
+{
+	StringInfoData buf;
+	bool		pending_space = false;
+
+	initStringInfo(&buf);
+
+	for (; *str; str++)
+	{
+		if (*str == ' ' || *str == '\t' || *str == '\n' || *str == '\r')
+		{
+			/* only emit a separator if something else follows */
+			pending_space = (buf.len > 0);
+			continue;
+		}
+
+		if (pending_space)
+		{
+			appendStringInfoChar(&buf, ' ');
+			pending_space = false;
+		}
+		appendStringInfoChar(&buf, *str);
+	}
+
+	return buf.data;
+}
+
+/*
+ * Is this node an item of the sequence to atomize?
+ *
+ * libxml child lists hold things the XQuery data model has no concept of: a
+ * document's children include the DTD internal subset as an XML_DTD_NODE.
+ * Match the node kinds XDM does have, so anything else is ignored rather than
+ * counted.  Entity references cannot appear here because xml_parse() passes
+ * XML_PARSE_NOENT, and XInclude markers require xmlXIncludeProcess(), which
+ * PostgreSQL never calls.
+ */
+static bool
+xmlcast_node_is_item(xmlNodePtr node)
+{
+	switch (node->type)
+	{
+		case XML_ELEMENT_NODE:
+		case XML_TEXT_NODE:
+		case XML_CDATA_SECTION_NODE:
+		case XML_COMMENT_NODE:
+		case XML_PI_NODE:
+			return true;
+		default:
+			return false;
+	}
+}
+
+/*
+ * Atomize an XML value, per SQL/XML:2023 Subclause 6.7 General Rules 4.a and
+ * 4.b: document nodes are removed and fn:data() applied to what remains.  For
+ * PostgreSQL's untyped content an item atomizes to its string value, which is
+ * what libxml's xmlNodeGetContent() computes.  Reversing a fixed list of
+ * escapes by hand would not do: what an XML value may arrive as is
+ * open-ended, whereas an escaper's output is not.
+ *
+ * General Rule 4.h then casts the sequence to a single value, which an XQuery
+ * cast can only do for one item, so two or more is an error rather than a
+ * concatenation: "<a>1</a><b>2</b>" must not become 12.  That counts items,
+ * not nodes -- the XQuery data model has no CDATA, so adjacent text and CDATA
+ * nodes are one item, and "<x><y>bar</y>foo</x>" is one element whose string
+ * value is legitimately "barfoo".
+ *
+ * *is_empty_sequence is set when there are no items at all, which General
+ * Rule 4.c makes a null result, unlike an item whose string value is empty.
+ */
+static char *
+xmlcast_atomize(Datum value, bool *is_empty_sequence)
+{
+	xmltype    *data = DatumGetXmlP(value);
+	XmlOptionType parsed_type;
+	volatile xmlDocPtr doc = NULL;
+	volatile xmlNodePtr nodes = NULL;
+	volatile xmlChar *content = NULL;
+	PgXmlErrorContext *volatile xmlerrcxt = NULL;
+	char	   *volatile result = NULL;
+
+	*is_empty_sequence = false;
+
+	/* xml_parse() brackets its own libxml usage; ours starts after it */
+	doc = xml_parse(data, XMLOPTION_CONTENT, true, GetDatabaseEncoding(),
+					&parsed_type, (xmlNodePtr *) &nodes, NULL);
+
+	/*
+	 * We already have a libxml document to free, so like
+	 * xmltotext_with_options() we call pg_xml_init() inside the PG_TRY and
+	 * are prepared for it not to have run.
+	 */
+	PG_TRY();
+	{
+		StringInfoData buf;
+		xmlNodePtr	list;
+		xmlNodePtr	cur;
+		int			nitems = 0;
+		bool		prev_was_text = false;
+
+		xmlerrcxt = pg_xml_init(PG_XML_STRICTNESS_ALL);
+
+		/*
+		 * Removing the document node leaves its children, so either way the
+		 * sequence to atomize is the top-level node list.
+		 */
+		list = (parsed_type == XMLOPTION_DOCUMENT) ?
+			doc->children : (xmlNodePtr) nodes;
+
+		for (cur = list; cur != NULL; cur = cur->next)
+		{
+			if (!xmlcast_node_is_item(cur))
+				continue;
+
+			/* a run of text and CDATA nodes is one item, so count it once */
+			if (xmlcast_node_is_text(cur) && prev_was_text)
+				continue;
+
+			prev_was_text = xmlcast_node_is_text(cur);
+			nitems++;
+		}
+
+		if (nitems == 0)
+		{
+			*is_empty_sequence = true;
+		}
+		else if (nitems > 1)
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_ARGUMENT_FOR_XQUERY),
+					 errmsg("XMLCAST operand must atomize to a single value"),
+					 errdetail("The XML value contains %d items.", nitems)));
+		}
+		else
+		{
+			initStringInfo(&buf);
+
+			for (cur = list; cur != NULL; cur = cur->next)
+			{
+				if (!xmlcast_node_is_item(cur))
+					continue;
+
+				content = xmlNodeGetContent(cur);
+				if (content == NULL || xmlerrcxt->err_occurred)
+					xml_ereport(xmlerrcxt, ERROR, ERRCODE_OUT_OF_MEMORY,
+								"could not allocate xmlChar");
+				appendStringInfoString(&buf, (const char *) content);
+				xmlFree((void *) content);
+				content = NULL;
+			}
+
+			result = buf.data;
+		}
+	}
+	PG_CATCH();
+	{
+		if (content)
+			xmlFree((void *) content);
+		if (nodes)
+			xmlFreeNodeList((xmlNodePtr) nodes);
+		if (doc)
+			xmlFreeDoc(doc);
+		if (xmlerrcxt)
+			pg_xml_done(xmlerrcxt, true);
+
+		PG_RE_THROW();
+	}
+	PG_END_TRY();
+
+	if (nodes)
+		xmlFreeNodeList((xmlNodePtr) nodes);
+	if (doc)
+		xmlFreeDoc(doc);
+	pg_xml_done(xmlerrcxt, false);
+
+	return result;
+}
+
+/*
+ * Check that an XML value lies in the lexical space of the XML Schema type
+ * corresponding to targetType, and complain if it does not.
+ *
+ * This is what separates XMLCAST from a plain CAST in the XML -> SQL
+ * direction: a SQL input function accepts whatever PostgreSQL accepts, which
+ * is wider than the XSD lexical space (bool "yes", int "0x10", interval
+ * "3 days") and, for dates and timestamps, depends on DateStyle.
+ *
+ * libxml2 implements every built-in XSD lexical space, so validate with it
+ * rather than reimplementing the rules.  Do not, however, reach for
+ * xmlSchemaGetCanonValue() to normalize afterwards: as of libxml2 2.12 its
+ * xs:duration canonicalization inverts the sign and drops the day component.
+ */
+static void
+xmlcast_validate_lexical(const char *str, Oid targetType)
+{
+	int			i = xmlcast_type_index(targetType);
+#ifdef LIBXML_SCHEMAS_ENABLED
+	xmlSchemaValType xsdtype;
+	const char *xsdname;
+	volatile xmlSchemaValPtr val = NULL;
+	PgXmlErrorContext *xmlerrcxt;
+	int			rc;
+#endif
+
+	Assert(i >= 0);
+
+	/*
+	 * Nothing to check for a character-string target: the lexical space of
+	 * xs:string admits anything.  Decide this before the guard below, so that
+	 * those casts keep working in a build without XML Schema support.  bytea
+	 * has no entry in the table (its XSD type depends on xmlbinary) but is
+	 * checked all the same.
+	 */
+	if (targetType != BYTEAOID && xmlcast_types[i].xsdname == NULL)
+		return;
+
+#ifdef LIBXML_SCHEMAS_ENABLED
+	if (targetType == BYTEAOID)
+	{
+		/*
+		 * Which of the two XSD binary types applies is the xmlbinary choice,
+		 * so the table cannot record it.  Note this is stricter than
+		 * binary_decode(), which ignores embedded whitespace in both
+		 * encodings: xs:base64Binary permits it, xs:hexBinary does not.
+		 */
+		if (xmlbinary == XMLBINARY_BASE64)
+		{
+			xsdtype = XML_SCHEMAS_BASE64BINARY;
+			xsdname = "xs:base64Binary";
+		}
+		else
+		{
+			xsdtype = XML_SCHEMAS_HEXBINARY;
+			xsdname = "xs:hexBinary";
+		}
+	}
+	else
+	{
+		/*
+		 * xs:double admits INF, -INF and NaN, but General Rule 4.i.v rejects
+		 * them for an approximate numeric target even so.  An infinite float
+		 * therefore does not survive a round trip through XML.
+		 */
+		if ((targetType == FLOAT4OID || targetType == FLOAT8OID) &&
+			(strcmp(str, "INF") == 0 || strcmp(str, "-INF") == 0 ||
+			 strcmp(str, "NaN") == 0))
+			ereport(ERROR,
+					(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+					 errmsg("cannot cast value \"%s\" to %s",
+							str, format_type_be(targetType)),
+					 errdetail("XMLCAST does not accept infinity or NaN for approximate numeric types.")));
+
+		xsdtype = (xmlSchemaValType) xmlcast_types[i].xsdtype;
+		xsdname = xmlcast_types[i].xsdname;
+	}
+
+	xmlerrcxt = pg_xml_init(PG_XML_STRICTNESS_ALL);
+
+	PG_TRY();
+	{
+		xmlSchemaTypePtr xsd;
+
+		/*
+		 * Required on older libxml2; newer versions initialize the built-in
+		 * types from xmlInitParser().  It guards itself against being called
+		 * twice, and its return type changed from void to int along the way,
+		 * so just call it and ignore the result.
+		 */
+		xmlSchemaInitTypes();
+
+		xsd = xmlSchemaGetBuiltInType(xsdtype);
+		if (xsd == NULL)
+			elog(ERROR, "could not find XML Schema built-in type %d",
+				 (int) xsdtype);
+
+		rc = xmlSchemaValidatePredefinedType(xsd, (const xmlChar *) str,
+											 (xmlSchemaValPtr *) &val);
+
+		/* the parsed value is libxml-allocated, so release it either way */
+		if (val != NULL)
+		{
+			xmlSchemaFreeValue(val);
+			val = NULL;
+		}
+
+		if (rc != 0)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_XML_CONTENT),
+					 errmsg("invalid %s value: \"%s\"", xsdname, str),
+					 errdetail("XMLCAST requires the XML value to be in the lexical space of %s.",
+							   xsdname)));
+
+		/* a lexically valid value should not have logged anything either */
+		if (xmlerrcxt->err_occurred)
+			xml_ereport(xmlerrcxt, ERROR, ERRCODE_INVALID_XML_CONTENT,
+						"could not validate XML value");
+	}
+	PG_CATCH();
+	{
+		if (val != NULL)
+			xmlSchemaFreeValue(val);
+
+		pg_xml_done(xmlerrcxt, true);
+
+		PG_RE_THROW();
+	}
+	PG_END_TRY();
+
+	pg_xml_done(xmlerrcxt, false);
+#else
+	/* transformXmlCast() rejects such a cast via xmlcast_can_validate() */
+	elog(ERROR, "XML Schema validation is not available in this build");
+#endif							/* LIBXML_SCHEMAS_ENABLED */
+}
+
+/*
+ * If a validated XSD date/time lexical form carries a time zone, return the
+ * offset of where it starts; otherwise return -1.
+ *
+ * The zone, when present, is a trailing "Z" or "+hh:mm"/"-hh:mm".  Testing
+ * for the sign alone is not enough: in a bare "2002-09-24" the character six
+ * from the end is the year-month separator.  Requiring the ":" of "hh:mm" in
+ * its place distinguishes the two, and also leaves a leading "-" (a negative
+ * year) alone.
+ */
+static int
+xsd_datetime_timezone_offset(const char *str)
+{
+	size_t		len = strlen(str);
+
+	if (len >= 1 && str[len - 1] == 'Z')
+		return (int) len - 1;
+
+	if (len >= 6 && (str[len - 6] == '+' || str[len - 6] == '-') &&
+		str[len - 3] == ':')
+		return (int) len - 6;
+
+	return -1;
+}
+
+/*
+ * Convert a validated XSD date/time lexical form to the requested SQL type.
+ *
+ * SQL/XML:2023 Subclause 6.7 General Rules 4.h.i-iii and 4.i.vii-viii pin
+ * down how time zones are handled, and it is not what the SQL input functions
+ * do on their own:
+ *
+ * - For a target WITHOUT TIME ZONE, a value that carries a zone is first
+ *   adjusted to UTC and only then stripped of it, so "12:00:00+06:00" becomes
+ *   06:00:00 rather than 12:00:00.
+ *
+ * - For a target WITH TIME ZONE, a value that carries no zone is taken to be
+ *   in UTC, rather than in the session's TimeZone.
+ */
+static Datum
+xmlcast_to_datetime(const char *str, Oid targetType)
+{
+	int			tzoff = xsd_datetime_timezone_offset(str);
+	bool		has_tz = (tzoff >= 0);
+	Datum		d;
+
+	switch (targetType)
+	{
+		case DATEOID:
+			if (!has_tz)
+				return DirectFunctionCall3(date_in, CStringGetDatum(str),
+										   ObjectIdGetDatum(InvalidOid),
+										   Int32GetDatum(-1));
+
+			/*
+			 * An xs:date with a zone denotes midnight in that zone; normalize
+			 * that instant to UTC and take the date it falls on, which may be
+			 * the day before or after the one written.
+			 */
+			d = DirectFunctionCall3(timestamptz_in,
+									CStringGetDatum(psprintf("%.*sT00:00:00%s",
+															 tzoff, str,
+															 str + tzoff)),
+									ObjectIdGetDatum(InvalidOid),
+									Int32GetDatum(-1));
+			d = DirectFunctionCall2(timestamptz_zone,
+									CStringGetTextDatum("UTC"), d);
+			return DirectFunctionCall1(timestamp_date, d);
+
+		case TIMESTAMPOID:
+			if (!has_tz)
+				return DirectFunctionCall3(timestamp_in, CStringGetDatum(str),
+										   ObjectIdGetDatum(InvalidOid),
+										   Int32GetDatum(-1));
+
+			d = DirectFunctionCall3(timestamptz_in, CStringGetDatum(str),
+									ObjectIdGetDatum(InvalidOid),
+									Int32GetDatum(-1));
+			return DirectFunctionCall2(timestamptz_zone,
+									   CStringGetTextDatum("UTC"), d);
+
+		case TIMESTAMPTZOID:
+			/* an absent zone means UTC, not the session's TimeZone */
+			return DirectFunctionCall3(timestamptz_in,
+									   CStringGetDatum(has_tz ? str :
+													   psprintf("%sZ", str)),
+									   ObjectIdGetDatum(InvalidOid),
+									   Int32GetDatum(-1));
+
+		case TIMEOID:
+			if (!has_tz)
+				return DirectFunctionCall3(time_in, CStringGetDatum(str),
+										   ObjectIdGetDatum(InvalidOid),
+										   Int32GetDatum(-1));
+
+			d = DirectFunctionCall3(timetz_in, CStringGetDatum(str),
+									ObjectIdGetDatum(InvalidOid),
+									Int32GetDatum(-1));
+			d = DirectFunctionCall2(timetz_zone, CStringGetTextDatum("UTC"), d);
+			return DirectFunctionCall1(timetz_time, d);
+
+		case TIMETZOID:
+			/* an absent zone means UTC, not the session's TimeZone */
+			return DirectFunctionCall3(timetz_in,
+									   CStringGetDatum(has_tz ? str :
+													   psprintf("%sZ", str)),
+									   ObjectIdGetDatum(InvalidOid),
+									   Int32GetDatum(-1));
+	}
+
+	elog(ERROR, "unexpected XMLCAST datetime target type: %u", targetType);
+}
+
+#endif							/* USE_LIBXML */
+
+/*
+ * Execute an IS_XMLCAST expression.
+ *
+ * sourceType is the OID of the input value's type; targetType is the OID of
+ * the requested output type.  The returned Datum has the type that
+ * xmlcast_result_type() reports for the expression; for the targets not
+ * listed there the result is text, which an outer cast node inserted by the
+ * parser converts the rest of the way.
+ *
+ * *isnull is set when the result is the null value, which General Rule 4.c
+ * calls for when the XML value atomizes to the empty sequence.  The caller
+ * must initialize it to false.
+ */
+Datum
+exec_xmlcast(Datum value, Oid sourceType, Oid targetType, bool *isnull)
+{
+#ifdef USE_LIBXML
+	switch (targetType)
+	{
+	case XMLOID:
+		/*
+		 * The SQL -> XML direction, General Rule 3.a: the value is written in
+		 * the lexical form of the XML Schema type that Subclause 9.8 pairs
+		 * with its SQL type.  A value with no such form is rejected rather
+		 * than written approximately.
+		 *
+		 * A case that breaks out of this switch is one whose XSD form
+		 * map_sql_value_to_xml_value() already produces.
+		 */
+		switch (sourceType)
+		{
+			case XMLOID:
+				/* already XML, so nothing to escape */
+				return PointerGetDatum(DatumGetXmlP(value));
+
+			case TEXTOID:
+			case VARCHAROID:
+			case BPCHAROID:
+				/* a character string; xmltext() escapes what XML reserves */
+				return PointerGetDatum(DatumGetXmlP(DirectFunctionCall1(xmltext,
+																		value)));
+
+			case FLOAT4OID:
+			case FLOAT8OID:
+				{
+					float8		val = (sourceType == FLOAT4OID) ?
+						(float8) DatumGetFloat4(value) : DatumGetFloat8(value);
+
+					/*
+					 * xs:double writes INF and -INF where float8out() writes
+					 * Infinity.  It spells NaN the same way, but say so here
+					 * rather than leave the agreement to chance.
+					 */
+					if (isinf(val))
+						return PointerGetDatum(cstring_to_xmltype(val < 0 ?
+																  "-INF" : "INF"));
+					if (isnan(val))
+						return PointerGetDatum(cstring_to_xmltype("NaN"));
+				}
+				break;
+
+			case NUMERICOID:
+				{
+					Numeric		num = DatumGetNumeric(value);
+
+					/* xs:decimal has neither infinities nor NaN */
+					if (numeric_is_inf(num) || numeric_is_nan(num))
+						ereport(ERROR,
+								(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+								 errmsg("numeric out of range"),
+								 errdetail("XML does not support infinite or NaN numeric values.")));
+				}
+				break;
+
+			case INTERVALOID:
+				{
+					Interval   *in = DatumGetIntervalP(value);
+					struct pg_itm tt,
+							   *itm = &tt;
+					char		buf[MAXDATELEN + 1];
+					bool		negative;
+
+					/*
+					 * Infinity has no representation in the XSD lexical space
+					 * for xs:duration.
+					 */
+					if (INTERVAL_NOT_FINITE(in))
+						ereport(ERROR,
+								(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+								 errmsg("interval out of range"),
+								 errdetail("XML does not support infinite interval values.")));
+
+					/*
+					 * xs:duration carries one sign for the whole value, in
+					 * front of the "P", so a duration whose fields disagree in
+					 * sign cannot be represented at all.  EncodeInterval()
+					 * would happily write "P1Y-1D", which is not in the
+					 * lexical space.
+					 */
+					negative = (in->month < 0 || in->day < 0 || in->time < 0);
+					if (negative &&
+						(in->month > 0 || in->day > 0 || in->time > 0))
+						ereport(ERROR,
+								(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+								 errmsg("interval cannot be represented as xs:duration"),
+								 errdetail("Intervals whose fields differ in sign have no xs:duration representation.")));
+
+					/* encode the magnitude and put the sign in front */
+					if (negative)
+						in = DatumGetIntervalP(DirectFunctionCall1(interval_um,
+																   IntervalPGetDatum(in)));
+
+					interval2itm(*in, itm);
+					EncodeInterval(itm, INTSTYLE_ISO_8601, buf);
+
+					if (negative)
+						return PointerGetDatum(cstring_to_xmltype(psprintf("-%s",
+																		   buf)));
+
+					return PointerGetDatum(cstring_to_xmltype(buf));
+				}
+
+			case TIMEOID:
+			case TIMETZOID:
+				{
+					/*
+					 * xs:time wants a two-part time zone offset ("+02:00"),
+					 * which is what USE_XSD_DATES produces; time_out() and
+					 * timetz_out() would give "+02".
+					 * map_sql_value_to_xml_value() has no case for these
+					 * types, and adding one there would change the output of
+					 * XMLELEMENT and friends, so handle them here.
+					 */
+					struct pg_tm tm;
+					fsec_t		fsec;
+					int			tz;
+					char		buf[MAXDATELEN + 1];
+
+					if (sourceType == TIMEOID)
+					{
+						time2tm(DatumGetTimeADT(value), &tm, &fsec);
+						EncodeTimeOnly(&tm, fsec, false, 0, USE_XSD_DATES, buf);
+					}
+					else
+					{
+						timetz2tm(DatumGetTimeTzADTP(value), &tm, &fsec, &tz);
+						EncodeTimeOnly(&tm, fsec, true, tz, USE_XSD_DATES, buf);
+					}
+
+					return PointerGetDatum(cstring_to_xmltype(buf));
+				}
+
+			case BOOLOID:
+			case DATEOID:
+			case TIMESTAMPOID:
+			case TIMESTAMPTZOID:
+			case BYTEAOID:
+				break;
+
+			default:
+				elog(ERROR, "unexpected XMLCAST source type: %u", sourceType);
+		}
+
+		return PointerGetDatum(cstring_to_xmltype(map_sql_value_to_xml_value(value,
+																			 sourceType,
+																			 false)));
+
+	default:
+	{
+		/*
+		 * The XML -> SQL direction, SQL/XML:2023 Subclause 6.7 General Rule
+		 * 4.  Atomize first (4.a, 4.b), turn the empty sequence into a null
+		 * (4.c), check the value against the lexical space of the XML Schema
+		 * type that Syntax Rule 15 pairs with the target, then build the
+		 * value.  Anything this switch does not convert itself is handed back
+		 * as text for the outer cast node to finish, which is General Rule
+		 * 4.j's "CAST (A AS SQLT)".
+		 */
+		bool		is_empty_sequence;
+		char	   *str;
+		Datum		res;
+
+		Assert(sourceType == XMLOID);
+
+		str = xmlcast_atomize(value, &is_empty_sequence);
+
+		if (is_empty_sequence)
+		{
+			*isnull = true;
+			return (Datum) 0;
+		}
+
+		/*
+		 * Apply the target's whiteSpace facet before anything looks at the
+		 * string.  xmlSchemaValidatePredefinedType() normalizes on the fly
+		 * and would accept a padded value either way, but the conversions
+		 * below inspect the string directly.
+		 */
+		if (xmlcast_types[xmlcast_type_index(targetType)].collapse_ws)
+		{
+			char	   *collapsed = xsd_collapse_whitespace(str);
+
+			pfree(str);
+			str = collapsed;
+		}
+
+		xmlcast_validate_lexical(str, targetType);
+
+		switch (targetType)
+		{
+			case BYTEAOID:
+				res = DirectFunctionCall2(binary_decode,
+										  CStringGetTextDatum(str),
+										  CStringGetTextDatum(xmlbinary == XMLBINARY_BASE64 ?
+															  "base64" : "hex"));
+				break;
+
+			case INTERVALOID:
+				{
+					/*
+					 * interval_in() cannot read the canonical xs:duration
+					 * spelling of a negative duration: it wants "P-1Y-2M"
+					 * where the standard writes "-P1Y2M".  Convert the
+					 * magnitude and negate.
+					 */
+					bool		negative = (str[0] == '-');
+
+					res = DirectFunctionCall3(interval_in,
+											  CStringGetDatum(negative ? str + 1 : str),
+											  ObjectIdGetDatum(InvalidOid),
+											  Int32GetDatum(-1));
+					if (negative)
+						res = DirectFunctionCall1(interval_um, res);
+				}
+				break;
+
+			case DATEOID:
+			case TIMEOID:
+			case TIMETZOID:
+			case TIMESTAMPOID:
+			case TIMESTAMPTZOID:
+				res = xmlcast_to_datetime(str, targetType);
+				break;
+
+			default:
+				/*
+				 * Everything else is handed to the outer cast node as text,
+				 * so the table had better not have claimed we build it here.
+				 */
+				Assert(xmlcast_result_type(targetType) == TEXTOID);
+				res = PointerGetDatum(cstring_to_text(str));
+				break;
+		}
+
+		pfree(str);
+		return res;
+	}
+	}
+#else
+	NO_XML_SUPPORT();
+	return (Datum)0;
+#endif
+}
 
 xmltype *
 xmlparse(text *data, XmlOptionType xmloption_arg, bool preserve_whitespace, Node *escontext)
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index a0ab2b885e8..7191cd440f1 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -896,6 +896,22 @@ typedef struct XmlSerialize
 	ParseLoc	location;		/* token location, or -1 if unknown */
 } XmlSerialize;
 
+/*
+ * XMLCAST (in raw parse tree only)
+ */
+typedef struct XmlCast
+{
+	NodeTag		type;
+	Node	   *expr;
+	TypeName   *typeName;
+	/*
+	 * Was BY REF or BY VALUE written?  Which one makes no difference, but
+	 * Syntax Rule 9 restricts where the clause may appear at all.
+	 */
+	bool		passing_mech;
+	ParseLoc	location;		/* token location, or -1 if unknown */
+} XmlCast;
+
 /* Partitioning related definitions */
 
 /*
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index 5a636d1f179..8aabbd9b080 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -1597,6 +1597,7 @@ typedef enum XmlExprOp
 	IS_XMLROOT,					/* XMLROOT(xml, version, standalone) */
 	IS_XMLSERIALIZE,			/* XMLSERIALIZE(is_document, xmlval, indent) */
 	IS_DOCUMENT,				/* xmlval IS DOCUMENT */
+	IS_XMLCAST,					/* XMLCAST(op AS datatype) */
 } XmlExprOp;
 
 typedef enum XmlOptionType
@@ -1622,9 +1623,18 @@ typedef struct XmlExpr
 	XmlOptionType xmloption pg_node_attr(query_jumble_ignore);
 	/* INDENT option for XMLSERIALIZE */
 	bool		indent;
-	/* target type/typmod for XMLSERIALIZE */
+	/*
+	 * For XMLSERIALIZE, the declared target type/typmod; the node itself
+	 * produces text, which an outer cast converts.  For XMLCAST, the
+	 * type/typmod of the value the node produces, which exprType() reports.
+	 */
 	Oid			type pg_node_attr(query_jumble_ignore);
 	int32		typmod pg_node_attr(query_jumble_ignore);
+	/* declared target type/typmod for XMLCAST */
+	Oid			targetType;
+	int32		targetTypmod;
+	/* type of the lone argument, for XMLCAST */
+	Oid			sourceType pg_node_attr(query_jumble_ignore);
 	/* token location, or -1 if unknown */
 	ParseLoc	location;
 } XmlExpr;
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index a12d550ef60..9849a201056 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -521,6 +521,7 @@ PG_KEYWORD("wrapper", WRAPPER, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("write", WRITE, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("xml", XML_P, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("xmlattributes", XMLATTRIBUTES, COL_NAME_KEYWORD, BARE_LABEL)
+PG_KEYWORD("xmlcast", XMLCAST, COL_NAME_KEYWORD, BARE_LABEL)
 PG_KEYWORD("xmlconcat", XMLCONCAT, COL_NAME_KEYWORD, BARE_LABEL)
 PG_KEYWORD("xmlelement", XMLELEMENT, COL_NAME_KEYWORD, BARE_LABEL)
 PG_KEYWORD("xmlexists", XMLEXISTS, COL_NAME_KEYWORD, BARE_LABEL)
diff --git a/src/include/utils/xml.h b/src/include/utils/xml.h
index ca266f448d6..29c9ad6d44e 100644
--- a/src/include/utils/xml.h
+++ b/src/include/utils/xml.h
@@ -79,6 +79,11 @@ extern xmltype *xmlroot(xmltype *data, text *version, int standalone);
 extern bool xml_is_document(xmltype *arg);
 extern text *xmltotext_with_options(xmltype *data, XmlOptionType xmloption_arg,
 									bool indent);
+extern bool xmlcast_target_type_supported(Oid targetType);
+extern Oid	xmlcast_result_type(Oid targetType);
+extern bool xmlcast_can_validate(Oid targetType);
+extern Datum exec_xmlcast(Datum value, Oid sourceType, Oid targetType,
+						  bool *isnull);
 extern char *escape_xml(const char *str);
 
 extern char *map_sql_identifier_to_xml_name(const char *ident, bool fully_escaped, bool escape_period);
diff --git a/src/test/regress/expected/xml.out b/src/test/regress/expected/xml.out
index 350941f7172..e1691992934 100644
--- a/src/test/regress/expected/xml.out
+++ b/src/test/regress/expected/xml.out
@@ -1902,3 +1902,1171 @@ SELECT xmltext('x'|| '<P>73</P>'::xml || .42 || true || 'j'::char);
  x&lt;P&gt;73&lt;/P&gt;0.42truej
 (1 row)
 
+-- for xmlcast() tests
+INSERT INTO xmltest
+ VALUES (42,
+'<?xml version="1.0" encoding="utf-8"?>
+ <xmlcast>
+  <period1>P1Y2M3DT4H5M6S</period1>
+  <period2>1 year 2 mons 3 days 4 hours 5 minutes 6 seconds</period2>
+  <period3>-P1Y2M3DT4H5M6S</period3>
+  <date1>2002-09-24</date1>
+  <date2>2002-09-24+06:00</date2>
+  <time>09:30:10.5</time>
+  <time_tz1>09:30:10Z</time_tz1>
+  <time_tz2>09:30:10-06:00</time_tz2>
+  <time_tz3>09:30:10+06:00</time_tz3>
+  <timestamp1>2002-05-30T09:00:00</timestamp1>
+  <timestamp2>2002-05-30T09:30:10.5</timestamp2>
+  <timestamp_tz1>2002-05-30T09:30:10Z</timestamp_tz1>
+  <timestamp_tz2>2002-05-30T09:30:10-06:00</timestamp_tz2>
+  <timestamp_tz3>2002-05-30T09:30:10+06:00</timestamp_tz3>
+  <text1>foo bar</text1>
+  <text2>       foo bar     </text2>
+  <text3>foo &amp; &lt;&quot;bar&quot;&gt;</text3>
+  <decimal1>42.7312345678910</decimal1>
+  <decimal2>+42.7312345678910</decimal2>
+  <decimal3>-42.7312345678910</decimal3>
+  <decimal4>INF</decimal4>
+  <decimal5>-INF</decimal5>
+  <decimal6>NaN</decimal6>
+  <integer1>42</integer1>
+  <integer2>+42</integer2>
+  <integer3>-42</integer3>
+  <long1>4273535420162021</long1>
+  <long2>+4273535420162021</long2>
+  <long3>-4273535420162021</long3>
+  <bool1 att="true">42</bool1>
+  <bool2 att="false">73</bool2>
+  <empty></empty>
+ </xmlcast>'::xml
+);
+-- This prevents the xmlcast regression tests from failing if the system's timezone has been changed.
+SET timezone TO 'America/Los_Angeles';
+-- xmlcast exceptions
+\set VERBOSITY terse
+SELECT xmlcast((xpath('//text1/text()', data))[1] AS text[]) FROM xmltest WHERE id = 42;
+ERROR:  cannot cast type xml to text[] at character 8
+SELECT xmlcast((xpath('//text1/integer1()', data))[1] AS int[]) FROM xmltest WHERE id = 42;
+ERROR:  cannot cast type xml to integer[] at character 8
+SELECT xmlcast(NULL AS text);
+ERROR:  cannot cast type unknown to text at character 8
+SELECT xmlcast('foo'::text AS varchar);
+ERROR:  cannot cast type text to character varying at character 8
+SELECT xmlcast(42 AS text);
+ERROR:  cannot cast type integer to text at character 8
+SELECT xmlcast(array['foo','bar'] AS xml);
+ERROR:  cannot cast type text[] to xml at character 8
+SELECT xmlcast('not-a-number'::xml AS integer);
+ERROR:  invalid xs:integer value: "not-a-number"
+SELECT xmlcast('not-a-date'::xml AS date);
+ERROR:  invalid xs:date value: "not-a-date"
+\set VERBOSITY default
+-- Both sides are matched against the same exact set of types, not against a
+-- type category, which says nothing about a type's representation: oid and
+-- money are TYPCATEGORY_NUMERIC and a user-defined type may declare any
+-- category it likes while being pass-by-value.  A domain does not launder an
+-- unsupported base type either, since the check is applied to the base type.
+CREATE DOMAIN xmlcast_doid AS oid;
+CREATE TYPE xmlcast_byval;
+CREATE FUNCTION xmlcast_byval_in(cstring) RETURNS xmlcast_byval
+  AS 'int4in' LANGUAGE internal IMMUTABLE STRICT;
+NOTICE:  return type xmlcast_byval is only a shell
+CREATE FUNCTION xmlcast_byval_out(xmlcast_byval) RETURNS cstring
+  AS 'int4out' LANGUAGE internal IMMUTABLE STRICT;
+NOTICE:  argument type xmlcast_byval is only a shell
+LINE 1: CREATE FUNCTION xmlcast_byval_out(xmlcast_byval) RETURNS cst...
+                                          ^
+CREATE TYPE xmlcast_byval (INPUT = xmlcast_byval_in, OUTPUT = xmlcast_byval_out,
+  INTERNALLENGTH = 4, PASSEDBYVALUE, CATEGORY = 'D');
+SELECT xmlcast('1'::xml AS oid);
+ERROR:  cannot cast type xml to oid
+LINE 1: SELECT xmlcast('1'::xml AS oid);
+               ^
+SELECT xmlcast('1'::xml AS money);
+ERROR:  cannot cast type xml to money
+LINE 1: SELECT xmlcast('1'::xml AS money);
+               ^
+SELECT xmlcast('1'::xml AS xmlcast_doid);
+ERROR:  cannot cast type xml to xmlcast_doid
+LINE 1: SELECT xmlcast('1'::xml AS xmlcast_doid);
+               ^
+SELECT xmlcast('1'::xml AS int[]);
+ERROR:  cannot cast type xml to integer[]
+LINE 1: SELECT xmlcast('1'::xml AS int[]);
+               ^
+SELECT xmlcast(1::oid AS xml);
+ERROR:  cannot cast type oid to xml
+LINE 1: SELECT xmlcast(1::oid AS xml);
+               ^
+SELECT xmlcast(1::money AS xml);
+ERROR:  cannot cast type money to xml
+LINE 1: SELECT xmlcast(1::money AS xml);
+               ^
+SELECT xmlcast('1'::xmlcast_byval AS xml);
+ERROR:  cannot cast type xmlcast_byval to xml
+LINE 1: SELECT xmlcast('1'::xmlcast_byval AS xml);
+               ^
+SELECT xmlcast('1'::xml AS xmlcast_byval);
+ERROR:  cannot cast type xml to xmlcast_byval
+LINE 1: SELECT xmlcast('1'::xml AS xmlcast_byval);
+               ^
+DROP TYPE xmlcast_byval CASCADE;
+NOTICE:  drop cascades to 2 other objects
+DETAIL:  drop cascades to function xmlcast_byval_in(cstring)
+drop cascades to function xmlcast_byval_out(xmlcast_byval)
+DROP DOMAIN xmlcast_doid;
+-- xmlcast tests for "XML to non-XML" expressions
+SELECT
+  xmlcast((xpath('//date1/text()', data))[1] AS date), pg_typeof(xmlcast((xpath('//date1/text()', data))[1] AS date)),
+  xmlcast((xpath('//date2/text()', data))[1] AS date), pg_typeof(xmlcast((xpath('//date2/text()', data))[1] AS date))
+FROM xmltest WHERE id = 42;
+  xmlcast   | pg_typeof |  xmlcast   | pg_typeof 
+------------+-----------+------------+-----------
+ 09-24-2002 | date      | 09-23-2002 | date
+(1 row)
+
+SELECT
+  xmlcast((xpath('//period1/text()', data))[1] AS interval), pg_typeof(xmlcast((xpath('//period1/text()', data))[1] AS interval)),
+  xmlcast((xpath('//period3/text()', data))[1] AS interval), pg_typeof(xmlcast((xpath('//period3/text()', data))[1] AS interval))
+FROM xmltest WHERE id = 42;
+                   xmlcast                    | pg_typeof |                     xmlcast                      | pg_typeof 
+----------------------------------------------+-----------+--------------------------------------------------+-----------
+ @ 1 year 2 mons 3 days 4 hours 5 mins 6 secs | interval  | @ 1 year 2 mons 3 days 4 hours 5 mins 6 secs ago | interval
+(1 row)
+
+-- period2 holds PostgreSQL interval syntax, which is not in the lexical space
+-- of xs:duration and is therefore rejected
+SELECT xmlcast((xpath('//period2/text()', data))[1] AS interval) FROM xmltest WHERE id = 42;
+ERROR:  invalid xs:duration value: "1 year 2 mons 3 days 4 hours 5 minutes 6 seconds"
+DETAIL:  XMLCAST requires the XML value to be in the lexical space of xs:duration.
+SELECT
+  xmlcast((xpath('//time/text()', data))[1] AS time), pg_typeof(xmlcast((xpath('//time/text()', data))[1] AS time)),
+  xmlcast((xpath('//time_tz1/text()', data))[1] AS time with time zone), pg_typeof(xmlcast((xpath('//time_tz1/text()', data))[1] AS time with time zone)),
+  xmlcast((xpath('//time_tz2/text()', data))[1] AS time with time zone), pg_typeof(xmlcast((xpath('//time_tz2/text()', data))[1] AS time with time zone)),
+  xmlcast((xpath('//time_tz3/text()', data))[1] AS time with time zone), pg_typeof(xmlcast((xpath('//time_tz3/text()', data))[1] AS time with time zone))
+FROM xmltest WHERE id = 42;
+  xmlcast   |       pg_typeof        |   xmlcast   |      pg_typeof      |   xmlcast   |      pg_typeof      |   xmlcast   |      pg_typeof      
+------------+------------------------+-------------+---------------------+-------------+---------------------+-------------+---------------------
+ 09:30:10.5 | time without time zone | 09:30:10+00 | time with time zone | 09:30:10-06 | time with time zone | 09:30:10+06 | time with time zone
+(1 row)
+
+SELECT
+  xmlcast((xpath('//text1/text()', data))[1] AS text), pg_typeof(xmlcast((xpath('//text1/text()', data))[1] AS text)),
+  xmlcast((xpath('//text2/text()', data))[1] AS text), pg_typeof(xmlcast((xpath('//text2/text()', data))[1] AS text)),
+  xmlcast((xpath('//text3/text()', data))[1] AS text), pg_typeof(xmlcast((xpath('//text3/text()', data))[1] AS text))
+FROM xmltest WHERE id = 42;
+ xmlcast | pg_typeof |       xmlcast       | pg_typeof |    xmlcast    | pg_typeof 
+---------+-----------+---------------------+-----------+---------------+-----------
+ foo bar | text      |        foo bar      | text      | foo & <"bar"> | text
+(1 row)
+
+SELECT
+  xmlcast((xpath('//text1/text()', data))[1] AS varchar), pg_typeof(xmlcast((xpath('//text1/text()', data))[1] AS varchar)),
+  xmlcast((xpath('//text2/text()', data))[1] AS varchar), pg_typeof(xmlcast((xpath('//text2/text()', data))[1] AS varchar)),
+  xmlcast((xpath('//text3/text()', data))[1] AS varchar), pg_typeof(xmlcast((xpath('//text3/text()', data))[1] AS varchar))
+FROM xmltest WHERE id = 42;
+ xmlcast |     pg_typeof     |       xmlcast       |     pg_typeof     |    xmlcast    |     pg_typeof     
+---------+-------------------+---------------------+-------------------+---------------+-------------------
+ foo bar | character varying |        foo bar      | character varying | foo & <"bar"> | character varying
+(1 row)
+
+SELECT
+  xmlcast((xpath('//text1/text()', data))[1] AS name), pg_typeof(xmlcast((xpath('//text1/text()', data))[1] AS name)),
+  xmlcast((xpath('//text2/text()', data))[1] AS name), pg_typeof(xmlcast((xpath('//text2/text()', data))[1] AS name)),
+  xmlcast((xpath('//text3/text()', data))[1] AS name), pg_typeof(xmlcast((xpath('//text3/text()', data))[1] AS name))
+FROM xmltest WHERE id = 42;
+ xmlcast | pg_typeof |       xmlcast       | pg_typeof |    xmlcast    | pg_typeof 
+---------+-----------+---------------------+-----------+---------------+-----------
+ foo bar | name      |        foo bar      | name      | foo & <"bar"> | name
+(1 row)
+
+SELECT
+  xmlcast((xpath('//text1/text()', data))[1] AS bpchar), pg_typeof(xmlcast((xpath('//text1/text()', data))[1] AS bpchar)),
+  xmlcast((xpath('//text2/text()', data))[1] AS bpchar), pg_typeof(xmlcast((xpath('//text2/text()', data))[1] AS bpchar)),
+  xmlcast((xpath('//text3/text()', data))[1] AS bpchar), pg_typeof(xmlcast((xpath('//text3/text()', data))[1] AS bpchar))
+FROM xmltest WHERE id = 42;
+ xmlcast | pg_typeof |       xmlcast       | pg_typeof |    xmlcast    | pg_typeof 
+---------+-----------+---------------------+-----------+---------------+-----------
+ foo bar | character |        foo bar      | character | foo & <"bar"> | character
+(1 row)
+
+SELECT
+  xmlcast((xpath('//decimal1/text()', data))[1] AS numeric), pg_typeof(xmlcast((xpath('//decimal1/text()', data))[1] AS numeric)),
+  xmlcast((xpath('//decimal2/text()', data))[1] AS numeric), pg_typeof(xmlcast((xpath('//decimal2/text()', data))[1] AS numeric)),
+  xmlcast((xpath('//decimal3/text()', data))[1] AS numeric), pg_typeof(xmlcast((xpath('//decimal3/text()', data))[1] AS numeric))
+FROM xmltest WHERE id = 42;
+     xmlcast      | pg_typeof |     xmlcast      | pg_typeof |      xmlcast      | pg_typeof 
+------------------+-----------+------------------+-----------+-------------------+-----------
+ 42.7312345678910 | numeric   | 42.7312345678910 | numeric   | -42.7312345678910 | numeric
+(1 row)
+
+-- decimal4/5/6 hold INF, -INF and NaN, which are outside the lexical space of
+-- xs:decimal and, for approximate numerics, rejected outright by General Rule
+-- 4.i.v
+\set VERBOSITY terse
+SELECT xmlcast((xpath('//decimal4/text()', data))[1] AS numeric) FROM xmltest WHERE id = 42;
+ERROR:  invalid xs:decimal value: "INF"
+SELECT xmlcast((xpath('//decimal6/text()', data))[1] AS numeric) FROM xmltest WHERE id = 42;
+ERROR:  invalid xs:decimal value: "NaN"
+SELECT xmlcast((xpath('//decimal4/text()', data))[1] AS double precision) FROM xmltest WHERE id = 42;
+ERROR:  cannot cast value "INF" to double precision
+SELECT xmlcast((xpath('//decimal6/text()', data))[1] AS double precision) FROM xmltest WHERE id = 42;
+ERROR:  cannot cast value "NaN" to double precision
+\set VERBOSITY default
+SELECT
+  xmlcast((xpath('//decimal1/text()', data))[1] AS double precision), pg_typeof(xmlcast((xpath('//decimal1/text()', data))[1] AS double precision)),
+  xmlcast((xpath('//decimal2/text()', data))[1] AS double precision), pg_typeof(xmlcast((xpath('//decimal2/text()', data))[1] AS double precision)),
+  xmlcast((xpath('//decimal3/text()', data))[1] AS double precision), pg_typeof(xmlcast((xpath('//decimal3/text()', data))[1] AS double precision))
+FROM xmltest WHERE id = 42;
+     xmlcast     |    pg_typeof     |     xmlcast     |    pg_typeof     |     xmlcast      |    pg_typeof     
+-----------------+------------------+-----------------+------------------+------------------+------------------
+ 42.731234567891 | double precision | 42.731234567891 | double precision | -42.731234567891 | double precision
+(1 row)
+
+SELECT
+  xmlcast((xpath('//integer1/text()', data))[1] AS int), pg_typeof(xmlcast((xpath('//integer1/text()', data))[1] AS int)),
+  xmlcast((xpath('//integer2/text()', data))[1] AS int), pg_typeof(xmlcast((xpath('//integer2/text()', data))[1] AS int)),
+  xmlcast((xpath('//integer3/text()', data))[1] AS int), pg_typeof(xmlcast((xpath('//integer3/text()', data))[1] AS int))
+FROM xmltest WHERE id = 42;
+ xmlcast | pg_typeof | xmlcast | pg_typeof | xmlcast | pg_typeof 
+---------+-----------+---------+-----------+---------+-----------
+      42 | integer   |      42 | integer   |     -42 | integer
+(1 row)
+
+SELECT
+  xmlcast((xpath('//long1/text()', data))[1] AS bigint), pg_typeof(xmlcast((xpath('//long1/text()', data))[1] AS bigint)),
+  xmlcast((xpath('//long2/text()', data))[1] AS bigint), pg_typeof(xmlcast((xpath('//long2/text()', data))[1] AS bigint)),
+  xmlcast((xpath('//long3/text()', data))[1] AS bigint), pg_typeof(xmlcast((xpath('//long3/text()', data))[1] AS bigint))
+FROM xmltest WHERE id = 42;
+     xmlcast      | pg_typeof |     xmlcast      | pg_typeof |      xmlcast      | pg_typeof 
+------------------+-----------+------------------+-----------+-------------------+-----------
+ 4273535420162021 | bigint    | 4273535420162021 | bigint    | -4273535420162021 | bigint
+(1 row)
+
+SELECT
+  xmlcast((xpath('//bool1/@att', data))[1] AS boolean), pg_typeof(xmlcast((xpath('//bool1/@att', data))[1] AS boolean)),
+  xmlcast((xpath('//bool2/@att', data))[1] AS boolean), pg_typeof(xmlcast((xpath('//bool1/@att', data))[1] AS boolean))
+FROM xmltest WHERE id = 42;
+ xmlcast | pg_typeof | xmlcast | pg_typeof 
+---------+-----------+---------+-----------
+ t       | boolean   | f       | boolean
+(1 row)
+
+SELECT xmlcast((xpath('//empty/text()', data))[1] AS text), pg_typeof(xmlcast((xpath('//empty/text()', data))[1] AS text))
+FROM xmltest WHERE id = 42;
+ xmlcast | pg_typeof 
+---------+-----------
+         | text
+(1 row)
+
+-- xmlcast tests for "XML to XML" expressions
+SELECT
+  xmlcast((xpath('//text1/text()', data))[1] AS xml), pg_typeof(xmlcast((xpath('//text1/text()', data))[1] AS xml)),
+  xmlcast((xpath('//text2/text()', data))[1] AS xml), pg_typeof(xmlcast((xpath('//text2/text()', data))[1] AS xml)),
+  xmlcast((xpath('//text3/text()', data))[1] AS xml), pg_typeof(xmlcast((xpath('//text3/text()', data))[1] AS xml))
+FROM xmltest WHERE id = 42;
+ xmlcast | pg_typeof |       xmlcast       | pg_typeof |         xmlcast         | pg_typeof 
+---------+-----------+---------------------+-----------+-------------------------+-----------
+ foo bar | xml       |        foo bar      | xml       | foo &amp; &lt;"bar"&gt; | xml
+(1 row)
+
+SELECT
+  xmlcast((xpath('//timestamp1/text()', data))[1] AS timestamp), pg_typeof(xmlcast((xpath('//timestamp1/text()', data))[1] AS timestamp)),
+  xmlcast((xpath('//timestamp2/text()', data))[1] AS timestamp), pg_typeof(xmlcast((xpath('//timestamp2/text()', data))[1] AS timestamp))
+FROM xmltest WHERE id = 42;
+         xmlcast          |          pg_typeof          |          xmlcast           |          pg_typeof          
+--------------------------+-----------------------------+----------------------------+-----------------------------
+ Thu May 30 09:00:00 2002 | timestamp without time zone | Thu May 30 09:30:10.5 2002 | timestamp without time zone
+(1 row)
+
+SELECT
+  xmlcast((xpath('//timestamp_tz1/text()', data))[1] AS timestamp with time zone), pg_typeof(xmlcast((xpath('//timestamp_tz1/text()', data))[1] AS timestamp with time zone)),
+  xmlcast((xpath('//timestamp_tz2/text()', data))[1] AS timestamp with time zone), pg_typeof(xmlcast((xpath('//timestamp_tz2/text()', data))[1] AS timestamp with time zone)),
+  xmlcast((xpath('//timestamp_tz3/text()', data))[1] AS timestamp with time zone), pg_typeof(xmlcast((xpath('//timestamp_tz3/text()', data))[1] AS timestamp with time zone))
+FROM xmltest WHERE id = 42;
+           xmlcast            |        pg_typeof         |           xmlcast            |        pg_typeof         |           xmlcast            |        pg_typeof         
+------------------------------+--------------------------+------------------------------+--------------------------+------------------------------+--------------------------
+ Thu May 30 02:30:10 2002 PDT | timestamp with time zone | Thu May 30 08:30:10 2002 PDT | timestamp with time zone | Wed May 29 20:30:10 2002 PDT | timestamp with time zone
+(1 row)
+
+-- xmlcast tests for "non-XML to XML" expressions
+SELECT j, pg_typeof(j) FROM xmlcast(NULL AS xml) t(j);
+ j | pg_typeof 
+---+-----------
+   | xml
+(1 row)
+
+SELECT j, pg_typeof(j) FROM xmlcast('foo' AS xml) t(j);
+  j  | pg_typeof 
+-----+-----------
+ foo | xml
+(1 row)
+
+SELECT j, pg_typeof(j) FROM xmlcast(''::text AS xml) t(j);
+ j | pg_typeof 
+---+-----------
+   | xml
+(1 row)
+
+SELECT j, pg_typeof(j) FROM xmlcast(NULL::text AS xml) t(j);
+ j | pg_typeof 
+---+-----------
+   | xml
+(1 row)
+
+SELECT j, pg_typeof(j) FROM xmlcast(''::xml AS text) t(j);
+ j | pg_typeof 
+---+-----------
+   | text
+(1 row)
+
+SELECT j, pg_typeof(j) FROM xmlcast(NULL::xml AS text) t(j);
+ j | pg_typeof 
+---+-----------
+   | text
+(1 row)
+
+SELECT j, pg_typeof(j) FROM xmlcast('foo & <"bar">'::text AS xml) t(j);
+                 j                 | pg_typeof 
+-----------------------------------+-----------
+ foo &amp; &lt;&quot;bar&quot;&gt; | xml
+(1 row)
+
+SELECT j, pg_typeof(j) FROM xmlcast('foo & <"bar">'::varchar AS xml) t(j);
+                 j                 | pg_typeof 
+-----------------------------------+-----------
+ foo &amp; &lt;&quot;bar&quot;&gt; | xml
+(1 row)
+
+SELECT j, pg_typeof(j) FROM xmlcast('foo & <"bar">'::name AS xml) t(j);
+                 j                 | pg_typeof 
+-----------------------------------+-----------
+ foo &amp; &lt;&quot;bar&quot;&gt; | xml
+(1 row)
+
+SELECT j, pg_typeof(j) FROM xmlcast(xmltext(E'foo & <"bar">\r') AS text) t(j);
+        j        | pg_typeof 
+-----------------+-----------
+ foo & <"bar">\r | text
+(1 row)
+
+SELECT j, pg_typeof(j) FROM xmlcast(xmlcast(E'foo & <"bar">\r' AS xml) AS text) t(j);
+        j        | pg_typeof 
+-----------------+-----------
+ foo & <"bar">\r | text
+(1 row)
+
+SELECT j, pg_typeof(j) FROM xmlcast(to_date('29/05/2024','dd/mm/yyyy') AS xml) t(j);
+     j      | pg_typeof 
+------------+-----------
+ 2024-05-29 | xml
+(1 row)
+
+SELECT j, pg_typeof(j) FROM xmlcast('2024-05-29 12:04:10.703585+02'::timestamp with time zone at time zone 'Europe/Berlin' AS xml) t(j);
+             j              | pg_typeof 
+----------------------------+-----------
+ 2024-05-29T12:04:10.703585 | xml
+(1 row)
+
+SELECT j, pg_typeof(j) FROM xmlcast('2024-05-29 12:04:10.703585+02'::timestamp without time zone AS xml) t(j);
+             j              | pg_typeof 
+----------------------------+-----------
+ 2024-05-29T12:04:10.703585 | xml
+(1 row)
+
+SELECT j, pg_typeof(j) FROM xmlcast('1 year 2 months 3 days 4 hours 5 minutes 6 seconds'::interval AS xml) t(j);
+       j        | pg_typeof 
+----------------+-----------
+ P1Y2M3DT4H5M6S | xml
+(1 row)
+
+SELECT j, pg_typeof(j) FROM xmlcast(42::smallint AS xml) t(j);
+ j  | pg_typeof 
+----+-----------
+ 42 | xml
+(1 row)
+
+SELECT j, pg_typeof(j) FROM xmlcast(427353542 AS xml) t(j);
+     j     | pg_typeof 
+-----------+-----------
+ 427353542 | xml
+(1 row)
+
+SELECT j, pg_typeof(j) FROM xmlcast(4273535420162021 AS xml) t(j);
+        j         | pg_typeof 
+------------------+-----------
+ 4273535420162021 | xml
+(1 row)
+
+SELECT j, pg_typeof(j) FROM xmlcast(42.007312345678910 AS xml) t(j);
+         j          | pg_typeof 
+--------------------+-----------
+ 42.007312345678910 | xml
+(1 row)
+
+SELECT j, pg_typeof(j) FROM xmlcast(42.007312345678910::double precision AS xml) t(j);
+         j         | pg_typeof 
+-------------------+-----------
+ 42.00731234567891 | xml
+(1 row)
+
+SELECT j, pg_typeof(j) FROM xmlcast(42.0::real AS xml) t(j);
+ j  | pg_typeof 
+----+-----------
+ 42 | xml
+(1 row)
+
+SELECT j, pg_typeof(j) FROM xmlcast('infinity'::real AS xml) t(j);
+  j  | pg_typeof 
+-----+-----------
+ INF | xml
+(1 row)
+
+SELECT j, pg_typeof(j) FROM xmlcast('-infinity'::real AS xml) t(j);
+  j   | pg_typeof 
+------+-----------
+ -INF | xml
+(1 row)
+
+SELECT j, pg_typeof(j) FROM xmlcast('nan'::real AS xml) t(j);
+  j  | pg_typeof 
+-----+-----------
+ NaN | xml
+(1 row)
+
+SELECT j, pg_typeof(j) FROM xmlcast('infinity'::double precision AS xml) t(j);
+  j  | pg_typeof 
+-----+-----------
+ INF | xml
+(1 row)
+
+SELECT j, pg_typeof(j) FROM xmlcast('-infinity'::double precision AS xml) t(j);
+  j   | pg_typeof 
+------+-----------
+ -INF | xml
+(1 row)
+
+SELECT j, pg_typeof(j) FROM xmlcast('nan'::double precision AS xml) t(j);
+  j  | pg_typeof 
+-----+-----------
+ NaN | xml
+(1 row)
+
+SELECT j, pg_typeof(j) FROM xmlcast('infinity'::numeric AS xml) t(j);
+ERROR:  numeric out of range
+DETAIL:  XML does not support infinite or NaN numeric values.
+SELECT j, pg_typeof(j) FROM xmlcast('-infinity'::numeric AS xml) t(j);
+ERROR:  numeric out of range
+DETAIL:  XML does not support infinite or NaN numeric values.
+SELECT j, pg_typeof(j) FROM xmlcast('nan'::numeric AS xml) t(j);
+ERROR:  numeric out of range
+DETAIL:  XML does not support infinite or NaN numeric values.
+SELECT j, pg_typeof(j) FROM xmlcast(true AS xml) t(j);
+  j   | pg_typeof 
+------+-----------
+ true | xml
+(1 row)
+
+SELECT j, pg_typeof(j) FROM xmlcast(false AS xml) t(j);
+   j   | pg_typeof 
+-------+-----------
+ false | xml
+(1 row)
+
+SELECT j, pg_typeof(j) FROM xmlcast(42 = 73 AS xml) t(j);
+   j   | pg_typeof 
+-------+-----------
+ false | xml
+(1 row)
+
+SELECT j, pg_typeof(j) FROM xmlcast(42 <> 73 AS xml) t(j);
+  j   | pg_typeof 
+------+-----------
+ true | xml
+(1 row)
+
+SELECT j, pg_typeof(j) FROM xmlcast('11:11:11.5'::time AS xml) t(j);
+     j      | pg_typeof 
+------------+-----------
+ 11:11:11.5 | xml
+(1 row)
+
+SELECT j, pg_typeof(j) FROM xmlcast('11:11:11.5+01'::time with time zone AS xml) t(j);
+        j         | pg_typeof 
+------------------+-----------
+ 11:11:11.5+01:00 | xml
+(1 row)
+
+SELECT j, pg_typeof(j) FROM xmlcast('infinity'::interval AS xml) t(j);
+ERROR:  interval out of range
+DETAIL:  XML does not support infinite interval values.
+SELECT j, pg_typeof(j) FROM xmlcast('-infinity'::interval AS xml) t(j);
+ERROR:  interval out of range
+DETAIL:  XML does not support infinite interval values.
+-- The XML side of a binary string is xs:hexBinary or xs:base64Binary,
+-- selected by xmlbinary in both directions, so bytea round-trips under
+-- either setting.
+SET xmlbinary TO hex;
+SELECT xmlcast(E'\\xDEADBEEF'::bytea AS xml);
+ xmlcast  
+----------
+ DEADBEEF
+(1 row)
+
+SELECT xmlcast(xmlcast(E'\\xDEADBEEF'::bytea AS xml) AS bytea);
+  xmlcast   
+------------
+ \xdeadbeef
+(1 row)
+
+SET xmlbinary TO base64;
+SELECT xmlcast(E'\\xDEADBEEF'::bytea AS xml);
+ xmlcast  
+----------
+ 3q2+7w==
+(1 row)
+
+SELECT xmlcast(xmlcast(E'\\xDEADBEEF'::bytea AS xml) AS bytea);
+  xmlcast   
+------------
+ \xdeadbeef
+(1 row)
+
+-- xmlcast() to bytea yields bytea directly, not text run through byteain()
+SELECT pg_typeof(xmlcast('QQ=='::xml AS bytea));
+ pg_typeof 
+-----------
+ bytea
+(1 row)
+
+-- edge values: empty, an embedded NUL, and bytes with the high bit set
+SET xmlbinary TO hex;
+SELECT xmlcast(xmlcast(''::bytea AS xml) AS bytea) IS NULL AS empty_is_null,
+       xmlcast(xmlcast(E'\\x00'::bytea AS xml) AS bytea),
+       xmlcast(xmlcast(E'\\xFF00FF'::bytea AS xml) AS bytea);
+ empty_is_null | xmlcast | xmlcast  
+---------------+---------+----------
+ t             | \x00    | \xff00ff
+(1 row)
+
+SET xmlbinary TO base64;
+SELECT xmlcast(xmlcast(''::bytea AS xml) AS bytea) IS NULL AS empty_is_null,
+       xmlcast(xmlcast(E'\\x00'::bytea AS xml) AS bytea),
+       xmlcast(xmlcast(E'\\xFF00FF'::bytea AS xml) AS bytea);
+ empty_is_null | xmlcast | xmlcast  
+---------------+---------+----------
+ t             | \x00    | \xff00ff
+(1 row)
+
+-- xs:hexBinary collapses leading/trailing whitespace but forbids embedded
+-- whitespace; xs:base64Binary permits it
+SET xmlbinary TO hex;
+SELECT xmlcast('  DEAD  '::xml AS bytea);
+ xmlcast 
+---------
+ \xdead
+(1 row)
+
+SELECT xmlcast('DE AD'::xml AS bytea);
+ERROR:  invalid xs:hexBinary value: "DE AD"
+DETAIL:  XMLCAST requires the XML value to be in the lexical space of xs:hexBinary.
+SET xmlbinary TO base64;
+SELECT xmlcast('3q2 +7w=='::xml AS bytea);
+  xmlcast   
+------------
+ \xdeadbeef
+(1 row)
+
+-- invalid lexical forms are rejected
+\set VERBOSITY terse
+SET xmlbinary TO hex;
+SELECT xmlcast('DEA'::xml AS bytea);
+ERROR:  invalid xs:hexBinary value: "DEA"
+SELECT xmlcast('zz'::xml AS bytea);
+ERROR:  invalid xs:hexBinary value: "zz"
+SET xmlbinary TO base64;
+SELECT xmlcast('QQ='::xml AS bytea);
+ERROR:  invalid xs:base64Binary value: "QQ="
+\set VERBOSITY default
+-- bytea is not collatable; exercise the collation-assignment path
+SET xmlbinary TO hex;
+SELECT xmlcast(v AS bytea) FROM (VALUES ('41'::xml), ('42'::xml)) t(v) ORDER BY 1;
+ xmlcast 
+---------
+ \x41
+ \x42
+(2 rows)
+
+-- a natively produced target needs no outer cast node, unlike int below
+CREATE VIEW xmlcast_bytea_view AS
+  SELECT xmlcast('DEADBEEF'::xml AS bytea) AS b, xmlcast('42'::xml AS int) AS i;
+\sv xmlcast_bytea_view
+CREATE OR REPLACE VIEW public.xmlcast_bytea_view AS
+ SELECT XMLCAST('DEADBEEF'::xml AS bytea) AS b,
+    XMLCAST('42'::xml AS integer)::integer AS i
+DROP VIEW xmlcast_bytea_view;
+SET xmlbinary TO base64;
+-- XSD lexical validation on the XML -> SQL direction.  A SQL input function
+-- accepts whatever PostgreSQL accepts, which is both wider than the XSD
+-- lexical space and, for dates and timestamps, dependent on DateStyle;
+-- XMLCAST pins the input to the XML Schema lexical form instead.
+\set VERBOSITY terse
+SELECT xmlcast('yes'::xml AS boolean);
+ERROR:  invalid xs:boolean value: "yes"
+SELECT xmlcast('on'::xml AS boolean);
+ERROR:  invalid xs:boolean value: "on"
+SELECT xmlcast('t'::xml AS boolean);
+ERROR:  invalid xs:boolean value: "t"
+SELECT xmlcast('0x10'::xml AS int);
+ERROR:  invalid xs:integer value: "0x10"
+SELECT xmlcast('1e2'::xml AS numeric);
+ERROR:  invalid xs:decimal value: "1e2"
+SELECT xmlcast('99999'::xml AS int2);
+ERROR:  value "99999" is out of range for type smallint
+SELECT xmlcast('Infinity'::xml AS float8);
+ERROR:  invalid xs:double value: "Infinity"
+SELECT xmlcast('3 days'::xml AS interval);
+ERROR:  invalid xs:duration value: "3 days"
+SELECT xmlcast('P1Y-1D'::xml AS interval);
+ERROR:  invalid xs:duration value: "P1Y-1D"
+-- xs:duration has no week designator, though interval input syntax does
+SELECT xmlcast('P3W'::xml AS interval);
+ERROR:  invalid xs:duration value: "P3W"
+SELECT xmlcast('2024-01-01 12:00:00'::xml AS timestamp);
+ERROR:  invalid xs:dateTime value: "2024-01-01 12:00:00"
+SELECT xmlcast('12:00:00+02'::xml AS timetz);
+ERROR:  invalid xs:time value: "12:00:00+02"
+\set VERBOSITY default
+-- ... while the XSD lexical forms are accepted, including the ones a SQL
+-- input function would reject on its own
+SELECT xmlcast('1'::xml AS boolean), xmlcast('0'::xml AS boolean),
+       xmlcast('-P1Y2M'::xml AS interval),
+       xmlcast('12:00:00+02:00'::xml AS timetz);
+ xmlcast | xmlcast |       xmlcast       |   xmlcast   
+---------+---------+---------------------+-------------
+ t       | f       | @ 1 year 2 mons ago | 12:00:00+02
+(1 row)
+
+-- General Rule 4.i.v: an approximate numeric target rejects INF/-INF/NaN even
+-- though xs:double admits them, and xs:decimal has no such values at all.
+-- Mapping an infinite float to XML and back is therefore not a round trip.
+\set VERBOSITY terse
+SELECT xmlcast('INF'::xml AS float8);
+ERROR:  cannot cast value "INF" to double precision
+SELECT xmlcast('-INF'::xml AS float8);
+ERROR:  cannot cast value "-INF" to double precision
+SELECT xmlcast('NaN'::xml AS float8);
+ERROR:  cannot cast value "NaN" to double precision
+SELECT xmlcast('INF'::xml AS numeric);
+ERROR:  invalid xs:decimal value: "INF"
+SELECT xmlcast('NaN'::xml AS numeric);
+ERROR:  invalid xs:decimal value: "NaN"
+\set VERBOSITY default
+-- Syntax Rule 15 picks the XSD type from the SQL target, so the very same
+-- lexical form is accepted or rejected depending on where it is going:
+-- exponents belong to xs:double but not to xs:decimal or xs:integer, and a
+-- fraction belongs to neither xs:integer nor an integer target.
+SELECT xmlcast('1e2'::xml AS float8) AS e_lower,
+       xmlcast('1E2'::xml AS float8) AS e_upper,
+       xmlcast('-1.5e-3'::xml AS float8) AS e_signed,
+       xmlcast('1.0'::xml AS float8) AS frac_float,
+       xmlcast('.5'::xml AS float8) AS leading_dot,
+       xmlcast('1.0'::xml AS numeric) AS frac_numeric;
+ e_lower | e_upper | e_signed | frac_float | leading_dot | frac_numeric 
+---------+---------+----------+------------+-------------+--------------
+     100 |     100 |  -0.0015 |          1 |         0.5 |          1.0
+(1 row)
+
+\set VERBOSITY terse
+SELECT xmlcast('1E2'::xml AS numeric);
+ERROR:  invalid xs:decimal value: "1E2"
+SELECT xmlcast('1e2'::xml AS int);
+ERROR:  invalid xs:integer value: "1e2"
+SELECT xmlcast('1.0'::xml AS int);
+ERROR:  invalid xs:integer value: "1.0"
+SELECT xmlcast('.5'::xml AS int);
+ERROR:  invalid xs:integer value: ".5"
+-- "Infinity" is not in the lexical space of xs:double at all, whereas "INF"
+-- is but General Rule 4.i.v refuses it: two different errors
+SELECT xmlcast('Infinity'::xml AS float8);
+ERROR:  invalid xs:double value: "Infinity"
+SELECT xmlcast('INF'::xml AS float8);
+ERROR:  cannot cast value "INF" to double precision
+\set VERBOSITY default
+-- General Rule 4.c: a value that atomizes to the empty sequence is null,
+-- which is distinct from one whose string value is empty
+SELECT xmlcast(''::xml AS int) IS NULL AS empty_is_null,
+       xmlcast(''::xml AS text) IS NULL AS empty_text_is_null,
+       xmlcast('<a></a>'::xml AS text) = '' AS elem_is_empty_string;
+ empty_is_null | empty_text_is_null | elem_is_empty_string 
+---------------+--------------------+----------------------
+ t             | t                  | t
+(1 row)
+
+-- General Rules 4.a and 4.b: the value is atomized with fn:data(), so markup
+-- is stripped, references of every spelling are resolved, CDATA is unwrapped
+-- and comments contribute nothing
+SELECT xmlcast('<a>x</a>'::xml AS text) AS markup,
+       xmlcast('&lt;a&gt;x&lt;/a&gt;'::xml AS text) AS entities,
+       xmlcast('<a>x</a>'::xml AS text) = xmlcast('&lt;a&gt;x&lt;/a&gt;'::xml AS text) AS collide;
+ markup | entities | collide 
+--------+----------+---------
+ x      | <a>x</a> | f
+(1 row)
+
+SELECT xmlcast('&apos;'::xml AS text) AS apos,
+       xmlcast('&#65;'::xml AS text) AS dec_ref,
+       xmlcast('&#x41;'::xml AS text) AS hex_ref,
+       xmlcast('&#x0d;'::xml AS text) = E'\r' AS cr_lower,
+       xmlcast('&#x0D;'::xml AS text) = E'\r' AS cr_upper,
+       xmlcast('&#13;'::xml AS text) = E'\r' AS cr_dec,
+       xmlcast('<![CDATA[a<b]]>'::xml AS text) AS lone_cdata,
+       xmlcast('a<![CDATA[b]]>c'::xml AS text) AS text_cdata_run,
+       xmlcast('<!--c-->'::xml AS text) AS lone_comment,
+       xmlcast('<a>42</a>'::xml AS int) AS elem_to_int;
+ apos | dec_ref | hex_ref | cr_lower | cr_upper | cr_dec | lone_cdata | text_cdata_run | lone_comment | elem_to_int 
+------+---------+---------+----------+----------+--------+------------+----------------+--------------+-------------
+ '    | A       | A       | t        | t        | t      | a<b        | abc            | c            |          42
+(1 row)
+
+-- General Rule 4.h casts the atomized sequence to a single value, which an
+-- XQuery cast can only do for one item.  Two or more items is an error, not a
+-- concatenation -- otherwise <a>1</a><b>2</b> would silently become 12.
+\set VERBOSITY terse
+SELECT xmlcast('<a/> <b/>'::xml AS text);
+ERROR:  XMLCAST operand must atomize to a single value
+SELECT xmlcast('<a>1</a><b>2</b>'::xml AS int);
+ERROR:  XMLCAST operand must atomize to a single value
+SELECT xmlcast('foo<a/>'::xml AS text);
+ERROR:  XMLCAST operand must atomize to a single value
+SELECT xmlcast('pre<!--c-->post'::xml AS text);
+ERROR:  XMLCAST operand must atomize to a single value
+\set VERBOSITY default
+-- ... but a single element whose content spans several nodes is one item, and
+-- its string value is legitimately the concatenation
+SELECT xmlcast('<x><y>bar</y>foo</x>'::xml AS text) AS one_element,
+       xmlcast('<x>pre<!--c-->post</x>'::xml AS text) AS comment_inside_element;
+ one_element | comment_inside_element 
+-------------+------------------------
+ barfoo      | prepost
+(1 row)
+
+-- A DTD is not a node in the XQuery data model, even though libxml links the
+-- internal subset into the document's child list, so it is not an item.
+SELECT xmlcast('<!DOCTYPE a><a>42</a>'::xml AS int) AS doctype,
+       xmlcast('<!DOCTYPE a [<!ELEMENT a (#PCDATA)>]><a>42</a>'::xml AS int) AS internal_subset,
+       xmlcast('<?xml version="1.0"?><!DOCTYPE a><a>x</a>'::xml AS text) AS decl_and_dtd,
+       xmlcast('<!DOCTYPE a [<!ENTITY e "hi">]><a>&e;</a>'::xml AS text) AS entity_in_subset;
+ doctype | internal_subset | decl_and_dtd | entity_in_subset 
+---------+-----------------+--------------+------------------
+      42 |              42 | x            | hi
+(1 row)
+
+-- A comment or processing instruction in the prolog, by contrast, is an item
+\set VERBOSITY terse
+SELECT xmlcast('<!--c--><a>42</a>'::xml AS int);
+ERROR:  XMLCAST operand must atomize to a single value
+SELECT xmlcast('<?pi?><a>42</a>'::xml AS int);
+ERROR:  XMLCAST operand must atomize to a single value
+\set VERBOSITY default
+-- General Rules 4.h.i-iii: a target WITHOUT TIME ZONE normalizes to UTC
+-- before dropping the zone, rather than ignoring it; and 4.i.vii-viii: an
+-- absent zone means UTC, not the session's TimeZone
+SET TimeZone TO 'America/Los_Angeles';
+SELECT xmlcast('2002-09-24+06:00'::xml AS date) AS d_plus,
+       xmlcast('2002-09-24-06:00'::xml AS date) AS d_minus,
+       xmlcast('2002-09-24Z'::xml AS date) AS d_z,
+       xmlcast('2002-09-24'::xml AS date) AS d_none;
+   d_plus   |  d_minus   |    d_z     |   d_none   
+------------+------------+------------+------------
+ 09-23-2002 | 09-24-2002 | 09-24-2002 | 09-24-2002
+(1 row)
+
+SELECT xmlcast('2024-01-01T12:00:00+06:00'::xml AS timestamp) AS ts_plus,
+       xmlcast('09:30:10-06:00'::xml AS time) AS t_minus,
+       xmlcast('12:00:00'::xml AS timetz) AS timetz_none;
+         ts_plus          | t_minus  | timetz_none 
+--------------------------+----------+-------------
+ Mon Jan 01 06:00:00 2024 | 15:30:10 | 12:00:00+00
+(1 row)
+
+SET TimeZone TO 'Asia/Tokyo';
+SELECT xmlcast('2002-09-24+06:00'::xml AS date) AS d_plus,
+       xmlcast('2024-01-01T12:00:00+06:00'::xml AS timestamp) AS ts_plus,
+       xmlcast('09:30:10-06:00'::xml AS time) AS t_minus,
+       xmlcast('12:00:00'::xml AS timetz) AS timetz_none;
+   d_plus   |         ts_plus          | t_minus  | timetz_none 
+------------+--------------------------+----------+-------------
+ 09-23-2002 | Mon Jan 01 06:00:00 2024 | 15:30:10 | 12:00:00+00
+(1 row)
+
+SET TimeZone TO 'America/Los_Angeles';
+-- The XML Schema whiteSpace facet is applied before validating or
+-- converting, so a padded value behaves exactly like an unpadded one -- in
+-- particular it still gets the time zone normalization above.  xs:string
+-- preserves whitespace, so character targets are left alone.
+SELECT xmlcast('  -P1Y2M  '::xml AS interval) AS interval_padded,
+       xmlcast('  2002-09-24+06:00  '::xml AS date) AS date_padded,
+       xmlcast('  2024-01-01T12:00:00+06:00  '::xml AS timestamp) AS ts_padded,
+       xmlcast('  09:30:10-06:00  '::xml AS time) AS time_padded,
+       xmlcast(E'\t\n 42 \n'::xml AS int) AS int_padded,
+       xmlcast('  true  '::xml AS boolean) AS bool_padded;
+   interval_padded   | date_padded |        ts_padded         | time_padded | int_padded | bool_padded 
+---------------------+-------------+--------------------------+-------------+------------+-------------
+ @ 1 year 2 mons ago | 09-23-2002  | Mon Jan 01 06:00:00 2024 | 15:30:10    |         42 | t
+(1 row)
+
+SELECT xmlcast('  foo  bar  '::xml AS text) AS text_preserved,
+       xmlcast('  foo  bar  '::xml AS varchar) AS varchar_preserved;
+ text_preserved | varchar_preserved 
+----------------+-------------------
+   foo  bar     |   foo  bar  
+(1 row)
+
+-- validation makes the conversion independent of DateStyle
+SET DateStyle TO 'DMY';
+SELECT xmlcast('2024-01-02'::xml AS date);
+  xmlcast   
+------------
+ 02-01-2024
+(1 row)
+
+SET DateStyle TO 'MDY';
+SELECT xmlcast('2024-01-02'::xml AS date);
+  xmlcast   
+------------
+ 01-02-2024
+(1 row)
+
+RESET DateStyle;
+-- an interval whose fields differ in sign has no xs:duration representation
+SELECT xmlcast('1 year -1 day'::interval AS xml);
+ERROR:  interval cannot be represented as xs:duration
+DETAIL:  Intervals whose fields differ in sign have no xs:duration representation.
+-- every supported type survives a SQL -> XML -> SQL round trip
+SET xmlbinary TO hex;
+SET TimeZone TO 'UTC';
+SELECT xmlcast(xmlcast(true AS xml) AS boolean) = true AS bool_ok,
+       xmlcast(xmlcast(42::int2 AS xml) AS int2) = 42 AS int2_ok,
+       xmlcast(xmlcast(-42 AS xml) AS int4) = -42 AS int4_ok,
+       xmlcast(xmlcast(4273535420162021::int8 AS xml) AS int8) = 4273535420162021 AS int8_ok,
+       xmlcast(xmlcast(42.73::numeric AS xml) AS numeric) = 42.73 AS numeric_ok,
+       xmlcast(xmlcast(42.5::float4 AS xml) AS float4) = 42.5 AS float4_ok,
+       xmlcast(xmlcast(42.5::float8 AS xml) AS float8) = 42.5 AS float8_ok;
+ bool_ok | int2_ok | int4_ok | int8_ok | numeric_ok | float4_ok | float8_ok 
+---------+---------+---------+---------+------------+-----------+-----------
+ t       | t       | t       | t       | t          | t         | t
+(1 row)
+
+SELECT xmlcast(xmlcast('2024-05-29'::date AS xml) AS date) = '2024-05-29'::date AS date_ok,
+       xmlcast(xmlcast('11:11:11.5'::time AS xml) AS time) = '11:11:11.5'::time AS time_ok,
+       xmlcast(xmlcast('11:11:11+01'::timetz AS xml) AS timetz) = '11:11:11+01'::timetz AS timetz_ok,
+       xmlcast(xmlcast('2024-05-29 12:04:10.5'::timestamp AS xml) AS timestamp) = '2024-05-29 12:04:10.5'::timestamp AS ts_ok,
+       xmlcast(xmlcast('2024-05-29 12:04:10+02'::timestamptz AS xml) AS timestamptz) = '2024-05-29 12:04:10+02'::timestamptz AS tstz_ok,
+       xmlcast(xmlcast(E'\\xdeadbeef'::bytea AS xml) AS bytea) = E'\\xdeadbeef'::bytea AS bytea_ok,
+       xmlcast(xmlcast('foo & <"bar">'::text AS xml) AS text) = 'foo & <"bar">' AS text_ok;
+ date_ok | time_ok | timetz_ok | ts_ok | tstz_ok | bytea_ok | text_ok 
+---------+---------+-----------+-------+---------+----------+---------
+ t       | t       | t         | t     | t       | t        | t
+(1 row)
+
+SELECT v AS original, xmlcast(v AS xml) AS as_xml,
+       xmlcast(xmlcast(v AS xml) AS interval) = v AS ok
+FROM (VALUES ('1 year 2 mons'::interval), ('-1 year -2 mons'),
+             ('P1Y2M3DT4H5M6S'), ('-1 year -2 mons -3 days -04:05:06'),
+             ('0'), ('-00:00:01'), ('1 year 1 day 1 second'),
+             ('-1 year -1 day -1 second'), ('1 mon'), ('1 minute')) t(v);
+                     original                     |     as_xml      | ok 
+--------------------------------------------------+-----------------+----
+ @ 1 year 2 mons                                  | P1Y2M           | t
+ @ 1 year 2 mons ago                              | -P1Y2M          | t
+ @ 1 year 2 mons 3 days 4 hours 5 mins 6 secs     | P1Y2M3DT4H5M6S  | t
+ @ 1 year 2 mons 3 days 4 hours 5 mins 6 secs ago | -P1Y2M3DT4H5M6S | t
+ @ 0                                              | PT0S            | t
+ @ 1 sec ago                                      | -PT1S           | t
+ @ 1 year 1 day 1 sec                             | P1Y1DT1S        | t
+ @ 1 year 1 day 1 sec ago                         | -P1Y1DT1S       | t
+ @ 1 mon                                          | P1M             | t
+ @ 1 min                                          | PT1M            | t
+(10 rows)
+
+-- each xs:duration designator on its own, and back again.  Note P1M is a
+-- month while PT1M is a minute, and that the trip preserves the value rather
+-- than the spelling: -P0D and PT0S denote the same duration.
+SELECT v AS lexical, xmlcast(v::xml AS interval) AS as_interval,
+       xmlcast(xmlcast(v::xml AS interval) AS xml) AS back
+FROM (VALUES ('PT0S'), ('-P0D'), ('P1Y'), ('P1M'), ('PT1M'), ('P1D'),
+             ('PT1S'), ('P1Y1DT1S'), ('-P1Y1DT1S')) t(v);
+  lexical  |       as_interval        |   back    
+-----------+--------------------------+-----------
+ PT0S      | @ 0                      | PT0S
+ -P0D      | @ 0                      | PT0S
+ P1Y       | @ 1 year                 | P1Y
+ P1M       | @ 1 mon                  | P1M
+ PT1M      | @ 1 min                  | PT1M
+ P1D       | @ 1 day                  | P1D
+ PT1S      | @ 1 sec                  | PT1S
+ P1Y1DT1S  | @ 1 year 1 day 1 sec     | P1Y1DT1S
+ -P1Y1DT1S | @ 1 year 1 day 1 sec ago | -P1Y1DT1S
+(9 rows)
+
+SET TimeZone TO 'America/Los_Angeles';
+SET xmlbinary TO base64;
+-- Domains are flattened to their base type on both sides, so a domain over
+-- xml is still XML and a domain over a supported SQL type keeps that type's
+-- XML Schema lexical form.  The declared type is still what comes back, and
+-- its constraints are enforced.
+CREATE DOMAIN xc_dxml AS xml;
+CREATE DOMAIN xc_dint AS int;
+CREATE DOMAIN xc_dbytea AS bytea;
+CREATE DOMAIN xc_dvc AS varchar(5);
+CREATE DOMAIN xc_dts AS timestamp;
+CREATE DOMAIN xc_ddint AS xc_dint;
+SET xmlbinary TO hex;
+SELECT xmlcast('1'::xc_dxml AS int), xmlcast(1 AS xc_dxml),
+       pg_typeof(xmlcast(1 AS xc_dxml));
+ xmlcast | xmlcast | pg_typeof 
+---------+---------+-----------
+       1 | 1       | xc_dxml
+(1 row)
+
+SELECT xmlcast('1'::xml AS xc_dint), pg_typeof(xmlcast('1'::xml AS xc_dint)),
+       xmlcast(1::xc_dint AS xml);
+ xmlcast | pg_typeof | xmlcast 
+---------+-----------+---------
+       1 | xc_dint   | 1
+(1 row)
+
+SELECT xmlcast('41'::xml AS xc_dbytea), pg_typeof(xmlcast('41'::xml AS xc_dbytea));
+ xmlcast | pg_typeof 
+---------+-----------
+ \x41    | xc_dbytea
+(1 row)
+
+SELECT xmlcast('hello world'::xml AS xc_dvc), pg_typeof(xmlcast('hello world'::xml AS xc_dvc));
+ xmlcast | pg_typeof 
+---------+-----------
+ hello   | xc_dvc
+(1 row)
+
+-- a domain over timestamp still uses the xs:dateTime form, not text
+SELECT xmlcast('2002-05-30 09:30:10'::xc_dts AS xml);
+       xmlcast       
+---------------------
+ 2002-05-30T09:30:10
+(1 row)
+
+-- domain over a domain
+SELECT xmlcast('1'::xml AS xc_ddint), pg_typeof(xmlcast('1'::xml AS xc_ddint));
+ xmlcast | pg_typeof 
+---------+-----------
+       1 | xc_ddint
+(1 row)
+
+-- domain constraints are enforced on the result
+CREATE DOMAIN xc_dpos AS int CHECK (VALUE > 0);
+CREATE DOMAIN xc_dnn AS int NOT NULL;
+CREATE DOMAIN xc_dshort AS xml CHECK (length(VALUE::text) < 3);
+\set VERBOSITY terse
+SELECT xmlcast('-1'::xml AS xc_dpos);
+ERROR:  value for domain xc_dpos violates check constraint "xc_dpos_check"
+SELECT xmlcast(NULL::xml AS xc_dnn);
+ERROR:  domain xc_dnn does not allow null values
+SELECT xmlcast('abcdef' AS xc_dshort);
+ERROR:  value for domain xc_dshort violates check constraint "xc_dshort_check"
+\set VERBOSITY default
+CREATE VIEW xmlcast_domain_view AS
+  SELECT xmlcast('1'::xml AS xc_dint) AS a,
+         xmlcast(1 AS xc_dxml) AS b,
+         xmlcast('41'::xml AS xc_dbytea) AS c,
+         xmlcast('1'::xc_dxml AS int) AS d;
+\sv xmlcast_domain_view
+CREATE OR REPLACE VIEW public.xmlcast_domain_view AS
+ SELECT XMLCAST('1'::xml AS integer)::xc_dint AS a,
+    XMLCAST(1::text AS xml)::xc_dxml AS b,
+    XMLCAST('41'::xml AS bytea)::xc_dbytea AS c,
+    XMLCAST('1'::xml::xc_dxml::xml AS integer)::integer AS d
+SELECT * FROM xmlcast_domain_view;
+ a | b |  c   | d 
+---+---+------+---
+ 1 | 1 | \x41 | 1
+(1 row)
+
+DROP VIEW xmlcast_domain_view;
+DROP DOMAIN xc_dxml, xc_dint, xc_dbytea, xc_dvc, xc_dts, xc_ddint,
+            xc_dpos, xc_dnn, xc_dshort;
+SET xmlbinary TO base64;
+-- Syntax Rule 9: an <XML passing mechanism> may only be written when both the
+-- operand and the target are XML types.  Which one is asked for is ignored,
+-- so the results must match those without the clause.
+SELECT
+  xmlcast('foo'::xml AS xml)::text = xmlcast('foo'::xml AS xml BY REF)::text,
+  xmlcast('foo'::xml AS xml)::text = xmlcast('foo'::xml AS xml BY VALUE)::text;
+ ?column? | ?column? 
+----------+----------
+ t        | t
+(1 row)
+
+CREATE DOMAIN xc_byref_dxml AS xml;
+SELECT
+  xmlcast('foo'::xml AS xc_byref_dxml)::text = xmlcast('foo'::xml AS xc_byref_dxml BY REF)::text,
+  xmlcast('foo'::xc_byref_dxml AS xml)::text = xmlcast('foo'::xc_byref_dxml AS xml BY VALUE)::text;
+ ?column? | ?column? 
+----------+----------
+ t        | t
+(1 row)
+
+DROP DOMAIN xc_byref_dxml;
+-- ... and is rejected anywhere else
+\set VERBOSITY terse
+SELECT xmlcast('foo' AS xml BY REF);
+ERROR:  BY REF and BY VALUE are only allowed when both the XMLCAST operand and target are of type xml at character 8
+SELECT xmlcast('foo'::xml AS text BY REF);
+ERROR:  BY REF and BY VALUE are only allowed when both the XMLCAST operand and target are of type xml at character 8
+SELECT xmlcast('42'::xml AS int BY VALUE);
+ERROR:  BY REF and BY VALUE are only allowed when both the XMLCAST operand and target are of type xml at character 8
+SELECT xmlcast('P1Y2M'::xml AS interval BY REF);
+ERROR:  BY REF and BY VALUE are only allowed when both the XMLCAST operand and target are of type xml at character 8
+\set VERBOSITY default
+-- tests for xmlcast() with explicit length modifiers
+SELECT xmlcast('hello world'::xml AS varchar(5));
+ xmlcast 
+---------
+ hello
+(1 row)
+
+SELECT xmlcast('42.7312'::xml AS numeric(5,2));
+ xmlcast 
+---------
+   42.73
+(1 row)
+
+CREATE VIEW view_xmlcast_to_xml AS
+SELECT
+  xmlcast(NULL AS xml) AS c1,
+  xmlcast('foo' AS xml) AS c2,
+  xmlcast(''::text AS xml) AS c3,
+  xmlcast(NULL::text AS xml) AS c4,
+  xmlcast(''::xml AS text) AS c5,
+  xmlcast(NULL::xml AS text) c6,
+  xmlcast('foo & <"bar">'::text AS xml) AS c7,
+  xmlcast('foo & <"bar">'::varchar AS xml) AS c8,
+  xmlcast('foo & <"bar">'::name AS xml) AS c9,
+  xmlcast(xmltext(E'foo & <"bar">\r') AS text) AS c10,
+  xmlcast(xmlcast(E'foo & <"bar">\r' AS xml) AS text) AS c11,
+  xmlcast(to_date('29/05/2024','dd/mm/yyyy') AS xml) AS c12,
+  xmlcast('2024-05-29 12:04:10.703585+02'::timestamp with time zone at time zone 'Europe/Berlin' AS xml) AS c13,
+  xmlcast('2024-05-29 12:04:10.703585+02'::timestamp without time zone AS xml) AS c14,
+  xmlcast('1 year 2 months 3 days 4 hours 5 minutes 6 seconds'::interval AS xml) AS c15,
+  xmlcast(427353542 AS xml) AS c16,
+  xmlcast(4273535420162021 AS xml) AS c17,
+  xmlcast(42.007312345678910 AS xml) AS c18,
+  xmlcast(42.007312345678910::double precision AS xml) AS c19,
+  xmlcast(true AS xml) AS c20,
+  xmlcast(false AS xml) AS c21,
+  xmlcast(42 = 73 AS xml) AS c22,
+  xmlcast(42 <> 73 AS xml) AS c23,
+  xmlcast('11:11:11.5'::time AS xml) AS c24,
+  xmlcast('11:11:11.5+01'::time with time zone AS xml) AS c25;
+\sv view_xmlcast_to_xml
+CREATE OR REPLACE VIEW public.view_xmlcast_to_xml AS
+ SELECT XMLCAST(NULL::text AS xml) AS c1,
+    XMLCAST('foo'::text AS xml) AS c2,
+    XMLCAST(''::text AS xml) AS c3,
+    XMLCAST(NULL::text AS xml) AS c4,
+    XMLCAST(''::xml AS text) AS c5,
+    XMLCAST(NULL::xml AS text) AS c6,
+    XMLCAST('foo & <"bar">'::text AS xml) AS c7,
+    XMLCAST('foo & <"bar">'::character varying AS xml) AS c8,
+    XMLCAST('foo & <"bar">'::name::text AS xml) AS c9,
+    XMLCAST(xmltext('foo & <"bar">'::text) AS text) AS c10,
+    XMLCAST(XMLCAST('foo & <"bar">'::text AS xml) AS text) AS c11,
+    XMLCAST(to_date('29/05/2024'::text, 'dd/mm/yyyy'::text) AS xml) AS c12,
+    XMLCAST(('Wed May 29 03:04:10.703585 2024 PDT'::timestamp with time zone AT TIME ZONE 'Europe/Berlin'::text) AS xml) AS c13,
+    XMLCAST('Wed May 29 12:04:10.703585 2024'::timestamp without time zone AS xml) AS c14,
+    XMLCAST('@ 1 year 2 mons 3 days 4 hours 5 mins 6 secs'::interval AS xml) AS c15,
+    XMLCAST(427353542::text AS xml) AS c16,
+    XMLCAST('4273535420162021'::bigint::text AS xml) AS c17,
+    XMLCAST(42.007312345678910 AS xml) AS c18,
+    XMLCAST(42.007312345678910::double precision AS xml) AS c19,
+    XMLCAST(true AS xml) AS c20,
+    XMLCAST(false AS xml) AS c21,
+    XMLCAST(42 = 73 AS xml) AS c22,
+    XMLCAST(42 <> 73 AS xml) AS c23,
+    XMLCAST('11:11:11.5'::time without time zone AS xml) AS c24,
+    XMLCAST('11:11:11.5+01'::time with time zone AS xml) AS c25
+SELECT * FROM view_xmlcast_to_xml;
+ c1 | c2  | c3 | c4 | c5 | c6 |                c7                 |                c8                 |                c9                 |       c10       |       c11       |    c12     |            c13             |            c14             |      c15       |    c16    |       c17        |        c18         |        c19        | c20  |  c21  |  c22  | c23  |    c24     |       c25        
+----+-----+----+----+----+----+-----------------------------------+-----------------------------------+-----------------------------------+-----------------+-----------------+------------+----------------------------+----------------------------+----------------+-----------+------------------+--------------------+-------------------+------+-------+-------+------+------------+------------------
+    | foo |    |    |    |    | foo &amp; &lt;&quot;bar&quot;&gt; | foo &amp; &lt;&quot;bar&quot;&gt; | foo &amp; &lt;&quot;bar&quot;&gt; | foo & <"bar">\r | foo & <"bar">\r | 2024-05-29 | 2024-05-29T12:04:10.703585 | 2024-05-29T12:04:10.703585 | P1Y2M3DT4H5M6S | 427353542 | 4273535420162021 | 42.007312345678910 | 42.00731234567891 | true | false | false | true | 11:11:11.5 | 11:11:11.5+01:00
+(1 row)
+
+CREATE VIEW view_xmlcast_from_xml AS
+SELECT
+  xmlcast('P1Y2M3DT4H5M6S'::xml AS interval) AS c1,
+  xmlcast('-P1Y2M3DT4H5M6S'::xml AS interval) AS c2,
+  xmlcast('2002-09-24'::xml AS date) AS c3,
+  xmlcast('2002-09-24+06:00'::xml AS date) AS c4,
+  xmlcast('09:30:10Z'::xml AS time with time zone) AS c5,
+  xmlcast('09:30:10-06:00'::xml AS time with time zone) AS c6,
+  xmlcast('09:30:10+06:00'::xml AS time with time zone) AS c7,
+  xmlcast('2002-05-30T09:30:10Z'::xml AS timestamp with time zone) at time zone 'Europe/Berlin' AS c8,
+  xmlcast('2002-05-30T09:30:10-06:00'::xml AS timestamp with time zone) at time zone 'Europe/Berlin' AS c9,
+  xmlcast('2002-05-30T09:30:10+06:00'::xml AS timestamp with time zone) at time zone 'Europe/Berlin' AS c10,
+  xmlcast('foo bar'::xml AS text) AS c11,
+  xmlcast('       foo bar     '::xml AS varchar) AS c12,
+  xmlcast('foo &amp; &lt;&quot;bar&quot;&gt;'::xml AS text) AS c13,
+  xmlcast('42.7312345678910'::xml AS numeric) AS c14,
+  xmlcast('+42.7312345678910'::xml AS numeric) AS c15,
+  xmlcast('-42.7312345678910'::xml AS numeric) AS c16,
+  xmlcast('42'::xml AS integer) AS c17,
+  xmlcast('+42'::xml AS integer) AS c18,
+  xmlcast('-42'::xml AS integer) AS c19,
+  xmlcast('4273535420162021'::xml AS bigint) AS c20,
+  xmlcast('+4273535420162021'::xml AS bigint) AS c21,
+  xmlcast('-4273535420162021'::xml AS bigint) AS c22,
+  xmlcast('true'::xml AS boolean) AS c23,
+  xmlcast('false'::xml AS boolean) AS c24,
+  xmlcast(''::xml AS character varying) AS c25,
+  xmlcast(NULL::xml AS character varying) AS c26,
+  xmlcast('hello world'::xml AS varchar(5)) AS c27,
+  xmlcast('42.7312'::xml AS numeric(5,2)) AS c28;
+\sv view_xmlcast_from_xml
+CREATE OR REPLACE VIEW public.view_xmlcast_from_xml AS
+ SELECT XMLCAST('P1Y2M3DT4H5M6S'::xml AS interval) AS c1,
+    XMLCAST('-P1Y2M3DT4H5M6S'::xml AS interval) AS c2,
+    XMLCAST('2002-09-24'::xml AS date) AS c3,
+    XMLCAST('2002-09-24+06:00'::xml AS date) AS c4,
+    XMLCAST('09:30:10Z'::xml AS time with time zone) AS c5,
+    XMLCAST('09:30:10-06:00'::xml AS time with time zone) AS c6,
+    XMLCAST('09:30:10+06:00'::xml AS time with time zone) AS c7,
+    (XMLCAST('2002-05-30T09:30:10Z'::xml AS timestamp with time zone) AT TIME ZONE 'Europe/Berlin'::text) AS c8,
+    (XMLCAST('2002-05-30T09:30:10-06:00'::xml AS timestamp with time zone) AT TIME ZONE 'Europe/Berlin'::text) AS c9,
+    (XMLCAST('2002-05-30T09:30:10+06:00'::xml AS timestamp with time zone) AT TIME ZONE 'Europe/Berlin'::text) AS c10,
+    XMLCAST('foo bar'::xml AS text) AS c11,
+    XMLCAST('       foo bar     '::xml AS character varying)::character varying AS c12,
+    XMLCAST('foo &amp; &lt;&quot;bar&quot;&gt;'::xml AS text) AS c13,
+    XMLCAST('42.7312345678910'::xml AS numeric)::numeric AS c14,
+    XMLCAST('+42.7312345678910'::xml AS numeric)::numeric AS c15,
+    XMLCAST('-42.7312345678910'::xml AS numeric)::numeric AS c16,
+    XMLCAST('42'::xml AS integer)::integer AS c17,
+    XMLCAST('+42'::xml AS integer)::integer AS c18,
+    XMLCAST('-42'::xml AS integer)::integer AS c19,
+    XMLCAST('4273535420162021'::xml AS bigint)::bigint AS c20,
+    XMLCAST('+4273535420162021'::xml AS bigint)::bigint AS c21,
+    XMLCAST('-4273535420162021'::xml AS bigint)::bigint AS c22,
+    XMLCAST('true'::xml AS boolean)::boolean AS c23,
+    XMLCAST('false'::xml AS boolean)::boolean AS c24,
+    XMLCAST(''::xml AS character varying)::character varying AS c25,
+    XMLCAST(NULL::xml AS character varying)::character varying AS c26,
+    XMLCAST('hello world'::xml AS character varying(5))::character varying(5) AS c27,
+    XMLCAST('42.7312'::xml AS numeric(5,2))::numeric(5,2) AS c28
+SELECT * FROM view_xmlcast_from_xml;
+                      c1                      |                        c2                        |     c3     |     c4     |     c5      |     c6      |     c7      |            c8            |            c9            |           c10            |   c11   |         c12         |      c13      |       c14        |       c15        |        c16        | c17 | c18 | c19 |       c20        |       c21        |        c22        | c23 | c24 | c25 | c26 |  c27  |  c28  
+----------------------------------------------+--------------------------------------------------+------------+------------+-------------+-------------+-------------+--------------------------+--------------------------+--------------------------+---------+---------------------+---------------+------------------+------------------+-------------------+-----+-----+-----+------------------+------------------+-------------------+-----+-----+-----+-----+-------+-------
+ @ 1 year 2 mons 3 days 4 hours 5 mins 6 secs | @ 1 year 2 mons 3 days 4 hours 5 mins 6 secs ago | 09-24-2002 | 09-23-2002 | 09:30:10+00 | 09:30:10-06 | 09:30:10+06 | Thu May 30 11:30:10 2002 | Thu May 30 17:30:10 2002 | Thu May 30 05:30:10 2002 | foo bar |        foo bar      | foo & <"bar"> | 42.7312345678910 | 42.7312345678910 | -42.7312345678910 |  42 |  42 | -42 | 4273535420162021 | 4273535420162021 | -4273535420162021 | t   | f   |     |     | hello | 42.73
+(1 row)
+
+RESET xmlbinary;
+RESET timezone;
diff --git a/src/test/regress/sql/xml.sql b/src/test/regress/sql/xml.sql
index ea0438aa45d..52d8c759feb 100644
--- a/src/test/regress/sql/xml.sql
+++ b/src/test/regress/sql/xml.sql
@@ -692,3 +692,605 @@ SELECT xmltext('  ');
 SELECT xmltext('foo `$_-+?=*^%!|/\()[]{}');
 SELECT xmltext('foo & <"bar">');
 SELECT xmltext('x'|| '<P>73</P>'::xml || .42 || true || 'j'::char);
+
+-- for xmlcast() tests
+INSERT INTO xmltest
+ VALUES (42,
+'<?xml version="1.0" encoding="utf-8"?>
+ <xmlcast>
+  <period1>P1Y2M3DT4H5M6S</period1>
+  <period2>1 year 2 mons 3 days 4 hours 5 minutes 6 seconds</period2>
+  <period3>-P1Y2M3DT4H5M6S</period3>
+  <date1>2002-09-24</date1>
+  <date2>2002-09-24+06:00</date2>
+  <time>09:30:10.5</time>
+  <time_tz1>09:30:10Z</time_tz1>
+  <time_tz2>09:30:10-06:00</time_tz2>
+  <time_tz3>09:30:10+06:00</time_tz3>
+  <timestamp1>2002-05-30T09:00:00</timestamp1>
+  <timestamp2>2002-05-30T09:30:10.5</timestamp2>
+  <timestamp_tz1>2002-05-30T09:30:10Z</timestamp_tz1>
+  <timestamp_tz2>2002-05-30T09:30:10-06:00</timestamp_tz2>
+  <timestamp_tz3>2002-05-30T09:30:10+06:00</timestamp_tz3>
+  <text1>foo bar</text1>
+  <text2>       foo bar     </text2>
+  <text3>foo &amp; &lt;&quot;bar&quot;&gt;</text3>
+  <decimal1>42.7312345678910</decimal1>
+  <decimal2>+42.7312345678910</decimal2>
+  <decimal3>-42.7312345678910</decimal3>
+  <decimal4>INF</decimal4>
+  <decimal5>-INF</decimal5>
+  <decimal6>NaN</decimal6>
+  <integer1>42</integer1>
+  <integer2>+42</integer2>
+  <integer3>-42</integer3>
+  <long1>4273535420162021</long1>
+  <long2>+4273535420162021</long2>
+  <long3>-4273535420162021</long3>
+  <bool1 att="true">42</bool1>
+  <bool2 att="false">73</bool2>
+  <empty></empty>
+ </xmlcast>'::xml
+);
+
+-- This prevents the xmlcast regression tests from failing if the system's timezone has been changed.
+SET timezone TO 'America/Los_Angeles';
+
+-- xmlcast exceptions
+\set VERBOSITY terse
+SELECT xmlcast((xpath('//text1/text()', data))[1] AS text[]) FROM xmltest WHERE id = 42;
+SELECT xmlcast((xpath('//text1/integer1()', data))[1] AS int[]) FROM xmltest WHERE id = 42;
+SELECT xmlcast(NULL AS text);
+SELECT xmlcast('foo'::text AS varchar);
+SELECT xmlcast(42 AS text);
+SELECT xmlcast(array['foo','bar'] AS xml);
+SELECT xmlcast('not-a-number'::xml AS integer);
+SELECT xmlcast('not-a-date'::xml AS date);
+\set VERBOSITY default
+
+-- Both sides are matched against the same exact set of types, not against a
+-- type category, which says nothing about a type's representation: oid and
+-- money are TYPCATEGORY_NUMERIC and a user-defined type may declare any
+-- category it likes while being pass-by-value.  A domain does not launder an
+-- unsupported base type either, since the check is applied to the base type.
+CREATE DOMAIN xmlcast_doid AS oid;
+CREATE TYPE xmlcast_byval;
+CREATE FUNCTION xmlcast_byval_in(cstring) RETURNS xmlcast_byval
+  AS 'int4in' LANGUAGE internal IMMUTABLE STRICT;
+CREATE FUNCTION xmlcast_byval_out(xmlcast_byval) RETURNS cstring
+  AS 'int4out' LANGUAGE internal IMMUTABLE STRICT;
+CREATE TYPE xmlcast_byval (INPUT = xmlcast_byval_in, OUTPUT = xmlcast_byval_out,
+  INTERNALLENGTH = 4, PASSEDBYVALUE, CATEGORY = 'D');
+
+SELECT xmlcast('1'::xml AS oid);
+SELECT xmlcast('1'::xml AS money);
+SELECT xmlcast('1'::xml AS xmlcast_doid);
+SELECT xmlcast('1'::xml AS int[]);
+SELECT xmlcast(1::oid AS xml);
+SELECT xmlcast(1::money AS xml);
+SELECT xmlcast('1'::xmlcast_byval AS xml);
+SELECT xmlcast('1'::xml AS xmlcast_byval);
+
+DROP TYPE xmlcast_byval CASCADE;
+DROP DOMAIN xmlcast_doid;
+
+-- xmlcast tests for "XML to non-XML" expressions
+SELECT
+  xmlcast((xpath('//date1/text()', data))[1] AS date), pg_typeof(xmlcast((xpath('//date1/text()', data))[1] AS date)),
+  xmlcast((xpath('//date2/text()', data))[1] AS date), pg_typeof(xmlcast((xpath('//date2/text()', data))[1] AS date))
+FROM xmltest WHERE id = 42;
+
+SELECT
+  xmlcast((xpath('//period1/text()', data))[1] AS interval), pg_typeof(xmlcast((xpath('//period1/text()', data))[1] AS interval)),
+  xmlcast((xpath('//period3/text()', data))[1] AS interval), pg_typeof(xmlcast((xpath('//period3/text()', data))[1] AS interval))
+FROM xmltest WHERE id = 42;
+
+-- period2 holds PostgreSQL interval syntax, which is not in the lexical space
+-- of xs:duration and is therefore rejected
+SELECT xmlcast((xpath('//period2/text()', data))[1] AS interval) FROM xmltest WHERE id = 42;
+
+SELECT
+  xmlcast((xpath('//time/text()', data))[1] AS time), pg_typeof(xmlcast((xpath('//time/text()', data))[1] AS time)),
+  xmlcast((xpath('//time_tz1/text()', data))[1] AS time with time zone), pg_typeof(xmlcast((xpath('//time_tz1/text()', data))[1] AS time with time zone)),
+  xmlcast((xpath('//time_tz2/text()', data))[1] AS time with time zone), pg_typeof(xmlcast((xpath('//time_tz2/text()', data))[1] AS time with time zone)),
+  xmlcast((xpath('//time_tz3/text()', data))[1] AS time with time zone), pg_typeof(xmlcast((xpath('//time_tz3/text()', data))[1] AS time with time zone))
+FROM xmltest WHERE id = 42;
+
+SELECT
+  xmlcast((xpath('//text1/text()', data))[1] AS text), pg_typeof(xmlcast((xpath('//text1/text()', data))[1] AS text)),
+  xmlcast((xpath('//text2/text()', data))[1] AS text), pg_typeof(xmlcast((xpath('//text2/text()', data))[1] AS text)),
+  xmlcast((xpath('//text3/text()', data))[1] AS text), pg_typeof(xmlcast((xpath('//text3/text()', data))[1] AS text))
+FROM xmltest WHERE id = 42;
+
+SELECT
+  xmlcast((xpath('//text1/text()', data))[1] AS varchar), pg_typeof(xmlcast((xpath('//text1/text()', data))[1] AS varchar)),
+  xmlcast((xpath('//text2/text()', data))[1] AS varchar), pg_typeof(xmlcast((xpath('//text2/text()', data))[1] AS varchar)),
+  xmlcast((xpath('//text3/text()', data))[1] AS varchar), pg_typeof(xmlcast((xpath('//text3/text()', data))[1] AS varchar))
+FROM xmltest WHERE id = 42;
+
+SELECT
+  xmlcast((xpath('//text1/text()', data))[1] AS name), pg_typeof(xmlcast((xpath('//text1/text()', data))[1] AS name)),
+  xmlcast((xpath('//text2/text()', data))[1] AS name), pg_typeof(xmlcast((xpath('//text2/text()', data))[1] AS name)),
+  xmlcast((xpath('//text3/text()', data))[1] AS name), pg_typeof(xmlcast((xpath('//text3/text()', data))[1] AS name))
+FROM xmltest WHERE id = 42;
+
+SELECT
+  xmlcast((xpath('//text1/text()', data))[1] AS bpchar), pg_typeof(xmlcast((xpath('//text1/text()', data))[1] AS bpchar)),
+  xmlcast((xpath('//text2/text()', data))[1] AS bpchar), pg_typeof(xmlcast((xpath('//text2/text()', data))[1] AS bpchar)),
+  xmlcast((xpath('//text3/text()', data))[1] AS bpchar), pg_typeof(xmlcast((xpath('//text3/text()', data))[1] AS bpchar))
+FROM xmltest WHERE id = 42;
+
+SELECT
+  xmlcast((xpath('//decimal1/text()', data))[1] AS numeric), pg_typeof(xmlcast((xpath('//decimal1/text()', data))[1] AS numeric)),
+  xmlcast((xpath('//decimal2/text()', data))[1] AS numeric), pg_typeof(xmlcast((xpath('//decimal2/text()', data))[1] AS numeric)),
+  xmlcast((xpath('//decimal3/text()', data))[1] AS numeric), pg_typeof(xmlcast((xpath('//decimal3/text()', data))[1] AS numeric))
+FROM xmltest WHERE id = 42;
+
+-- decimal4/5/6 hold INF, -INF and NaN, which are outside the lexical space of
+-- xs:decimal and, for approximate numerics, rejected outright by General Rule
+-- 4.i.v
+\set VERBOSITY terse
+SELECT xmlcast((xpath('//decimal4/text()', data))[1] AS numeric) FROM xmltest WHERE id = 42;
+SELECT xmlcast((xpath('//decimal6/text()', data))[1] AS numeric) FROM xmltest WHERE id = 42;
+SELECT xmlcast((xpath('//decimal4/text()', data))[1] AS double precision) FROM xmltest WHERE id = 42;
+SELECT xmlcast((xpath('//decimal6/text()', data))[1] AS double precision) FROM xmltest WHERE id = 42;
+\set VERBOSITY default
+
+SELECT
+  xmlcast((xpath('//decimal1/text()', data))[1] AS double precision), pg_typeof(xmlcast((xpath('//decimal1/text()', data))[1] AS double precision)),
+  xmlcast((xpath('//decimal2/text()', data))[1] AS double precision), pg_typeof(xmlcast((xpath('//decimal2/text()', data))[1] AS double precision)),
+  xmlcast((xpath('//decimal3/text()', data))[1] AS double precision), pg_typeof(xmlcast((xpath('//decimal3/text()', data))[1] AS double precision))
+FROM xmltest WHERE id = 42;
+
+SELECT
+  xmlcast((xpath('//integer1/text()', data))[1] AS int), pg_typeof(xmlcast((xpath('//integer1/text()', data))[1] AS int)),
+  xmlcast((xpath('//integer2/text()', data))[1] AS int), pg_typeof(xmlcast((xpath('//integer2/text()', data))[1] AS int)),
+  xmlcast((xpath('//integer3/text()', data))[1] AS int), pg_typeof(xmlcast((xpath('//integer3/text()', data))[1] AS int))
+FROM xmltest WHERE id = 42;
+
+SELECT
+  xmlcast((xpath('//long1/text()', data))[1] AS bigint), pg_typeof(xmlcast((xpath('//long1/text()', data))[1] AS bigint)),
+  xmlcast((xpath('//long2/text()', data))[1] AS bigint), pg_typeof(xmlcast((xpath('//long2/text()', data))[1] AS bigint)),
+  xmlcast((xpath('//long3/text()', data))[1] AS bigint), pg_typeof(xmlcast((xpath('//long3/text()', data))[1] AS bigint))
+FROM xmltest WHERE id = 42;
+
+SELECT
+  xmlcast((xpath('//bool1/@att', data))[1] AS boolean), pg_typeof(xmlcast((xpath('//bool1/@att', data))[1] AS boolean)),
+  xmlcast((xpath('//bool2/@att', data))[1] AS boolean), pg_typeof(xmlcast((xpath('//bool1/@att', data))[1] AS boolean))
+FROM xmltest WHERE id = 42;
+
+SELECT xmlcast((xpath('//empty/text()', data))[1] AS text), pg_typeof(xmlcast((xpath('//empty/text()', data))[1] AS text))
+FROM xmltest WHERE id = 42;
+
+-- xmlcast tests for "XML to XML" expressions
+SELECT
+  xmlcast((xpath('//text1/text()', data))[1] AS xml), pg_typeof(xmlcast((xpath('//text1/text()', data))[1] AS xml)),
+  xmlcast((xpath('//text2/text()', data))[1] AS xml), pg_typeof(xmlcast((xpath('//text2/text()', data))[1] AS xml)),
+  xmlcast((xpath('//text3/text()', data))[1] AS xml), pg_typeof(xmlcast((xpath('//text3/text()', data))[1] AS xml))
+FROM xmltest WHERE id = 42;
+
+SELECT
+  xmlcast((xpath('//timestamp1/text()', data))[1] AS timestamp), pg_typeof(xmlcast((xpath('//timestamp1/text()', data))[1] AS timestamp)),
+  xmlcast((xpath('//timestamp2/text()', data))[1] AS timestamp), pg_typeof(xmlcast((xpath('//timestamp2/text()', data))[1] AS timestamp))
+FROM xmltest WHERE id = 42;
+
+SELECT
+  xmlcast((xpath('//timestamp_tz1/text()', data))[1] AS timestamp with time zone), pg_typeof(xmlcast((xpath('//timestamp_tz1/text()', data))[1] AS timestamp with time zone)),
+  xmlcast((xpath('//timestamp_tz2/text()', data))[1] AS timestamp with time zone), pg_typeof(xmlcast((xpath('//timestamp_tz2/text()', data))[1] AS timestamp with time zone)),
+  xmlcast((xpath('//timestamp_tz3/text()', data))[1] AS timestamp with time zone), pg_typeof(xmlcast((xpath('//timestamp_tz3/text()', data))[1] AS timestamp with time zone))
+FROM xmltest WHERE id = 42;
+
+-- xmlcast tests for "non-XML to XML" expressions
+SELECT j, pg_typeof(j) FROM xmlcast(NULL AS xml) t(j);
+SELECT j, pg_typeof(j) FROM xmlcast('foo' AS xml) t(j);
+SELECT j, pg_typeof(j) FROM xmlcast(''::text AS xml) t(j);
+SELECT j, pg_typeof(j) FROM xmlcast(NULL::text AS xml) t(j);
+SELECT j, pg_typeof(j) FROM xmlcast(''::xml AS text) t(j);
+SELECT j, pg_typeof(j) FROM xmlcast(NULL::xml AS text) t(j);
+SELECT j, pg_typeof(j) FROM xmlcast('foo & <"bar">'::text AS xml) t(j);
+SELECT j, pg_typeof(j) FROM xmlcast('foo & <"bar">'::varchar AS xml) t(j);
+SELECT j, pg_typeof(j) FROM xmlcast('foo & <"bar">'::name AS xml) t(j);
+SELECT j, pg_typeof(j) FROM xmlcast(xmltext(E'foo & <"bar">\r') AS text) t(j);
+SELECT j, pg_typeof(j) FROM xmlcast(xmlcast(E'foo & <"bar">\r' AS xml) AS text) t(j);
+SELECT j, pg_typeof(j) FROM xmlcast(to_date('29/05/2024','dd/mm/yyyy') AS xml) t(j);
+SELECT j, pg_typeof(j) FROM xmlcast('2024-05-29 12:04:10.703585+02'::timestamp with time zone at time zone 'Europe/Berlin' AS xml) t(j);
+SELECT j, pg_typeof(j) FROM xmlcast('2024-05-29 12:04:10.703585+02'::timestamp without time zone AS xml) t(j);
+SELECT j, pg_typeof(j) FROM xmlcast('1 year 2 months 3 days 4 hours 5 minutes 6 seconds'::interval AS xml) t(j);
+SELECT j, pg_typeof(j) FROM xmlcast(42::smallint AS xml) t(j);
+SELECT j, pg_typeof(j) FROM xmlcast(427353542 AS xml) t(j);
+SELECT j, pg_typeof(j) FROM xmlcast(4273535420162021 AS xml) t(j);
+SELECT j, pg_typeof(j) FROM xmlcast(42.007312345678910 AS xml) t(j);
+SELECT j, pg_typeof(j) FROM xmlcast(42.007312345678910::double precision AS xml) t(j);
+SELECT j, pg_typeof(j) FROM xmlcast(42.0::real AS xml) t(j);
+SELECT j, pg_typeof(j) FROM xmlcast('infinity'::real AS xml) t(j);
+SELECT j, pg_typeof(j) FROM xmlcast('-infinity'::real AS xml) t(j);
+SELECT j, pg_typeof(j) FROM xmlcast('nan'::real AS xml) t(j);
+SELECT j, pg_typeof(j) FROM xmlcast('infinity'::double precision AS xml) t(j);
+SELECT j, pg_typeof(j) FROM xmlcast('-infinity'::double precision AS xml) t(j);
+SELECT j, pg_typeof(j) FROM xmlcast('nan'::double precision AS xml) t(j);
+SELECT j, pg_typeof(j) FROM xmlcast('infinity'::numeric AS xml) t(j);
+SELECT j, pg_typeof(j) FROM xmlcast('-infinity'::numeric AS xml) t(j);
+SELECT j, pg_typeof(j) FROM xmlcast('nan'::numeric AS xml) t(j);
+SELECT j, pg_typeof(j) FROM xmlcast(true AS xml) t(j);
+SELECT j, pg_typeof(j) FROM xmlcast(false AS xml) t(j);
+SELECT j, pg_typeof(j) FROM xmlcast(42 = 73 AS xml) t(j);
+SELECT j, pg_typeof(j) FROM xmlcast(42 <> 73 AS xml) t(j);
+SELECT j, pg_typeof(j) FROM xmlcast('11:11:11.5'::time AS xml) t(j);
+SELECT j, pg_typeof(j) FROM xmlcast('11:11:11.5+01'::time with time zone AS xml) t(j);
+SELECT j, pg_typeof(j) FROM xmlcast('infinity'::interval AS xml) t(j);
+SELECT j, pg_typeof(j) FROM xmlcast('-infinity'::interval AS xml) t(j);
+
+-- The XML side of a binary string is xs:hexBinary or xs:base64Binary,
+-- selected by xmlbinary in both directions, so bytea round-trips under
+-- either setting.
+SET xmlbinary TO hex;
+SELECT xmlcast(E'\\xDEADBEEF'::bytea AS xml);
+SELECT xmlcast(xmlcast(E'\\xDEADBEEF'::bytea AS xml) AS bytea);
+SET xmlbinary TO base64;
+SELECT xmlcast(E'\\xDEADBEEF'::bytea AS xml);
+SELECT xmlcast(xmlcast(E'\\xDEADBEEF'::bytea AS xml) AS bytea);
+
+-- xmlcast() to bytea yields bytea directly, not text run through byteain()
+SELECT pg_typeof(xmlcast('QQ=='::xml AS bytea));
+
+-- edge values: empty, an embedded NUL, and bytes with the high bit set
+SET xmlbinary TO hex;
+SELECT xmlcast(xmlcast(''::bytea AS xml) AS bytea) IS NULL AS empty_is_null,
+       xmlcast(xmlcast(E'\\x00'::bytea AS xml) AS bytea),
+       xmlcast(xmlcast(E'\\xFF00FF'::bytea AS xml) AS bytea);
+SET xmlbinary TO base64;
+SELECT xmlcast(xmlcast(''::bytea AS xml) AS bytea) IS NULL AS empty_is_null,
+       xmlcast(xmlcast(E'\\x00'::bytea AS xml) AS bytea),
+       xmlcast(xmlcast(E'\\xFF00FF'::bytea AS xml) AS bytea);
+
+-- xs:hexBinary collapses leading/trailing whitespace but forbids embedded
+-- whitespace; xs:base64Binary permits it
+SET xmlbinary TO hex;
+SELECT xmlcast('  DEAD  '::xml AS bytea);
+SELECT xmlcast('DE AD'::xml AS bytea);
+SET xmlbinary TO base64;
+SELECT xmlcast('3q2 +7w=='::xml AS bytea);
+
+-- invalid lexical forms are rejected
+\set VERBOSITY terse
+SET xmlbinary TO hex;
+SELECT xmlcast('DEA'::xml AS bytea);
+SELECT xmlcast('zz'::xml AS bytea);
+SET xmlbinary TO base64;
+SELECT xmlcast('QQ='::xml AS bytea);
+\set VERBOSITY default
+
+-- bytea is not collatable; exercise the collation-assignment path
+SET xmlbinary TO hex;
+SELECT xmlcast(v AS bytea) FROM (VALUES ('41'::xml), ('42'::xml)) t(v) ORDER BY 1;
+
+-- a natively produced target needs no outer cast node, unlike int below
+CREATE VIEW xmlcast_bytea_view AS
+  SELECT xmlcast('DEADBEEF'::xml AS bytea) AS b, xmlcast('42'::xml AS int) AS i;
+\sv xmlcast_bytea_view
+DROP VIEW xmlcast_bytea_view;
+
+SET xmlbinary TO base64;
+
+-- XSD lexical validation on the XML -> SQL direction.  A SQL input function
+-- accepts whatever PostgreSQL accepts, which is both wider than the XSD
+-- lexical space and, for dates and timestamps, dependent on DateStyle;
+-- XMLCAST pins the input to the XML Schema lexical form instead.
+\set VERBOSITY terse
+SELECT xmlcast('yes'::xml AS boolean);
+SELECT xmlcast('on'::xml AS boolean);
+SELECT xmlcast('t'::xml AS boolean);
+SELECT xmlcast('0x10'::xml AS int);
+SELECT xmlcast('1e2'::xml AS numeric);
+SELECT xmlcast('99999'::xml AS int2);
+SELECT xmlcast('Infinity'::xml AS float8);
+SELECT xmlcast('3 days'::xml AS interval);
+SELECT xmlcast('P1Y-1D'::xml AS interval);
+-- xs:duration has no week designator, though interval input syntax does
+SELECT xmlcast('P3W'::xml AS interval);
+SELECT xmlcast('2024-01-01 12:00:00'::xml AS timestamp);
+SELECT xmlcast('12:00:00+02'::xml AS timetz);
+\set VERBOSITY default
+
+-- ... while the XSD lexical forms are accepted, including the ones a SQL
+-- input function would reject on its own
+SELECT xmlcast('1'::xml AS boolean), xmlcast('0'::xml AS boolean),
+       xmlcast('-P1Y2M'::xml AS interval),
+       xmlcast('12:00:00+02:00'::xml AS timetz);
+
+-- General Rule 4.i.v: an approximate numeric target rejects INF/-INF/NaN even
+-- though xs:double admits them, and xs:decimal has no such values at all.
+-- Mapping an infinite float to XML and back is therefore not a round trip.
+\set VERBOSITY terse
+SELECT xmlcast('INF'::xml AS float8);
+SELECT xmlcast('-INF'::xml AS float8);
+SELECT xmlcast('NaN'::xml AS float8);
+SELECT xmlcast('INF'::xml AS numeric);
+SELECT xmlcast('NaN'::xml AS numeric);
+\set VERBOSITY default
+
+-- Syntax Rule 15 picks the XSD type from the SQL target, so the very same
+-- lexical form is accepted or rejected depending on where it is going:
+-- exponents belong to xs:double but not to xs:decimal or xs:integer, and a
+-- fraction belongs to neither xs:integer nor an integer target.
+SELECT xmlcast('1e2'::xml AS float8) AS e_lower,
+       xmlcast('1E2'::xml AS float8) AS e_upper,
+       xmlcast('-1.5e-3'::xml AS float8) AS e_signed,
+       xmlcast('1.0'::xml AS float8) AS frac_float,
+       xmlcast('.5'::xml AS float8) AS leading_dot,
+       xmlcast('1.0'::xml AS numeric) AS frac_numeric;
+\set VERBOSITY terse
+SELECT xmlcast('1E2'::xml AS numeric);
+SELECT xmlcast('1e2'::xml AS int);
+SELECT xmlcast('1.0'::xml AS int);
+SELECT xmlcast('.5'::xml AS int);
+-- "Infinity" is not in the lexical space of xs:double at all, whereas "INF"
+-- is but General Rule 4.i.v refuses it: two different errors
+SELECT xmlcast('Infinity'::xml AS float8);
+SELECT xmlcast('INF'::xml AS float8);
+\set VERBOSITY default
+
+-- General Rule 4.c: a value that atomizes to the empty sequence is null,
+-- which is distinct from one whose string value is empty
+SELECT xmlcast(''::xml AS int) IS NULL AS empty_is_null,
+       xmlcast(''::xml AS text) IS NULL AS empty_text_is_null,
+       xmlcast('<a></a>'::xml AS text) = '' AS elem_is_empty_string;
+
+-- General Rules 4.a and 4.b: the value is atomized with fn:data(), so markup
+-- is stripped, references of every spelling are resolved, CDATA is unwrapped
+-- and comments contribute nothing
+SELECT xmlcast('<a>x</a>'::xml AS text) AS markup,
+       xmlcast('&lt;a&gt;x&lt;/a&gt;'::xml AS text) AS entities,
+       xmlcast('<a>x</a>'::xml AS text) = xmlcast('&lt;a&gt;x&lt;/a&gt;'::xml AS text) AS collide;
+SELECT xmlcast('&apos;'::xml AS text) AS apos,
+       xmlcast('&#65;'::xml AS text) AS dec_ref,
+       xmlcast('&#x41;'::xml AS text) AS hex_ref,
+       xmlcast('&#x0d;'::xml AS text) = E'\r' AS cr_lower,
+       xmlcast('&#x0D;'::xml AS text) = E'\r' AS cr_upper,
+       xmlcast('&#13;'::xml AS text) = E'\r' AS cr_dec,
+       xmlcast('<![CDATA[a<b]]>'::xml AS text) AS lone_cdata,
+       xmlcast('a<![CDATA[b]]>c'::xml AS text) AS text_cdata_run,
+       xmlcast('<!--c-->'::xml AS text) AS lone_comment,
+       xmlcast('<a>42</a>'::xml AS int) AS elem_to_int;
+
+-- General Rule 4.h casts the atomized sequence to a single value, which an
+-- XQuery cast can only do for one item.  Two or more items is an error, not a
+-- concatenation -- otherwise <a>1</a><b>2</b> would silently become 12.
+\set VERBOSITY terse
+SELECT xmlcast('<a/> <b/>'::xml AS text);
+SELECT xmlcast('<a>1</a><b>2</b>'::xml AS int);
+SELECT xmlcast('foo<a/>'::xml AS text);
+SELECT xmlcast('pre<!--c-->post'::xml AS text);
+\set VERBOSITY default
+
+-- ... but a single element whose content spans several nodes is one item, and
+-- its string value is legitimately the concatenation
+SELECT xmlcast('<x><y>bar</y>foo</x>'::xml AS text) AS one_element,
+       xmlcast('<x>pre<!--c-->post</x>'::xml AS text) AS comment_inside_element;
+
+-- A DTD is not a node in the XQuery data model, even though libxml links the
+-- internal subset into the document's child list, so it is not an item.
+SELECT xmlcast('<!DOCTYPE a><a>42</a>'::xml AS int) AS doctype,
+       xmlcast('<!DOCTYPE a [<!ELEMENT a (#PCDATA)>]><a>42</a>'::xml AS int) AS internal_subset,
+       xmlcast('<?xml version="1.0"?><!DOCTYPE a><a>x</a>'::xml AS text) AS decl_and_dtd,
+       xmlcast('<!DOCTYPE a [<!ENTITY e "hi">]><a>&e;</a>'::xml AS text) AS entity_in_subset;
+
+-- A comment or processing instruction in the prolog, by contrast, is an item
+\set VERBOSITY terse
+SELECT xmlcast('<!--c--><a>42</a>'::xml AS int);
+SELECT xmlcast('<?pi?><a>42</a>'::xml AS int);
+\set VERBOSITY default
+
+-- General Rules 4.h.i-iii: a target WITHOUT TIME ZONE normalizes to UTC
+-- before dropping the zone, rather than ignoring it; and 4.i.vii-viii: an
+-- absent zone means UTC, not the session's TimeZone
+SET TimeZone TO 'America/Los_Angeles';
+SELECT xmlcast('2002-09-24+06:00'::xml AS date) AS d_plus,
+       xmlcast('2002-09-24-06:00'::xml AS date) AS d_minus,
+       xmlcast('2002-09-24Z'::xml AS date) AS d_z,
+       xmlcast('2002-09-24'::xml AS date) AS d_none;
+SELECT xmlcast('2024-01-01T12:00:00+06:00'::xml AS timestamp) AS ts_plus,
+       xmlcast('09:30:10-06:00'::xml AS time) AS t_minus,
+       xmlcast('12:00:00'::xml AS timetz) AS timetz_none;
+SET TimeZone TO 'Asia/Tokyo';
+SELECT xmlcast('2002-09-24+06:00'::xml AS date) AS d_plus,
+       xmlcast('2024-01-01T12:00:00+06:00'::xml AS timestamp) AS ts_plus,
+       xmlcast('09:30:10-06:00'::xml AS time) AS t_minus,
+       xmlcast('12:00:00'::xml AS timetz) AS timetz_none;
+SET TimeZone TO 'America/Los_Angeles';
+
+-- The XML Schema whiteSpace facet is applied before validating or
+-- converting, so a padded value behaves exactly like an unpadded one -- in
+-- particular it still gets the time zone normalization above.  xs:string
+-- preserves whitespace, so character targets are left alone.
+SELECT xmlcast('  -P1Y2M  '::xml AS interval) AS interval_padded,
+       xmlcast('  2002-09-24+06:00  '::xml AS date) AS date_padded,
+       xmlcast('  2024-01-01T12:00:00+06:00  '::xml AS timestamp) AS ts_padded,
+       xmlcast('  09:30:10-06:00  '::xml AS time) AS time_padded,
+       xmlcast(E'\t\n 42 \n'::xml AS int) AS int_padded,
+       xmlcast('  true  '::xml AS boolean) AS bool_padded;
+SELECT xmlcast('  foo  bar  '::xml AS text) AS text_preserved,
+       xmlcast('  foo  bar  '::xml AS varchar) AS varchar_preserved;
+
+-- validation makes the conversion independent of DateStyle
+SET DateStyle TO 'DMY';
+SELECT xmlcast('2024-01-02'::xml AS date);
+SET DateStyle TO 'MDY';
+SELECT xmlcast('2024-01-02'::xml AS date);
+RESET DateStyle;
+
+-- an interval whose fields differ in sign has no xs:duration representation
+SELECT xmlcast('1 year -1 day'::interval AS xml);
+
+-- every supported type survives a SQL -> XML -> SQL round trip
+SET xmlbinary TO hex;
+SET TimeZone TO 'UTC';
+SELECT xmlcast(xmlcast(true AS xml) AS boolean) = true AS bool_ok,
+       xmlcast(xmlcast(42::int2 AS xml) AS int2) = 42 AS int2_ok,
+       xmlcast(xmlcast(-42 AS xml) AS int4) = -42 AS int4_ok,
+       xmlcast(xmlcast(4273535420162021::int8 AS xml) AS int8) = 4273535420162021 AS int8_ok,
+       xmlcast(xmlcast(42.73::numeric AS xml) AS numeric) = 42.73 AS numeric_ok,
+       xmlcast(xmlcast(42.5::float4 AS xml) AS float4) = 42.5 AS float4_ok,
+       xmlcast(xmlcast(42.5::float8 AS xml) AS float8) = 42.5 AS float8_ok;
+SELECT xmlcast(xmlcast('2024-05-29'::date AS xml) AS date) = '2024-05-29'::date AS date_ok,
+       xmlcast(xmlcast('11:11:11.5'::time AS xml) AS time) = '11:11:11.5'::time AS time_ok,
+       xmlcast(xmlcast('11:11:11+01'::timetz AS xml) AS timetz) = '11:11:11+01'::timetz AS timetz_ok,
+       xmlcast(xmlcast('2024-05-29 12:04:10.5'::timestamp AS xml) AS timestamp) = '2024-05-29 12:04:10.5'::timestamp AS ts_ok,
+       xmlcast(xmlcast('2024-05-29 12:04:10+02'::timestamptz AS xml) AS timestamptz) = '2024-05-29 12:04:10+02'::timestamptz AS tstz_ok,
+       xmlcast(xmlcast(E'\\xdeadbeef'::bytea AS xml) AS bytea) = E'\\xdeadbeef'::bytea AS bytea_ok,
+       xmlcast(xmlcast('foo & <"bar">'::text AS xml) AS text) = 'foo & <"bar">' AS text_ok;
+SELECT v AS original, xmlcast(v AS xml) AS as_xml,
+       xmlcast(xmlcast(v AS xml) AS interval) = v AS ok
+FROM (VALUES ('1 year 2 mons'::interval), ('-1 year -2 mons'),
+             ('P1Y2M3DT4H5M6S'), ('-1 year -2 mons -3 days -04:05:06'),
+             ('0'), ('-00:00:01'), ('1 year 1 day 1 second'),
+             ('-1 year -1 day -1 second'), ('1 mon'), ('1 minute')) t(v);
+
+-- each xs:duration designator on its own, and back again.  Note P1M is a
+-- month while PT1M is a minute, and that the trip preserves the value rather
+-- than the spelling: -P0D and PT0S denote the same duration.
+SELECT v AS lexical, xmlcast(v::xml AS interval) AS as_interval,
+       xmlcast(xmlcast(v::xml AS interval) AS xml) AS back
+FROM (VALUES ('PT0S'), ('-P0D'), ('P1Y'), ('P1M'), ('PT1M'), ('P1D'),
+             ('PT1S'), ('P1Y1DT1S'), ('-P1Y1DT1S')) t(v);
+SET TimeZone TO 'America/Los_Angeles';
+SET xmlbinary TO base64;
+
+-- Domains are flattened to their base type on both sides, so a domain over
+-- xml is still XML and a domain over a supported SQL type keeps that type's
+-- XML Schema lexical form.  The declared type is still what comes back, and
+-- its constraints are enforced.
+CREATE DOMAIN xc_dxml AS xml;
+CREATE DOMAIN xc_dint AS int;
+CREATE DOMAIN xc_dbytea AS bytea;
+CREATE DOMAIN xc_dvc AS varchar(5);
+CREATE DOMAIN xc_dts AS timestamp;
+CREATE DOMAIN xc_ddint AS xc_dint;
+
+SET xmlbinary TO hex;
+SELECT xmlcast('1'::xc_dxml AS int), xmlcast(1 AS xc_dxml),
+       pg_typeof(xmlcast(1 AS xc_dxml));
+SELECT xmlcast('1'::xml AS xc_dint), pg_typeof(xmlcast('1'::xml AS xc_dint)),
+       xmlcast(1::xc_dint AS xml);
+SELECT xmlcast('41'::xml AS xc_dbytea), pg_typeof(xmlcast('41'::xml AS xc_dbytea));
+SELECT xmlcast('hello world'::xml AS xc_dvc), pg_typeof(xmlcast('hello world'::xml AS xc_dvc));
+-- a domain over timestamp still uses the xs:dateTime form, not text
+SELECT xmlcast('2002-05-30 09:30:10'::xc_dts AS xml);
+-- domain over a domain
+SELECT xmlcast('1'::xml AS xc_ddint), pg_typeof(xmlcast('1'::xml AS xc_ddint));
+
+-- domain constraints are enforced on the result
+CREATE DOMAIN xc_dpos AS int CHECK (VALUE > 0);
+CREATE DOMAIN xc_dnn AS int NOT NULL;
+CREATE DOMAIN xc_dshort AS xml CHECK (length(VALUE::text) < 3);
+\set VERBOSITY terse
+SELECT xmlcast('-1'::xml AS xc_dpos);
+SELECT xmlcast(NULL::xml AS xc_dnn);
+SELECT xmlcast('abcdef' AS xc_dshort);
+\set VERBOSITY default
+
+CREATE VIEW xmlcast_domain_view AS
+  SELECT xmlcast('1'::xml AS xc_dint) AS a,
+         xmlcast(1 AS xc_dxml) AS b,
+         xmlcast('41'::xml AS xc_dbytea) AS c,
+         xmlcast('1'::xc_dxml AS int) AS d;
+\sv xmlcast_domain_view
+SELECT * FROM xmlcast_domain_view;
+DROP VIEW xmlcast_domain_view;
+
+DROP DOMAIN xc_dxml, xc_dint, xc_dbytea, xc_dvc, xc_dts, xc_ddint,
+            xc_dpos, xc_dnn, xc_dshort;
+SET xmlbinary TO base64;
+
+-- Syntax Rule 9: an <XML passing mechanism> may only be written when both the
+-- operand and the target are XML types.  Which one is asked for is ignored,
+-- so the results must match those without the clause.
+SELECT
+  xmlcast('foo'::xml AS xml)::text = xmlcast('foo'::xml AS xml BY REF)::text,
+  xmlcast('foo'::xml AS xml)::text = xmlcast('foo'::xml AS xml BY VALUE)::text;
+
+CREATE DOMAIN xc_byref_dxml AS xml;
+SELECT
+  xmlcast('foo'::xml AS xc_byref_dxml)::text = xmlcast('foo'::xml AS xc_byref_dxml BY REF)::text,
+  xmlcast('foo'::xc_byref_dxml AS xml)::text = xmlcast('foo'::xc_byref_dxml AS xml BY VALUE)::text;
+DROP DOMAIN xc_byref_dxml;
+
+-- ... and is rejected anywhere else
+\set VERBOSITY terse
+SELECT xmlcast('foo' AS xml BY REF);
+SELECT xmlcast('foo'::xml AS text BY REF);
+SELECT xmlcast('42'::xml AS int BY VALUE);
+SELECT xmlcast('P1Y2M'::xml AS interval BY REF);
+\set VERBOSITY default
+
+-- tests for xmlcast() with explicit length modifiers
+SELECT xmlcast('hello world'::xml AS varchar(5));
+SELECT xmlcast('42.7312'::xml AS numeric(5,2));
+
+CREATE VIEW view_xmlcast_to_xml AS
+SELECT
+  xmlcast(NULL AS xml) AS c1,
+  xmlcast('foo' AS xml) AS c2,
+  xmlcast(''::text AS xml) AS c3,
+  xmlcast(NULL::text AS xml) AS c4,
+  xmlcast(''::xml AS text) AS c5,
+  xmlcast(NULL::xml AS text) c6,
+  xmlcast('foo & <"bar">'::text AS xml) AS c7,
+  xmlcast('foo & <"bar">'::varchar AS xml) AS c8,
+  xmlcast('foo & <"bar">'::name AS xml) AS c9,
+  xmlcast(xmltext(E'foo & <"bar">\r') AS text) AS c10,
+  xmlcast(xmlcast(E'foo & <"bar">\r' AS xml) AS text) AS c11,
+  xmlcast(to_date('29/05/2024','dd/mm/yyyy') AS xml) AS c12,
+  xmlcast('2024-05-29 12:04:10.703585+02'::timestamp with time zone at time zone 'Europe/Berlin' AS xml) AS c13,
+  xmlcast('2024-05-29 12:04:10.703585+02'::timestamp without time zone AS xml) AS c14,
+  xmlcast('1 year 2 months 3 days 4 hours 5 minutes 6 seconds'::interval AS xml) AS c15,
+  xmlcast(427353542 AS xml) AS c16,
+  xmlcast(4273535420162021 AS xml) AS c17,
+  xmlcast(42.007312345678910 AS xml) AS c18,
+  xmlcast(42.007312345678910::double precision AS xml) AS c19,
+  xmlcast(true AS xml) AS c20,
+  xmlcast(false AS xml) AS c21,
+  xmlcast(42 = 73 AS xml) AS c22,
+  xmlcast(42 <> 73 AS xml) AS c23,
+  xmlcast('11:11:11.5'::time AS xml) AS c24,
+  xmlcast('11:11:11.5+01'::time with time zone AS xml) AS c25;
+
+\sv view_xmlcast_to_xml
+SELECT * FROM view_xmlcast_to_xml;
+
+CREATE VIEW view_xmlcast_from_xml AS
+SELECT
+  xmlcast('P1Y2M3DT4H5M6S'::xml AS interval) AS c1,
+  xmlcast('-P1Y2M3DT4H5M6S'::xml AS interval) AS c2,
+  xmlcast('2002-09-24'::xml AS date) AS c3,
+  xmlcast('2002-09-24+06:00'::xml AS date) AS c4,
+  xmlcast('09:30:10Z'::xml AS time with time zone) AS c5,
+  xmlcast('09:30:10-06:00'::xml AS time with time zone) AS c6,
+  xmlcast('09:30:10+06:00'::xml AS time with time zone) AS c7,
+  xmlcast('2002-05-30T09:30:10Z'::xml AS timestamp with time zone) at time zone 'Europe/Berlin' AS c8,
+  xmlcast('2002-05-30T09:30:10-06:00'::xml AS timestamp with time zone) at time zone 'Europe/Berlin' AS c9,
+  xmlcast('2002-05-30T09:30:10+06:00'::xml AS timestamp with time zone) at time zone 'Europe/Berlin' AS c10,
+  xmlcast('foo bar'::xml AS text) AS c11,
+  xmlcast('       foo bar     '::xml AS varchar) AS c12,
+  xmlcast('foo &amp; &lt;&quot;bar&quot;&gt;'::xml AS text) AS c13,
+  xmlcast('42.7312345678910'::xml AS numeric) AS c14,
+  xmlcast('+42.7312345678910'::xml AS numeric) AS c15,
+  xmlcast('-42.7312345678910'::xml AS numeric) AS c16,
+  xmlcast('42'::xml AS integer) AS c17,
+  xmlcast('+42'::xml AS integer) AS c18,
+  xmlcast('-42'::xml AS integer) AS c19,
+  xmlcast('4273535420162021'::xml AS bigint) AS c20,
+  xmlcast('+4273535420162021'::xml AS bigint) AS c21,
+  xmlcast('-4273535420162021'::xml AS bigint) AS c22,
+  xmlcast('true'::xml AS boolean) AS c23,
+  xmlcast('false'::xml AS boolean) AS c24,
+  xmlcast(''::xml AS character varying) AS c25,
+  xmlcast(NULL::xml AS character varying) AS c26,
+  xmlcast('hello world'::xml AS varchar(5)) AS c27,
+  xmlcast('42.7312'::xml AS numeric(5,2)) AS c28;
+
+\sv view_xmlcast_from_xml
+SELECT * FROM view_xmlcast_from_xml;
+
+RESET xmlbinary;
+RESET timezone;
\ No newline at end of file
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 58c4749e7e4..b5d375f4a9e 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -3553,6 +3553,7 @@ XidBoundsViolation
 XidCacheStatus
 XidCommitStatus
 XidStatus
+XmlCast
 XmlExpr
 XmlExprOp
 XmlOptionType
-- 
2.55.0

