rebase
Jim
From 0ef302deb60dc86cfd48e7f0625021a1add778a9 Mon Sep 17 00:00:00 2001
From: Jim Jones <[email protected]>
Date: Sat, 7 Nov 2026 09:41:45 +0100
Subject: [PATCH v13] Add XMLDocument function (SQL/XML X030)
This adds the SQL/XML function XMLDocument (feature X030). Per the
standard, XMLDocument applied to an XML value has effects equivalent
to the XML Query expression "document { $EXPR }": it replaces any
document nodes in the input with their children and wraps the result
in a single document node, which may have any number of child nodes.
Since our xml data type corresponds to XML(CONTENT(ANY)), any non-null
value of type xml is already valid output for XMLDocument, so the
function simply returns its input unchanged. While trivial, this
conforms to the standard and eases porting SQL from other systems
that provide XMLDocument. The RETURNING clause is not supported.
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)
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]>
Reviewed-by: Solai V <[email protected]>
---
doc/src/sgml/func/func-xml.sgml | 58 +++++++++++++++++++++++
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 | 70 ++++++++++++++++++++++++++++
src/test/regress/sql/xml.sql | 21 +++++++++
6 files changed, 171 insertions(+), 1 deletion(-)
diff --git a/doc/src/sgml/func/func-xml.sgml b/doc/src/sgml/func/func-xml.sgml
index 3b42a3100f9..81ee6c602dd 100644
--- a/doc/src/sgml/func/func-xml.sgml
+++ b/doc/src/sgml/func/func-xml.sgml
@@ -93,6 +93,64 @@ 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.
+ </para>
+
+ <para>
+ The SQL-standard <function>XMLDocument</function> function applied to an
+ XML value <literal>$EXPR</literal> has effects equivalent to the XML
+ Query expression <literal>document { $EXPR }</literal>.
+ It replaces any document nodes in the input with their children and wraps the whole
+ result in a single <firstterm>document node</firstterm>.
+ </para>
+
+ <para>
+ In the XML Query standard, a document node 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 a513b85a508..ac6f019d705 100644
--- a/src/backend/catalog/sql_features.txt
+++ b/src/backend/catalog/sql_features.txt
@@ -625,7 +625,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 44a3a21d135..2c8fb9ba149 100644
--- a/src/backend/utils/adt/xml.c
+++ b/src/backend/utils/adt/xml.c
@@ -522,6 +522,24 @@ xmlcomment(PG_FUNCTION_ARGS)
}
+/*
+ * xmldocument implements the SQL/XML function XMLDocument (X030).
+ * Since our XML data type corresponds to XML(CONTENT(ANY)), any
+ * non-null value of type xml, however it was produced, is already
+ * 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 9b78fcad414..cd03a736128 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -9362,6 +9362,9 @@
{ oid => '3813', descr => 'generate XML text node',
proname => 'xmltext', prorettype => 'xml', proargtypes => 'text',
prosrc => 'xmltext' },
+{ oid => '8220', 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 6998745ee03..9c1de0011c2 100644
--- a/src/test/regress/expected/xml.out
+++ b/src/test/regress/expected/xml.out
@@ -1933,3 +1933,73 @@ 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)
+
+SELECT xmldocument('<?xml version="1.0" standalone="yes"?><foo/>'::xml);
+ xmldocument
+----------------------------------------------
+ <?xml version="1.0" standalone="yes"?><foo/>
+(1 row)
+
+SELECT xmldocument('<!DOCTYPE foo [<!ELEMENT foo EMPTY>]><foo/>'::xml);
+ xmldocument
+---------------------------------------------
+ <!DOCTYPE foo [<!ELEMENT foo EMPTY>]><foo/>
+(1 row)
+
diff --git a/src/test/regress/sql/xml.sql b/src/test/regress/sql/xml.sql
index 0581d21b03f..b0cc0e4ea59 100644
--- a/src/test/regress/sql/xml.sql
+++ b/src/test/regress/sql/xml.sql
@@ -724,3 +724,24 @@ 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'));
+SELECT xmldocument('<?xml version="1.0" standalone="yes"?><foo/>'::xml);
+SELECT xmldocument('<!DOCTYPE foo [<!ELEMENT foo EMPTY>]><foo/>'::xml);
--
2.56.0