On 14.03.23 18:40, Tom Lane wrote:
Jim Jones <jim.jo...@uni-muenster.de> writes:
[ v22-0001-Add-pretty-printed-XML-output-option.patch ]
I poked at this for awhile and ran into a problem that I'm not sure
how to solve: it misbehaves for input with embedded DOCTYPE.
regression=# SELECT xmlserialize(DOCUMENT '<!DOCTYPE a><a/>' as text indent);
xmlserialize
--------------
<!DOCTYPE a>+
<a></a> +
(1 row)
The issue was the flag XML_SAVE_NO_EMPTY. It was forcing empty elements
to be serialized with start-end tag pairs. Removing it did the trick ...
postgres=# SELECT xmlserialize(DOCUMENT '<!DOCTYPE a><a/>' AS text INDENT);
xmlserialize
--------------
<!DOCTYPE a>+
<a/> +
(1 row)
... but as a side effect empty start-end tags will be now serialized as
empty elements
postgres=# SELECT xmlserialize(CONTENT '<foo><bar></bar></foo>' AS text
INDENT);
xmlserialize
--------------
<foo> +
<bar/> +
</foo>
(1 row)
It seems to be the standard behavior of other xml indent tools
(including Oracle)
regression=# SELECT xmlserialize(CONTENT '<!DOCTYPE a><a/>' as text indent);
xmlserialize
--------------
(1 row)
The bad result for CONTENT is because xml_parse() decides to
parse_as_document, but xmlserialize_indent has no idea that happened
and tries to use the content_nodes list anyway. I don't especially
care for the laissez faire "maybe we'll set *content_nodes and maybe
we won't" API you adopted for xml_parse, which seems to be contributing
to the mess. We could pass back more info so that xmlserialize_indent
knows what really happened.
I added a new (nullable) parameter to the xml_parse function that will
return the actual XmlOptionType used to parse the xml data. Now
xmlserialize_indent knows how the data was really parsed:
postgres=# SELECT xmlserialize(CONTENT '<!DOCTYPE a><a/>' AS text INDENT);
xmlserialize
--------------
<!DOCTYPE a>+
<a/> +
(1 row)
I added test cases for these queries.
v23 attached.
Thanks!
Best, Jim
From 98fe15f07da345e046b8d29d5dde27ce191055a2 Mon Sep 17 00:00:00 2001
From: Jim Jones <jim.jo...@uni-muenster.de>
Date: Fri, 10 Mar 2023 13:47:16 +0100
Subject: [PATCH v23] Add pretty-printed XML output option
This patch implements the XML/SQL:2011 feature 'X069, XMLSERIALIZE: INDENT.'
It adds the options INDENT and NO INDENT (default) to the existing
xmlserialize function. It uses the indentation feature of xmlSaveToBuffer
from libxml2 to indent XML strings - see option XML_SAVE_FORMAT.
Although the INDENT feature is designed to work with xml strings of type
DOCUMENT, this implementation also allows the usage of CONTENT type strings
as long as it contains a well balanced xml.
This patch also includes documentation, regression tests and their three
possible output files xml.out, xml_1.out and xml_2.out.
---
doc/src/sgml/datatype.sgml | 8 +-
src/backend/catalog/sql_features.txt | 2 +-
src/backend/executor/execExprInterp.c | 9 +-
src/backend/parser/gram.y | 14 +-
src/backend/parser/parse_expr.c | 1 +
src/backend/utils/adt/xml.c | 154 +++++++++++++++++++--
src/include/nodes/parsenodes.h | 1 +
src/include/nodes/primnodes.h | 4 +-
src/include/parser/kwlist.h | 1 +
src/include/utils/xml.h | 1 +
src/test/regress/expected/xml.out | 188 ++++++++++++++++++++++++++
src/test/regress/expected/xml_1.out | 106 +++++++++++++++
src/test/regress/expected/xml_2.out | 188 ++++++++++++++++++++++++++
src/test/regress/sql/xml.sql | 38 ++++++
14 files changed, 697 insertions(+), 18 deletions(-)
diff --git a/doc/src/sgml/datatype.sgml b/doc/src/sgml/datatype.sgml
index 467b49b199..53d59662b9 100644
--- a/doc/src/sgml/datatype.sgml
+++ b/doc/src/sgml/datatype.sgml
@@ -4460,14 +4460,18 @@ xml '<foo>bar</foo>'
<type>xml</type>, uses the function
<function>xmlserialize</function>:<indexterm><primary>xmlserialize</primary></indexterm>
<synopsis>
-XMLSERIALIZE ( { DOCUMENT | CONTENT } <replaceable>value</replaceable> AS <replaceable>type</replaceable> )
+XMLSERIALIZE ( { DOCUMENT | CONTENT } <replaceable>value</replaceable> AS <replaceable>type</replaceable> [ [NO] INDENT ] )
</synopsis>
<replaceable>type</replaceable> can be
<type>character</type>, <type>character varying</type>, or
<type>text</type> (or an alias for one of those). Again, according
to the SQL standard, this is the only way to convert between type
<type>xml</type> and character types, but PostgreSQL also allows
- you to simply cast the value.
+ you to simply cast the value. The option <type>INDENT</type> allows to
+ indent the serialized xml output - the default is <type>NO INDENT</type>.
+ It is designed to indent XML strings of type <type>DOCUMENT</type>, but it can also
+ be used with <type>CONTENT</type> as long as <replaceable>value</replaceable>
+ contains a well-formed XML.
</para>
<para>
diff --git a/src/backend/catalog/sql_features.txt b/src/backend/catalog/sql_features.txt
index 0fb9ab7533..bb4c135a7f 100644
--- a/src/backend/catalog/sql_features.txt
+++ b/src/backend/catalog/sql_features.txt
@@ -621,7 +621,7 @@ X061 XMLParse: character string input and DOCUMENT option YES
X065 XMLParse: binary string input and CONTENT option NO
X066 XMLParse: binary string input and DOCUMENT option NO
X068 XMLSerialize: BOM NO
-X069 XMLSerialize: INDENT NO
+X069 XMLSerialize: INDENT YES
X070 XMLSerialize: character string serialization and CONTENT option YES
X071 XMLSerialize: character string serialization and DOCUMENT option YES
X072 XMLSerialize: character string serialization YES
diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
index 19351fe34b..6e4425ca7c 100644
--- a/src/backend/executor/execExprInterp.c
+++ b/src/backend/executor/execExprInterp.c
@@ -3829,6 +3829,7 @@ ExecEvalXmlExpr(ExprState *state, ExprEvalStep *op)
{
Datum *argvalue = op->d.xmlexpr.argvalue;
bool *argnull = op->d.xmlexpr.argnull;
+ text *result;
/* argument type is known to be xml */
Assert(list_length(xexpr->args) == 1);
@@ -3837,8 +3838,12 @@ ExecEvalXmlExpr(ExprState *state, ExprEvalStep *op)
return;
value = argvalue[0];
- *op->resvalue = PointerGetDatum(xmltotext_with_xmloption(DatumGetXmlP(value),
- xexpr->xmloption));
+ result = xmltotext_with_xmloption(DatumGetXmlP(value),
+ xexpr->xmloption);
+ if (xexpr->indent)
+ result = xmlserialize_indent(result,xexpr->xmloption);
+
+ *op->resvalue = PointerGetDatum(result);
*op->resnull = false;
}
break;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index a0138382a1..efe88ccf9d 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -613,7 +613,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
%type <node> xml_root_version opt_xml_root_standalone
%type <node> xmlexists_argument
%type <ival> document_or_content
-%type <boolean> xml_whitespace_option
+%type <boolean> xml_indent_option xml_whitespace_option
%type <list> xmltable_column_list xmltable_column_option_list
%type <node> xmltable_column_el
%type <defelt> xmltable_column_option_el
@@ -702,7 +702,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
HANDLER HAVING HEADER_P HOLD HOUR_P
IDENTITY_P IF_P ILIKE IMMEDIATE IMMUTABLE IMPLICIT_P IMPORT_P IN_P INCLUDE
- INCLUDING INCREMENT INDEX INDEXES INHERIT INHERITS INITIALLY INLINE_P
+ INCLUDING INCREMENT INDENT INDEX INDEXES INHERIT INHERITS INITIALLY INLINE_P
INNER_P INOUT INPUT_P INSENSITIVE INSERT INSTEAD INT_P INTEGER
INTERSECT INTERVAL INTO INVOKER IS ISNULL ISOLATION
@@ -15532,13 +15532,14 @@ func_expr_common_subexpr:
$$ = makeXmlExpr(IS_XMLROOT, NULL, NIL,
list_make3($3, $5, $6), @1);
}
- | XMLSERIALIZE '(' document_or_content a_expr AS SimpleTypename ')'
+ | XMLSERIALIZE '(' document_or_content a_expr AS SimpleTypename xml_indent_option ')'
{
XmlSerialize *n = makeNode(XmlSerialize);
n->xmloption = $3;
n->expr = $4;
n->typeName = $6;
+ n->indent = $7;
n->location = @1;
$$ = (Node *) n;
}
@@ -15592,6 +15593,11 @@ document_or_content: DOCUMENT_P { $$ = XMLOPTION_DOCUMENT; }
| CONTENT_P { $$ = XMLOPTION_CONTENT; }
;
+xml_indent_option: INDENT { $$ = true; }
+ | NO INDENT { $$ = false; }
+ | /*EMPTY*/ { $$ = false; }
+ ;
+
xml_whitespace_option: PRESERVE WHITESPACE_P { $$ = true; }
| STRIP_P WHITESPACE_P { $$ = false; }
| /*EMPTY*/ { $$ = false; }
@@ -16828,6 +16834,7 @@ unreserved_keyword:
| INCLUDE
| INCLUDING
| INCREMENT
+ | INDENT
| INDEX
| INDEXES
| INHERIT
@@ -17384,6 +17391,7 @@ bare_label_keyword:
| INCLUDE
| INCLUDING
| INCREMENT
+ | INDENT
| INDEX
| INDEXES
| INHERIT
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 78221d2e0f..2331417552 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -2331,6 +2331,7 @@ transformXmlSerialize(ParseState *pstate, XmlSerialize *xs)
typenameTypeIdAndMod(pstate, xs->typeName, &targetType, &targetTypmod);
xexpr->xmloption = xs->xmloption;
+ xexpr->indent = xs->indent;
xexpr->location = xs->location;
/* We actually only need these to be able to parse back the expression. */
xexpr->type = targetType;
diff --git a/src/backend/utils/adt/xml.c b/src/backend/utils/adt/xml.c
index 079bcb1208..facd111f4f 100644
--- a/src/backend/utils/adt/xml.c
+++ b/src/backend/utils/adt/xml.c
@@ -52,6 +52,7 @@
#include <libxml/tree.h>
#include <libxml/uri.h>
#include <libxml/xmlerror.h>
+#include <libxml/xmlsave.h>
#include <libxml/xmlversion.h>
#include <libxml/xmlwriter.h>
#include <libxml/xpath.h>
@@ -146,7 +147,8 @@ static bool print_xml_decl(StringInfo buf, const xmlChar *version,
static bool xml_doctype_in_content(const xmlChar *str);
static xmlDocPtr xml_parse(text *data, XmlOptionType xmloption_arg,
bool preserve_whitespace, int encoding,
- Node *escontext);
+ Node *escontext, xmlNodePtr *parsed_nodes,
+ XmlOptionType *parsed_xmloptiontype);
static text *xml_xmlnodetoxmltype(xmlNodePtr cur, PgXmlErrorContext *xmlerrcxt);
static int xml_xpathobjtoxmlarray(xmlXPathObjectPtr xpathobj,
ArrayBuildState *astate,
@@ -273,7 +275,7 @@ xml_in(PG_FUNCTION_ARGS)
* Note: we don't need to worry about whether a soft error is detected.
*/
doc = xml_parse(vardata, xmloption, true, GetDatabaseEncoding(),
- fcinfo->context);
+ fcinfo->context, NULL,NULL);
if (doc != NULL)
xmlFreeDoc(doc);
@@ -400,7 +402,7 @@ xml_recv(PG_FUNCTION_ARGS)
* Parse the data to check if it is well-formed XML data. Assume that
* xml_parse will throw ERROR if not.
*/
- doc = xml_parse(result, xmloption, true, encoding, NULL);
+ doc = xml_parse(result, xmloption, true, encoding, NULL, NULL,NULL);
xmlFreeDoc(doc);
/* Now that we know what we're dealing with, convert to server encoding */
@@ -631,6 +633,123 @@ xmltotext_with_xmloption(xmltype *data, XmlOptionType xmloption_arg)
}
+text *
+xmlserialize_indent(text *data, XmlOptionType xmloption_arg)
+{
+#ifdef USE_LIBXML
+ text *result;
+ xmlDocPtr doc;
+ xmlSaveCtxtPtr ctxt = NULL;
+ xmlBufferPtr buf = NULL;
+ xmlChar *version;
+ xmlNodePtr content_nodes = NULL;
+ PgXmlErrorContext *xmlerrcxt;
+ XmlOptionType parsed_xmloptiontype;
+
+ parse_xml_decl(xml_text2xmlChar(data), NULL, &version, NULL, NULL);
+
+ doc = xml_parse(data, xmloption_arg, true,
+ GetDatabaseEncoding(), NULL, &content_nodes, &parsed_xmloptiontype);
+ Assert(doc);
+
+ xmlerrcxt = pg_xml_init(PG_XML_STRICTNESS_ALL);
+
+ PG_TRY();
+ {
+ buf = xmlBufferCreate();
+
+ if (buf == NULL || xmlerrcxt->err_occurred)
+ xml_ereport(xmlerrcxt, ERROR, ERRCODE_OUT_OF_MEMORY,
+ "could not allocate xmlBuffer");
+
+ if(!version)
+ ctxt = xmlSaveToBuffer(buf, GetDatabaseEncodingName(),
+ XML_SAVE_NO_DECL | XML_SAVE_FORMAT);
+ else
+ ctxt = xmlSaveToBuffer(buf, GetDatabaseEncodingName(),
+ XML_SAVE_FORMAT);
+
+ if (ctxt == NULL || xmlerrcxt->err_occurred)
+ xml_ereport(xmlerrcxt, ERROR, ERRCODE_OUT_OF_MEMORY,
+ "could not allocate parser context");
+
+ if(parsed_xmloptiontype == XMLOPTION_DOCUMENT)
+ {
+ if (xmlSaveDoc(ctxt, doc) == -1 || xmlerrcxt->err_occurred)
+ xml_ereport(xmlerrcxt, ERROR, ERRCODE_INTERNAL_ERROR,
+ "could not save document to xmlBuffer");
+ }
+ else
+ {
+ if(content_nodes != NULL)
+ {
+ xmlNodePtr root = NULL;
+ xmlNodePtr node = NULL;
+
+ /* This creates a root node for returned content from xml_parse,
+ * as it can contain a non singly-rooted XML. This is necessary
+ * to avoid the dump functions to ignore XML strings with
+ * multiple root nodes (CONTENT type). This new root node serves
+ * only as a container, so that we can iterate over its nodes
+ * and save each one of the formatted children into the buffer.
+ * Nodes are separated by a newline.
+ */
+ root = xmlNewNode(NULL, BAD_CAST "content-root");
+ xmlDocSetRootElement(doc, root);
+ xmlAddChild(root, content_nodes);
+
+ for (node = root->children; node; node = node->next) {
+
+ if (node->type != XML_TEXT_NODE && node->prev != NULL)
+ {
+ xmlNodePtr newline = NULL;
+ newline = xmlNewDocText(doc, (const xmlChar *) "\n");
+
+ if (xmlSaveTree(ctxt, newline) == -1 || xmlerrcxt->err_occurred)
+ xml_ereport(xmlerrcxt, ERROR, ERRCODE_INTERNAL_ERROR,
+ "could not save content's line separator to xmlBuffer");
+ }
+
+ if (xmlSaveTree(ctxt, node) == -1 || xmlerrcxt->err_occurred)
+ xml_ereport(xmlerrcxt, ERROR, ERRCODE_INTERNAL_ERROR,
+ "could not save content to xmlBuffer");
+ }
+ }
+ }
+
+ if (xmlSaveClose(ctxt) == -1 || xmlerrcxt->err_occurred)
+ xml_ereport(xmlerrcxt, ERROR, ERRCODE_INTERNAL_ERROR,
+ "could not close xmlSaveCtxtPtr");
+ }
+ PG_CATCH();
+ {
+ if (buf)
+ xmlBufferFree(buf);
+ if(doc)
+ xmlFreeDoc(doc);
+ if(ctxt)
+ xmlSaveClose(ctxt);
+
+ pg_xml_done(xmlerrcxt, true);
+
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ pg_xml_done(xmlerrcxt, false);
+ xmlFreeDoc(doc);
+
+ result = (text *) xmlBuffer_to_xmltype(buf);
+ xmlBufferFree(buf);
+
+ return result;
+#else
+ NO_XML_SUPPORT();
+ return NULL;
+#endif
+}
+
+
xmltype *
xmlelement(XmlExpr *xexpr,
Datum *named_argvalue, bool *named_argnull,
@@ -762,7 +881,7 @@ xmlparse(text *data, XmlOptionType xmloption_arg, bool preserve_whitespace)
xmlDocPtr doc;
doc = xml_parse(data, xmloption_arg, preserve_whitespace,
- GetDatabaseEncoding(), NULL);
+ GetDatabaseEncoding(), NULL, NULL,NULL);
xmlFreeDoc(doc);
return (xmltype *) data;
@@ -902,7 +1021,7 @@ xml_is_document(xmltype *arg)
* We'll report "true" if no soft error is reported by xml_parse().
*/
doc = xml_parse((text *) arg, XMLOPTION_DOCUMENT, true,
- GetDatabaseEncoding(), (Node *) &escontext);
+ GetDatabaseEncoding(), (Node *) &escontext, NULL,NULL);
if (doc)
xmlFreeDoc(doc);
@@ -1489,7 +1608,11 @@ xml_doctype_in_content(const xmlChar *str)
*
* data is the source data (must not be toasted!), encoding is its encoding,
* and xmloption_arg and preserve_whitespace are options for the
- * transformation.
+ * transformation. parsed_nodes will return the list of parsed nodes
+ * for XML of type XMLOPTION_CONTENT from the xmlParseBalancedChunkMemory
+ * call - it can be NULL. parsed_xmloptiontype will return the actual
+ * XmlOptionType used to parse the given data, as it may differ from
+ * xmloption_arg if the xml contains DOCTYPE declarations - it can be NULL.
*
* Errors normally result in ereport(ERROR), but if escontext is an
* ErrorSaveContext, then "safe" errors are reported there instead, and the
@@ -1504,7 +1627,8 @@ xml_doctype_in_content(const xmlChar *str)
*/
static xmlDocPtr
xml_parse(text *data, XmlOptionType xmloption_arg, bool preserve_whitespace,
- int encoding, Node *escontext)
+ int encoding, Node *escontext, xmlNodePtr *parsed_nodes,
+ XmlOptionType *parsed_xmloptiontype)
{
int32 len;
xmlChar *string;
@@ -1552,9 +1676,16 @@ xml_parse(text *data, XmlOptionType xmloption_arg, bool preserve_whitespace,
xml_ereport(xmlerrcxt, ERROR, ERRCODE_OUT_OF_MEMORY,
"could not allocate parser context");
+ if(parsed_xmloptiontype!=NULL)
+ *parsed_xmloptiontype = XMLOPTION_CONTENT;
+
/* Decide whether to parse as document or content */
if (xmloption_arg == XMLOPTION_DOCUMENT)
+ {
parse_as_document = true;
+ if(parsed_xmloptiontype!=NULL)
+ *parsed_xmloptiontype = XMLOPTION_DOCUMENT;
+ }
else
{
/* Parse and skip over the XML declaration, if any */
@@ -1571,7 +1702,12 @@ xml_parse(text *data, XmlOptionType xmloption_arg, bool preserve_whitespace,
/* Is there a DOCTYPE element? */
if (xml_doctype_in_content(utf8string + count))
+ {
parse_as_document = true;
+
+ if(parsed_xmloptiontype!=NULL)
+ *parsed_xmloptiontype = XMLOPTION_DOCUMENT;
+ }
}
if (parse_as_document)
@@ -1620,7 +1756,7 @@ xml_parse(text *data, XmlOptionType xmloption_arg, bool preserve_whitespace,
if (*(utf8string + count))
{
res_code = xmlParseBalancedChunkMemory(doc, NULL, NULL, 0,
- utf8string + count, NULL);
+ utf8string + count, parsed_nodes);
if (res_code != 0 || xmlerrcxt->err_occurred)
{
xml_errsave(escontext, xmlerrcxt,
@@ -4305,7 +4441,7 @@ wellformed_xml(text *data, XmlOptionType xmloption_arg)
* We'll report "true" if no soft error is reported by xml_parse().
*/
doc = xml_parse(data, xmloption_arg, true,
- GetDatabaseEncoding(), (Node *) &escontext);
+ GetDatabaseEncoding(), (Node *) &escontext, NULL,NULL);
if (doc)
xmlFreeDoc(doc);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index f7d7f10f7d..fc5b89a698 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -841,6 +841,7 @@ typedef struct XmlSerialize
XmlOptionType xmloption; /* DOCUMENT or CONTENT */
Node *expr;
TypeName *typeName;
+ bool indent; /* [NO] INDENT */
int location; /* token location, or -1 if unknown */
} XmlSerialize;
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index b4292253cc..2263dab8a1 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -1461,7 +1461,7 @@ typedef enum XmlExprOp
IS_XMLPARSE, /* XMLPARSE(text, is_doc, preserve_ws) */
IS_XMLPI, /* XMLPI(name [, args]) */
IS_XMLROOT, /* XMLROOT(xml, version, standalone) */
- IS_XMLSERIALIZE, /* XMLSERIALIZE(is_document, xmlval) */
+ IS_XMLSERIALIZE, /* XMLSERIALIZE(is_document, xmlval, indent) */
IS_DOCUMENT /* xmlval IS DOCUMENT */
} XmlExprOp;
@@ -1486,6 +1486,8 @@ typedef struct XmlExpr
List *args;
/* DOCUMENT or CONTENT */
XmlOptionType xmloption pg_node_attr(query_jumble_ignore);
+ /* INDENT option for XMLSERIALIZE */
+ bool indent;
/* target type/typmod for XMLSERIALIZE */
Oid type pg_node_attr(query_jumble_ignore);
int32 typmod pg_node_attr(query_jumble_ignore);
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index bb36213e6f..753e9ee174 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -205,6 +205,7 @@ PG_KEYWORD("in", IN_P, RESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("include", INCLUDE, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("including", INCLUDING, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("increment", INCREMENT, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("indent", INDENT, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("index", INDEX, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("indexes", INDEXES, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("inherit", INHERIT, UNRESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/include/utils/xml.h b/src/include/utils/xml.h
index 311da06cd6..ea14eae712 100644
--- a/src/include/utils/xml.h
+++ b/src/include/utils/xml.h
@@ -78,6 +78,7 @@ extern xmltype *xmlpi(const char *target, text *arg, bool arg_is_null, bool *res
extern xmltype *xmlroot(xmltype *data, text *version, int standalone);
extern bool xml_is_document(xmltype *arg);
extern text *xmltotext_with_xmloption(xmltype *data, XmlOptionType xmloption_arg);
+extern text *xmlserialize_indent(text *data, XmlOptionType xmloption_arg);
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 ad852dc2f7..8f1f3c7e65 100644
--- a/src/test/regress/expected/xml.out
+++ b/src/test/regress/expected/xml.out
@@ -486,6 +486,194 @@ SELECT xmlserialize(content 'good' as char(10));
SELECT xmlserialize(document 'bad' as text);
ERROR: not an XML document
+-- indent
+SELECT xmlserialize(DOCUMENT '<foo><bar><val x="y">42</val></bar></foo>' AS text INDENT);
+ xmlserialize
+-------------------------
+ <foo> +
+ <bar> +
+ <val x="y">42</val>+
+ </bar> +
+ </foo> +
+
+(1 row)
+
+SELECT xmlserialize(CONTENT '<foo><bar><val x="y">42</val></bar></foo>' AS text INDENT);
+ xmlserialize
+-------------------------
+ <foo> +
+ <bar> +
+ <val x="y">42</val>+
+ </bar> +
+ </foo>
+(1 row)
+
+-- no indent
+SELECT xmlserialize(DOCUMENT '<foo><bar><val x="y">42</val></bar></foo>' AS text NO INDENT);
+ xmlserialize
+-------------------------------------------
+ <foo><bar><val x="y">42</val></bar></foo>
+(1 row)
+
+SELECT xmlserialize(CONTENT '<foo><bar><val x="y">42</val></bar></foo>' AS text NO INDENT);
+ xmlserialize
+-------------------------------------------
+ <foo><bar><val x="y">42</val></bar></foo>
+(1 row)
+
+\set VERBOSITY terse
+-- indent non singly-rooted xml
+SELECT xmlserialize(DOCUMENT '<foo>73</foo><bar><val x="y">42</val></bar>' AS text INDENT);
+ERROR: not an XML document
+SELECT xmlserialize(CONTENT '<foo>73</foo><bar><val x="y">42</val></bar>' AS text INDENT);
+ xmlserialize
+-----------------------
+ <foo>73</foo> +
+ <bar> +
+ <val x="y">42</val>+
+ </bar>
+(1 row)
+
+-- indent non singly-rooted xml with mixed contents
+SELECT xmlserialize(DOCUMENT 'text node<foo>73</foo>text node<bar><val x="y">42</val></bar>' AS text INDENT);
+ERROR: not an XML document
+SELECT xmlserialize(CONTENT 'text node<foo>73</foo>text node<bar><val x="y">42</val></bar>' AS text INDENT);
+ xmlserialize
+------------------------
+ text node +
+ <foo>73</foo>text node+
+ <bar> +
+ <val x="y">42</val> +
+ </bar>
+(1 row)
+
+-- indent singly-rooted xml with mixed contents
+SELECT xmlserialize(DOCUMENT '<foo><bar><val x="y">42</val><val x="y">text node<val>73</val></val></bar></foo>' AS text INDENT);
+ xmlserialize
+---------------------------------------------
+ <foo> +
+ <bar> +
+ <val x="y">42</val> +
+ <val x="y">text node<val>73</val></val>+
+ </bar> +
+ </foo> +
+
+(1 row)
+
+SELECT xmlserialize(CONTENT '<foo><bar><val x="y">42</val><val x="y">text node<val>73</val></val></bar></foo>' AS text INDENT);
+ xmlserialize
+---------------------------------------------
+ <foo> +
+ <bar> +
+ <val x="y">42</val> +
+ <val x="y">text node<val>73</val></val>+
+ </bar> +
+ </foo>
+(1 row)
+
+-- indent empty string
+SELECT xmlserialize(DOCUMENT '' AS text INDENT);
+ERROR: not an XML document
+SELECT xmlserialize(CONTENT '' AS text INDENT);
+ xmlserialize
+--------------
+
+(1 row)
+
+-- whitespaces
+SELECT xmlserialize(DOCUMENT ' ' AS text INDENT);
+ERROR: not an XML document
+SELECT xmlserialize(CONTENT ' ' AS text INDENT);
+ xmlserialize
+--------------
+
+(1 row)
+
+\set VERBOSITY default
+-- indent null
+SELECT xmlserialize(DOCUMENT NULL AS text INDENT);
+ xmlserialize
+--------------
+
+(1 row)
+
+SELECT xmlserialize(CONTENT NULL AS text INDENT);
+ xmlserialize
+--------------
+
+(1 row)
+
+-- indent with XML declaration
+SELECT xmlserialize(DOCUMENT '<?xml version="1.0" encoding="UTF-8"?><foo><bar><val>73</val></bar></foo>' AS text INDENT);
+ xmlserialize
+---------------------------------------
+ <?xml version="1.0" encoding="UTF8"?>+
+ <foo> +
+ <bar> +
+ <val>73</val> +
+ </bar> +
+ </foo> +
+
+(1 row)
+
+SELECT xmlserialize(CONTENT '<?xml version="1.0" encoding="UTF-8"?><foo><bar><val>73</val></bar></foo>' AS text INDENT);
+ xmlserialize
+-------------------
+ <foo> +
+ <bar> +
+ <val>73</val>+
+ </bar> +
+ </foo>
+(1 row)
+
+-- indent containing DOCTYPE declaration
+SELECT xmlserialize(DOCUMENT '<!DOCTYPE a><a/>' AS text INDENT);
+ xmlserialize
+--------------
+ <!DOCTYPE a>+
+ <a/> +
+
+(1 row)
+
+SELECT xmlserialize(CONTENT '<!DOCTYPE a><a/>' AS text INDENT);
+ xmlserialize
+--------------
+ <!DOCTYPE a>+
+ <a/> +
+
+(1 row)
+
+-- indent xml with empty element
+SELECT xmlserialize(DOCUMENT '<foo><bar></bar></foo>' AS text INDENT);
+ xmlserialize
+--------------
+ <foo> +
+ <bar/> +
+ </foo> +
+
+(1 row)
+
+SELECT xmlserialize(CONTENT '<foo><bar></bar></foo>' AS text INDENT);
+ xmlserialize
+--------------
+ <foo> +
+ <bar/> +
+ </foo>
+(1 row)
+
+-- 'no indent' = not using 'no indent'
+SELECT xmlserialize(DOCUMENT '<foo><bar><val x="y">42</val></bar></foo>' AS text) = xmlserialize(DOCUMENT '<foo><bar><val x="y">42</val></bar></foo>' AS text NO INDENT);
+ ?column?
+----------
+ t
+(1 row)
+
+SELECT xmlserialize(CONTENT '<foo><bar><val x="y">42</val></bar></foo>' AS text) = xmlserialize(CONTENT '<foo><bar><val x="y">42</val></bar></foo>' AS text NO INDENT);
+ ?column?
+----------
+ t
+(1 row)
+
SELECT xml '<foo>bar</foo>' IS DOCUMENT;
?column?
----------
diff --git a/src/test/regress/expected/xml_1.out b/src/test/regress/expected/xml_1.out
index 70fe34a04f..6e08f8587e 100644
--- a/src/test/regress/expected/xml_1.out
+++ b/src/test/regress/expected/xml_1.out
@@ -309,6 +309,112 @@ ERROR: unsupported XML feature
LINE 1: SELECT xmlserialize(document 'bad' as text);
^
DETAIL: This functionality requires the server to be built with libxml support.
+-- indent
+SELECT xmlserialize(DOCUMENT '<foo><bar><val x="y">42</val></bar></foo>' AS text INDENT);
+ERROR: unsupported XML feature
+LINE 1: SELECT xmlserialize(DOCUMENT '<foo><bar><val x="y">42</val><...
+ ^
+DETAIL: This functionality requires the server to be built with libxml support.
+SELECT xmlserialize(CONTENT '<foo><bar><val x="y">42</val></bar></foo>' AS text INDENT);
+ERROR: unsupported XML feature
+LINE 1: SELECT xmlserialize(CONTENT '<foo><bar><val x="y">42</val><...
+ ^
+DETAIL: This functionality requires the server to be built with libxml support.
+-- no indent
+SELECT xmlserialize(DOCUMENT '<foo><bar><val x="y">42</val></bar></foo>' AS text NO INDENT);
+ERROR: unsupported XML feature
+LINE 1: SELECT xmlserialize(DOCUMENT '<foo><bar><val x="y">42</val><...
+ ^
+DETAIL: This functionality requires the server to be built with libxml support.
+SELECT xmlserialize(CONTENT '<foo><bar><val x="y">42</val></bar></foo>' AS text NO INDENT);
+ERROR: unsupported XML feature
+LINE 1: SELECT xmlserialize(CONTENT '<foo><bar><val x="y">42</val><...
+ ^
+DETAIL: This functionality requires the server to be built with libxml support.
+\set VERBOSITY terse
+-- indent non singly-rooted xml
+SELECT xmlserialize(DOCUMENT '<foo>73</foo><bar><val x="y">42</val></bar>' AS text INDENT);
+ERROR: unsupported XML feature at character 30
+SELECT xmlserialize(CONTENT '<foo>73</foo><bar><val x="y">42</val></bar>' AS text INDENT);
+ERROR: unsupported XML feature at character 30
+-- indent non singly-rooted xml with mixed contents
+SELECT xmlserialize(DOCUMENT 'text node<foo>73</foo>text node<bar><val x="y">42</val></bar>' AS text INDENT);
+ERROR: unsupported XML feature at character 30
+SELECT xmlserialize(CONTENT 'text node<foo>73</foo>text node<bar><val x="y">42</val></bar>' AS text INDENT);
+ERROR: unsupported XML feature at character 30
+-- indent singly-rooted xml with mixed contents
+SELECT xmlserialize(DOCUMENT '<foo><bar><val x="y">42</val><val x="y">text node<val>73</val></val></bar></foo>' AS text INDENT);
+ERROR: unsupported XML feature at character 30
+SELECT xmlserialize(CONTENT '<foo><bar><val x="y">42</val><val x="y">text node<val>73</val></val></bar></foo>' AS text INDENT);
+ERROR: unsupported XML feature at character 30
+-- indent empty string
+SELECT xmlserialize(DOCUMENT '' AS text INDENT);
+ERROR: unsupported XML feature at character 30
+SELECT xmlserialize(CONTENT '' AS text INDENT);
+ERROR: unsupported XML feature at character 30
+-- whitespaces
+SELECT xmlserialize(DOCUMENT ' ' AS text INDENT);
+ERROR: unsupported XML feature at character 30
+SELECT xmlserialize(CONTENT ' ' AS text INDENT);
+ERROR: unsupported XML feature at character 30
+\set VERBOSITY default
+-- indent null
+SELECT xmlserialize(DOCUMENT NULL AS text INDENT);
+ xmlserialize
+--------------
+
+(1 row)
+
+SELECT xmlserialize(CONTENT NULL AS text INDENT);
+ xmlserialize
+--------------
+
+(1 row)
+
+-- indent with XML declaration
+SELECT xmlserialize(DOCUMENT '<?xml version="1.0" encoding="UTF-8"?><foo><bar><val>73</val></bar></foo>' AS text INDENT);
+ERROR: unsupported XML feature
+LINE 1: SELECT xmlserialize(DOCUMENT '<?xml version="1.0" encoding="...
+ ^
+DETAIL: This functionality requires the server to be built with libxml support.
+SELECT xmlserialize(CONTENT '<?xml version="1.0" encoding="UTF-8"?><foo><bar><val>73</val></bar></foo>' AS text INDENT);
+ERROR: unsupported XML feature
+LINE 1: SELECT xmlserialize(CONTENT '<?xml version="1.0" encoding="...
+ ^
+DETAIL: This functionality requires the server to be built with libxml support.
+-- indent containing DOCTYPE declaration
+SELECT xmlserialize(DOCUMENT '<!DOCTYPE a><a/>' AS text INDENT);
+ERROR: unsupported XML feature
+LINE 1: SELECT xmlserialize(DOCUMENT '<!DOCTYPE a><a/>' AS text INDE...
+ ^
+DETAIL: This functionality requires the server to be built with libxml support.
+SELECT xmlserialize(CONTENT '<!DOCTYPE a><a/>' AS text INDENT);
+ERROR: unsupported XML feature
+LINE 1: SELECT xmlserialize(CONTENT '<!DOCTYPE a><a/>' AS text INDE...
+ ^
+DETAIL: This functionality requires the server to be built with libxml support.
+-- indent xml with empty element
+SELECT xmlserialize(DOCUMENT '<foo><bar></bar></foo>' AS text INDENT);
+ERROR: unsupported XML feature
+LINE 1: SELECT xmlserialize(DOCUMENT '<foo><bar></bar></foo>' AS tex...
+ ^
+DETAIL: This functionality requires the server to be built with libxml support.
+SELECT xmlserialize(CONTENT '<foo><bar></bar></foo>' AS text INDENT);
+ERROR: unsupported XML feature
+LINE 1: SELECT xmlserialize(CONTENT '<foo><bar></bar></foo>' AS tex...
+ ^
+DETAIL: This functionality requires the server to be built with libxml support.
+-- 'no indent' = not using 'no indent'
+SELECT xmlserialize(DOCUMENT '<foo><bar><val x="y">42</val></bar></foo>' AS text) = xmlserialize(DOCUMENT '<foo><bar><val x="y">42</val></bar></foo>' AS text NO INDENT);
+ERROR: unsupported XML feature
+LINE 1: SELECT xmlserialize(DOCUMENT '<foo><bar><val x="y">42</val><...
+ ^
+DETAIL: This functionality requires the server to be built with libxml support.
+SELECT xmlserialize(CONTENT '<foo><bar><val x="y">42</val></bar></foo>' AS text) = xmlserialize(CONTENT '<foo><bar><val x="y">42</val></bar></foo>' AS text NO INDENT);
+ERROR: unsupported XML feature
+LINE 1: SELECT xmlserialize(CONTENT '<foo><bar><val x="y">42</val><...
+ ^
+DETAIL: This functionality requires the server to be built with libxml support.
SELECT xml '<foo>bar</foo>' IS DOCUMENT;
ERROR: unsupported XML feature
LINE 1: SELECT xml '<foo>bar</foo>' IS DOCUMENT;
diff --git a/src/test/regress/expected/xml_2.out b/src/test/regress/expected/xml_2.out
index 4f029d0072..16fc787c18 100644
--- a/src/test/regress/expected/xml_2.out
+++ b/src/test/regress/expected/xml_2.out
@@ -466,6 +466,194 @@ SELECT xmlserialize(content 'good' as char(10));
SELECT xmlserialize(document 'bad' as text);
ERROR: not an XML document
+-- indent
+SELECT xmlserialize(DOCUMENT '<foo><bar><val x="y">42</val></bar></foo>' AS text INDENT);
+ xmlserialize
+-------------------------
+ <foo> +
+ <bar> +
+ <val x="y">42</val>+
+ </bar> +
+ </foo> +
+
+(1 row)
+
+SELECT xmlserialize(CONTENT '<foo><bar><val x="y">42</val></bar></foo>' AS text INDENT);
+ xmlserialize
+-------------------------
+ <foo> +
+ <bar> +
+ <val x="y">42</val>+
+ </bar> +
+ </foo>
+(1 row)
+
+-- no indent
+SELECT xmlserialize(DOCUMENT '<foo><bar><val x="y">42</val></bar></foo>' AS text NO INDENT);
+ xmlserialize
+-------------------------------------------
+ <foo><bar><val x="y">42</val></bar></foo>
+(1 row)
+
+SELECT xmlserialize(CONTENT '<foo><bar><val x="y">42</val></bar></foo>' AS text NO INDENT);
+ xmlserialize
+-------------------------------------------
+ <foo><bar><val x="y">42</val></bar></foo>
+(1 row)
+
+\set VERBOSITY terse
+-- indent non singly-rooted xml
+SELECT xmlserialize(DOCUMENT '<foo>73</foo><bar><val x="y">42</val></bar>' AS text INDENT);
+ERROR: not an XML document
+SELECT xmlserialize(CONTENT '<foo>73</foo><bar><val x="y">42</val></bar>' AS text INDENT);
+ xmlserialize
+-----------------------
+ <foo>73</foo> +
+ <bar> +
+ <val x="y">42</val>+
+ </bar>
+(1 row)
+
+-- indent non singly-rooted xml with mixed contents
+SELECT xmlserialize(DOCUMENT 'text node<foo>73</foo>text node<bar><val x="y">42</val></bar>' AS text INDENT);
+ERROR: not an XML document
+SELECT xmlserialize(CONTENT 'text node<foo>73</foo>text node<bar><val x="y">42</val></bar>' AS text INDENT);
+ xmlserialize
+------------------------
+ text node +
+ <foo>73</foo>text node+
+ <bar> +
+ <val x="y">42</val> +
+ </bar>
+(1 row)
+
+-- indent singly-rooted xml with mixed contents
+SELECT xmlserialize(DOCUMENT '<foo><bar><val x="y">42</val><val x="y">text node<val>73</val></val></bar></foo>' AS text INDENT);
+ xmlserialize
+---------------------------------------------
+ <foo> +
+ <bar> +
+ <val x="y">42</val> +
+ <val x="y">text node<val>73</val></val>+
+ </bar> +
+ </foo> +
+
+(1 row)
+
+SELECT xmlserialize(CONTENT '<foo><bar><val x="y">42</val><val x="y">text node<val>73</val></val></bar></foo>' AS text INDENT);
+ xmlserialize
+---------------------------------------------
+ <foo> +
+ <bar> +
+ <val x="y">42</val> +
+ <val x="y">text node<val>73</val></val>+
+ </bar> +
+ </foo>
+(1 row)
+
+-- indent empty string
+SELECT xmlserialize(DOCUMENT '' AS text INDENT);
+ERROR: not an XML document
+SELECT xmlserialize(CONTENT '' AS text INDENT);
+ xmlserialize
+--------------
+
+(1 row)
+
+-- whitespaces
+SELECT xmlserialize(DOCUMENT ' ' AS text INDENT);
+ERROR: not an XML document
+SELECT xmlserialize(CONTENT ' ' AS text INDENT);
+ xmlserialize
+--------------
+
+(1 row)
+
+\set VERBOSITY default
+-- indent null
+SELECT xmlserialize(DOCUMENT NULL AS text INDENT);
+ xmlserialize
+--------------
+
+(1 row)
+
+SELECT xmlserialize(CONTENT NULL AS text INDENT);
+ xmlserialize
+--------------
+
+(1 row)
+
+-- indent with XML declaration
+SELECT xmlserialize(DOCUMENT '<?xml version="1.0" encoding="UTF-8"?><foo><bar><val>73</val></bar></foo>' AS text INDENT);
+ xmlserialize
+---------------------------------------
+ <?xml version="1.0" encoding="UTF8"?>+
+ <foo> +
+ <bar> +
+ <val>73</val> +
+ </bar> +
+ </foo> +
+
+(1 row)
+
+SELECT xmlserialize(CONTENT '<?xml version="1.0" encoding="UTF-8"?><foo><bar><val>73</val></bar></foo>' AS text INDENT);
+ xmlserialize
+-------------------
+ <foo> +
+ <bar> +
+ <val>73</val>+
+ </bar> +
+ </foo>
+(1 row)
+
+-- indent containing DOCTYPE declaration
+SELECT xmlserialize(DOCUMENT '<!DOCTYPE a><a/>' AS text INDENT);
+ xmlserialize
+--------------
+ <!DOCTYPE a>+
+ <a/> +
+
+(1 row)
+
+SELECT xmlserialize(CONTENT '<!DOCTYPE a><a/>' AS text INDENT);
+ xmlserialize
+--------------
+ <!DOCTYPE a>+
+ <a/> +
+
+(1 row)
+
+-- indent xml with empty element
+SELECT xmlserialize(DOCUMENT '<foo><bar></bar></foo>' AS text INDENT);
+ xmlserialize
+--------------
+ <foo> +
+ <bar/> +
+ </foo> +
+
+(1 row)
+
+SELECT xmlserialize(CONTENT '<foo><bar></bar></foo>' AS text INDENT);
+ xmlserialize
+--------------
+ <foo> +
+ <bar/> +
+ </foo>
+(1 row)
+
+-- 'no indent' = not using 'no indent'
+SELECT xmlserialize(DOCUMENT '<foo><bar><val x="y">42</val></bar></foo>' AS text) = xmlserialize(DOCUMENT '<foo><bar><val x="y">42</val></bar></foo>' AS text NO INDENT);
+ ?column?
+----------
+ t
+(1 row)
+
+SELECT xmlserialize(CONTENT '<foo><bar><val x="y">42</val></bar></foo>' AS text) = xmlserialize(CONTENT '<foo><bar><val x="y">42</val></bar></foo>' AS text NO INDENT);
+ ?column?
+----------
+ t
+(1 row)
+
SELECT xml '<foo>bar</foo>' IS DOCUMENT;
?column?
----------
diff --git a/src/test/regress/sql/xml.sql b/src/test/regress/sql/xml.sql
index 24e40d2653..078e873bb2 100644
--- a/src/test/regress/sql/xml.sql
+++ b/src/test/regress/sql/xml.sql
@@ -132,6 +132,44 @@ SELECT xmlserialize(content data as character varying(20)) FROM xmltest;
SELECT xmlserialize(content 'good' as char(10));
SELECT xmlserialize(document 'bad' as text);
+-- indent
+SELECT xmlserialize(DOCUMENT '<foo><bar><val x="y">42</val></bar></foo>' AS text INDENT);
+SELECT xmlserialize(CONTENT '<foo><bar><val x="y">42</val></bar></foo>' AS text INDENT);
+-- no indent
+SELECT xmlserialize(DOCUMENT '<foo><bar><val x="y">42</val></bar></foo>' AS text NO INDENT);
+SELECT xmlserialize(CONTENT '<foo><bar><val x="y">42</val></bar></foo>' AS text NO INDENT);
+\set VERBOSITY terse
+-- indent non singly-rooted xml
+SELECT xmlserialize(DOCUMENT '<foo>73</foo><bar><val x="y">42</val></bar>' AS text INDENT);
+SELECT xmlserialize(CONTENT '<foo>73</foo><bar><val x="y">42</val></bar>' AS text INDENT);
+-- indent non singly-rooted xml with mixed contents
+SELECT xmlserialize(DOCUMENT 'text node<foo>73</foo>text node<bar><val x="y">42</val></bar>' AS text INDENT);
+SELECT xmlserialize(CONTENT 'text node<foo>73</foo>text node<bar><val x="y">42</val></bar>' AS text INDENT);
+-- indent singly-rooted xml with mixed contents
+SELECT xmlserialize(DOCUMENT '<foo><bar><val x="y">42</val><val x="y">text node<val>73</val></val></bar></foo>' AS text INDENT);
+SELECT xmlserialize(CONTENT '<foo><bar><val x="y">42</val><val x="y">text node<val>73</val></val></bar></foo>' AS text INDENT);
+-- indent empty string
+SELECT xmlserialize(DOCUMENT '' AS text INDENT);
+SELECT xmlserialize(CONTENT '' AS text INDENT);
+-- whitespaces
+SELECT xmlserialize(DOCUMENT ' ' AS text INDENT);
+SELECT xmlserialize(CONTENT ' ' AS text INDENT);
+\set VERBOSITY default
+-- indent null
+SELECT xmlserialize(DOCUMENT NULL AS text INDENT);
+SELECT xmlserialize(CONTENT NULL AS text INDENT);
+-- indent with XML declaration
+SELECT xmlserialize(DOCUMENT '<?xml version="1.0" encoding="UTF-8"?><foo><bar><val>73</val></bar></foo>' AS text INDENT);
+SELECT xmlserialize(CONTENT '<?xml version="1.0" encoding="UTF-8"?><foo><bar><val>73</val></bar></foo>' AS text INDENT);
+-- indent containing DOCTYPE declaration
+SELECT xmlserialize(DOCUMENT '<!DOCTYPE a><a/>' AS text INDENT);
+SELECT xmlserialize(CONTENT '<!DOCTYPE a><a/>' AS text INDENT);
+-- indent xml with empty element
+SELECT xmlserialize(DOCUMENT '<foo><bar></bar></foo>' AS text INDENT);
+SELECT xmlserialize(CONTENT '<foo><bar></bar></foo>' AS text INDENT);
+-- 'no indent' = not using 'no indent'
+SELECT xmlserialize(DOCUMENT '<foo><bar><val x="y">42</val></bar></foo>' AS text) = xmlserialize(DOCUMENT '<foo><bar><val x="y">42</val></bar></foo>' AS text NO INDENT);
+SELECT xmlserialize(CONTENT '<foo><bar><val x="y">42</val></bar></foo>' AS text) = xmlserialize(CONTENT '<foo><bar><val x="y">42</val></bar></foo>' AS text NO INDENT);
SELECT xml '<foo>bar</foo>' IS DOCUMENT;
SELECT xml '<foo>bar</foo><bar>foo</bar>' IS DOCUMENT;
--
2.25.1