rebase
Jim
From cd29f17dd3422c1f54e3996956f74c8e0403c055 Mon Sep 17 00:00:00 2001
From: Jim Jones <[email protected]>
Date: Mon, 15 Jun 2026 20:21:42 +0200
Subject: [PATCH v12] Add XMLDocument function (SQL/XML X030)
This patch adds the SQL/XML X030 function XMLDocument. It returns
an XML document from a given XML expression. An XML document node
can have any number of children nodes. Since our XML data type
corresponds to XML(CONTENT(ANY)), any expression already validated
by the input function is considered valid output for XMLDocument.
As a result, this function simply returns its input value. While
this implementation is quite trivial, it follows the SQL/XML
standard and facilitates the migration of SQL statements from
other database systems that also support X030.
Usage:
WITH t(x) AS (
VALUES
(xmlparse(DOCUMENT '<root><foo>bar</foo></root>')),
(xmlforest(42 AS foo, 73 AS bar)),
(NULL)
)
SELECT xmldocument(x) FROM t;
xmldocument
-----------------------------
<root><foo>bar</foo></root>
<foo>42</foo><bar>73</bar>
(3 rows)
This patch also adds documentation and tests.
Author: Jim Jones <[email protected]>
Reviewed-by: Chapman Flack <[email protected]>
Reviewed-by: Pavel Stehule <[email protected]>
Reviewed-by: Robert Treat <[email protected]>
Reviewed-by: Andrew Dunstan <[email protected]>
---
doc/src/sgml/func/func-xml.sgml | 54 ++++++++++++++++++++++++++
src/backend/catalog/sql_features.txt | 2 +-
src/backend/utils/adt/xml.c | 18 +++++++++
src/include/catalog/pg_proc.dat | 3 ++
src/test/regress/expected/xml.out | 58 ++++++++++++++++++++++++++++
src/test/regress/sql/xml.sql | 19 +++++++++
6 files changed, 153 insertions(+), 1 deletion(-)
diff --git a/doc/src/sgml/func/func-xml.sgml b/doc/src/sgml/func/func-xml.sgml
index 511bc90852a..00bf8c90081 100644
--- a/doc/src/sgml/func/func-xml.sgml
+++ b/doc/src/sgml/func/func-xml.sgml
@@ -93,6 +93,60 @@ SELECT xmlcomment('hello');
</para>
</sect3>
+ <sect3 id="functions-producing-xml-xmldocument">
+ <title><literal>xmldocument</literal></title>
+
+ <indexterm>
+ <primary>xmldocument</primary>
+ </indexterm>
+
+<synopsis>
+<function>xmldocument</function> ( <type>xml</type> ) <returnvalue>xml</returnvalue>
+</synopsis>
+
+ <para>
+ The <function>xmldocument</function> function returns the input argument
+ unchanged, or <literal>NULL</literal> if the argument is <literal>NULL</literal>,
+ and is provided for compatibility.
+
+ The SQL-standard <replaceable>XMLDocument</replaceable> function applied to an
+ XML value <literal>$EXPR</literal>, has effects equivalent to the XML
+ Query expression <literal>document { <replaceable>$EXPR</replaceable> }</literal>.
+ It replaces any document nodes in the input with their children and wraps the whole
+ result in a single <replaceable>document node</replaceable>.
+
+ In the XML Query standard, a <replaceable>document node</replaceable> represents
+ a relaxed version of an XML document structure. This corresponds to what PostgreSQL's
+ single XML type allows, meaning that any valid non-null PostgreSQL XML value can be
+ returned unchanged. Other systems may support more permissive XML data types,
+ such as <literal>XML(SEQUENCE)</literal>, which allow values that do not conform to
+ this structure. In PostgreSQL, every valid non-null value of the XML type already has
+ that structure, making additional processing by this function unnecessary.
+ </para>
+
+ <para>
+ Example:
+<screen><![CDATA[
+WITH xmldata (val) AS (
+ VALUES
+ (xmlparse(DOCUMENT '<root><foo>bar</foo></root>')),
+ (xmltext('foo&bar')),
+ (xmlelement(NAME el)),
+ (xmlforest(42 AS foo, 73 AS bar))
+)
+SELECT xmldocument(val) FROM xmldata;
+
+ xmldocument
+-----------------------------
+ <root><foo>bar</foo></root>
+ foo&bar
+ <el/>
+ <foo>42</foo><bar>73</bar>
+(4 rows)
+]]></screen>
+ </para>
+ </sect3>
+
<sect3 id="functions-producing-xml-xmlconcat">
<title><literal>xmlconcat</literal></title>
diff --git a/src/backend/catalog/sql_features.txt b/src/backend/catalog/sql_features.txt
index 626054cbcef..cfcb159530d 100644
--- a/src/backend/catalog/sql_features.txt
+++ b/src/backend/catalog/sql_features.txt
@@ -725,7 +725,7 @@ X015 Fields of XML type NO
X016 Persistent XML values YES
X020 XMLConcat YES
X025 XMLCast NO
-X030 XMLDocument NO
+X030 XMLDocument YES supported except for RETURNING
X031 XMLElement YES
X032 XMLForest YES
X034 XMLAgg YES
diff --git a/src/backend/utils/adt/xml.c b/src/backend/utils/adt/xml.c
index 0953ad2becb..2c6bf98a89e 100644
--- a/src/backend/utils/adt/xml.c
+++ b/src/backend/utils/adt/xml.c
@@ -523,6 +523,24 @@ xmlcomment(PG_FUNCTION_ARGS)
}
+/*
+ * xmldocument implements the SQL/XML function XMLDocument (X030).
+ * Since our XML data type corresponds to XML(CONTENT(ANY)), any
+ * expression already validated by the input function is considered
+ * valid output for XMLDocument. As a result, this function simply
+ * returns its input value.
+ */
+Datum
+xmldocument(PG_FUNCTION_ARGS)
+{
+#ifdef USE_LIBXML
+ PG_RETURN_DATUM(PG_GETARG_DATUM(0));
+#else
+ NO_XML_SUPPORT();
+ return 0;
+#endif /* not USE_LIBXML */
+}
+
Datum
xmltext(PG_FUNCTION_ARGS)
{
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index be157a5fbe9..6d875de8a94 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -9283,6 +9283,9 @@
{ oid => '3813', descr => 'generate XML text node',
proname => 'xmltext', prorettype => 'xml', proargtypes => 'text',
prosrc => 'xmltext' },
+{ oid => '3814', descr => 'generate XML document',
+ proname => 'xmldocument', prorettype => 'xml', proargtypes => 'xml',
+ prosrc => 'xmldocument' },
{ oid => '2923', descr => 'map table contents to XML',
proname => 'table_to_xml', procost => '100', provolatile => 's',
diff --git a/src/test/regress/expected/xml.out b/src/test/regress/expected/xml.out
index fb3e0ec41b2..8139c5eba79 100644
--- a/src/test/regress/expected/xml.out
+++ b/src/test/regress/expected/xml.out
@@ -1891,3 +1891,61 @@ SELECT xmltext('x'|| '<P>73</P>'::xml || .42 || true || 'j'::char);
x<P>73</P>0.42truej
(1 row)
+SELECT
+ xmldocument(
+ xmlelement(NAME root,
+ xmlattributes(42 AS att),
+ xmlcomment('comment'),
+ xmlelement(NAME foo,'<foo&bar>'),
+ xmlelement(NAME bar, xmlconcat('va', 'lue')),
+ xmlpi(name pi),
+ xmlelement(NAME txt, xmltext('<"&>'))
+ )
+ );
+ xmldocument
+------------------------------------------------------------------------------------------------------------------------
+ <root att="42"><!--comment--><foo><foo&bar></foo><bar>value</bar><?pi?><txt><"&></txt></root>
+(1 row)
+
+SELECT xmldocument(NULL);
+ xmldocument
+-------------
+
+(1 row)
+
+SELECT xmldocument('<foo>bar</foo>'::xml);
+ xmldocument
+----------------
+ <foo>bar</foo>
+(1 row)
+
+SELECT xmldocument('foo'::xml);
+ xmldocument
+-------------
+ foo
+(1 row)
+
+SELECT xmldocument('foo');
+ xmldocument
+-------------
+ foo
+(1 row)
+
+SELECT xmldocument('');
+ xmldocument
+-------------
+
+(1 row)
+
+SELECT xmldocument(' ');
+ xmldocument
+-------------
+
+(1 row)
+
+SELECT xmldocument(xmlcomment('comment'));
+ xmldocument
+----------------
+ <!--comment-->
+(1 row)
+
diff --git a/src/test/regress/sql/xml.sql b/src/test/regress/sql/xml.sql
index aafd39433a6..ce55de782b7 100644
--- a/src/test/regress/sql/xml.sql
+++ b/src/test/regress/sql/xml.sql
@@ -685,3 +685,22 @@ SELECT xmltext(' ');
SELECT xmltext('foo `$_-+?=*^%!|/\()[]{}');
SELECT xmltext('foo & <"bar">');
SELECT xmltext('x'|| '<P>73</P>'::xml || .42 || true || 'j'::char);
+
+SELECT
+ xmldocument(
+ xmlelement(NAME root,
+ xmlattributes(42 AS att),
+ xmlcomment('comment'),
+ xmlelement(NAME foo,'<foo&bar>'),
+ xmlelement(NAME bar, xmlconcat('va', 'lue')),
+ xmlpi(name pi),
+ xmlelement(NAME txt, xmltext('<"&>'))
+ )
+ );
+SELECT xmldocument(NULL);
+SELECT xmldocument('<foo>bar</foo>'::xml);
+SELECT xmldocument('foo'::xml);
+SELECT xmldocument('foo');
+SELECT xmldocument('');
+SELECT xmldocument(' ');
+SELECT xmldocument(xmlcomment('comment'));
--
2.54.0