Hi
When an xml value with an XML declaration is used as element content,
xmlelement() and xmlforest() copy the declaration into the element, for
example:
== master ==
postgres=# select xmlelement(name e, '<?xml version="1.0"
standalone="yes"?><a/>'::xml);
xmlelement
---------------------------------------------------
<e><?xml version="1.0" standalone="yes"?><a/></e>
(1 row)
A declaration is only allowed at the start of a document, so xml_in()
rejects the result,and it cannot be restored from a dump..
postgres=# select xmlelement(name e, '<?xml version="1.0"
standalone="yes"?><a/>'::xml)::text::xml;
ERROR: invalid XML content
DETAIL: line 1: XML declaration allowed only at the start of the document
<e><?xml version="1.0" standalone="yes"?><a/></e>
^
For reference, xmlconcat() drops the declarations of its arguments...
postgres=# select xmlconcat('<e/>'::xml, '<?xml version="1.0"
standalone="yes"?><a/>'::xml);
xmlconcat
-----------
<e/><a/>
(1 row)
IIUC, the SQL/XML (ISO/IEC 9075-14) says that XMLELEMENT (6.15) and
XMLFOREST (6.16) take an xml argument as is and pass it to "Removing
XQuery document nodes from an XQuery sequence" (10.19), which does what
its name says: "Replace each XQuery document node in a sequence by the
sequence of children of the XQuery document node." Only the children of
the document end up in the element, and the declaration is not one of them.
The attached patch adds a flag to xml_out_internal() to omit the
declaration, and uses it in map_sql_value_to_xml_value() when an xml
value is embedded as content. Attribute values are not affected.
== patched ==
postgres=# select xmlelement(name e, '<?xml version="1.0"
standalone="yes"?><a/>'::xml);
xmlelement
-------------
<e><a/></e>
(1 row)
BTW Oracle[1] and Db2[2] both drop the declaration as well.
Thoughts on this approach?
Best, Jim
1 - https://dbfiddle.uk/gyzP8Wet
2 - https://dbfiddle.uk/NL09RP-8
From d830b88ed345a631242708139854692de0e727b8 Mon Sep 17 00:00:00 2001
From: Jim Jones <[email protected]>
Date: Mon, 9 Nov 2026 09:59:02 +0100
Subject: [PATCH v1] Omit the XML declaration of xml values embedded as element
content
xmlelement(), xmlforest() and the query_to_xml() family copied the XML
declaration of an xml argument into the element whenever xml_out()
would print it, i.e. for a standalone property or a version other than
1.0. A declaration is only allowed at the start of a document, so the
result could not be read back by xml_in().
Add a flag to xml_out_internal() to omit the declaration, and use it in
map_sql_value_to_xml_value() for xml values used as element content.
---
src/backend/utils/adt/xml.c | 20 +++++++++++++++-----
src/test/regress/expected/xml.out | 19 +++++++++++++++++++
src/test/regress/sql/xml.sql | 4 ++++
3 files changed, 38 insertions(+), 5 deletions(-)
diff --git a/src/backend/utils/adt/xml.c b/src/backend/utils/adt/xml.c
index 44a3a21d135..91f0bba9766 100644
--- a/src/backend/utils/adt/xml.c
+++ b/src/backend/utils/adt/xml.c
@@ -305,9 +305,11 @@ xml_in(PG_FUNCTION_ARGS)
*
* This is only acceptable so long as xmltype and text use the same
* representation.
+ *
+ * If omit_xmldecl is true, no XML declaration is printed.
*/
static char *
-xml_out_internal(xmltype *x, pg_enc target_encoding)
+xml_out_internal(xmltype *x, pg_enc target_encoding, bool omit_xmldecl)
{
char *str = text_to_cstring((text *) x);
@@ -324,7 +326,8 @@ xml_out_internal(xmltype *x, pg_enc target_encoding)
initStringInfo(&buf);
- if (!print_xml_decl(&buf, version, target_encoding, standalone))
+ if (omit_xmldecl ||
+ !print_xml_decl(&buf, version, target_encoding, standalone))
{
/*
* If we are not going to produce an XML declaration, eat a single
@@ -361,7 +364,7 @@ xml_out(PG_FUNCTION_ARGS)
* different client encoding, so we'd do more harm than good by including
* it.
*/
- PG_RETURN_CSTRING(xml_out_internal(x, 0));
+ PG_RETURN_CSTRING(xml_out_internal(x, 0, false));
}
@@ -444,7 +447,7 @@ xml_send(PG_FUNCTION_ARGS)
* xml_out_internal doesn't convert the encoding, it just prints the right
* declaration. pq_sendtext will do the conversion.
*/
- outval = xml_out_internal(x, pg_get_client_encoding());
+ outval = xml_out_internal(x, pg_get_client_encoding(), false);
pq_begintypsend(&buf);
pq_sendtext(&buf, outval, strlen(outval));
@@ -2725,6 +2728,13 @@ map_sql_value_to_xml_value(Datum value, Oid type, bool xml_escape_strings)
}
+ /*
+ * An XML declaration is only allowed at the start of a document, so
+ * omit it from xml values used as element content.
+ */
+ if (type == XMLOID && xml_escape_strings)
+ return xml_out_internal(DatumGetXmlP(value), 0, true);
+
/*
* otherwise, just use the type's native text representation
*/
@@ -4836,7 +4846,7 @@ XmlTableSetDocument(TableFuncScanState *state, Datum value)
* Use out function for casting to string (remove encoding property). See
* comment in xml_out.
*/
- str = xml_out_internal(xmlval, 0);
+ str = xml_out_internal(xmlval, 0, false);
length = strlen(str);
xstr = pg_xmlCharStrndup(str, length);
diff --git a/src/test/regress/expected/xml.out b/src/test/regress/expected/xml.out
index 6998745ee03..66c0ac763c7 100644
--- a/src/test/regress/expected/xml.out
+++ b/src/test/regress/expected/xml.out
@@ -190,6 +190,25 @@ SELECT xmlelement(name foo, xml 'b<a/>r');
<foo>b<a/>r</foo>
(1 row)
+-- an XML declaration is not kept when an xml value is embedded as content
+SELECT xmlelement(name foo, '<?xml version="1.0" standalone="yes"?><a/>'::xml);
+ xmlelement
+-----------------
+ <foo><a/></foo>
+(1 row)
+
+SELECT xmlforest('<?xml version="1.1"?><a/>'::xml AS foo);
+ xmlforest
+-----------------
+ <foo><a/></foo>
+(1 row)
+
+SELECT xmlelement(name foo, array['<?xml version="1.0" standalone="no"?><a/>'::xml]);
+ xmlelement
+------------------------------------
+ <foo><element><a/></element></foo>
+(1 row)
+
SELECT xmlelement(name foo, array[1, 2, 3]);
xmlelement
-------------------------------------------------------------------------
diff --git a/src/test/regress/sql/xml.sql b/src/test/regress/sql/xml.sql
index 0581d21b03f..ce38a19bafb 100644
--- a/src/test/regress/sql/xml.sql
+++ b/src/test/regress/sql/xml.sql
@@ -62,6 +62,10 @@ SELECT xmlelement(name foo, text 'bar');
SELECT xmlelement(name foo, xml 'bar');
SELECT xmlelement(name foo, text 'b<a/>r');
SELECT xmlelement(name foo, xml 'b<a/>r');
+-- an XML declaration is not kept when an xml value is embedded as content
+SELECT xmlelement(name foo, '<?xml version="1.0" standalone="yes"?><a/>'::xml);
+SELECT xmlforest('<?xml version="1.1"?><a/>'::xml AS foo);
+SELECT xmlelement(name foo, array['<?xml version="1.0" standalone="no"?><a/>'::xml]);
SELECT xmlelement(name foo, array[1, 2, 3]);
SET xmlbinary TO base64;
SELECT xmlelement(name foo, bytea 'bar');
--
2.56.0