From 733b6f8af5b3f07a2c55145c9a797955566bb5e3 Mon Sep 17 00:00:00 2001
From: Markus Winand <markus.winand@winand.at>
Date: Sun, 8 Jul 2018 17:24:05 +0000
Subject: [PATCH 1/2] XML/XPath: comments, processing-instructions, array
 indexes

For text output, use correct data for comments and processing-instructions
and return only the selected node if an array index is used (e.g. node()[2]).
Elements now return the concatenation of all descendant text() nodes, not
just the immediate children of the element.

For XML output, serialize comments, processing-instructions and CDATA
nodes as XML.
---
 src/backend/utils/adt/xml.c         | 122 +++++++++++++++++++++++++-----------
 src/test/regress/expected/xml_2.out |  10 +--
 src/test/regress/sql/xml.sql        |   4 +-
 3 files changed, 92 insertions(+), 44 deletions(-)

diff --git a/src/backend/utils/adt/xml.c b/src/backend/utils/adt/xml.c
index 37d85f7..7cef817 100644
--- a/src/backend/utils/adt/xml.c
+++ b/src/backend/utils/adt/xml.c
@@ -143,6 +143,7 @@ static bool print_xml_decl(StringInfo buf, const xmlChar *version,
 static xmlDocPtr xml_parse(text *data, XmlOptionType xmloption_arg,
 		  bool preserve_whitespace, int encoding);
 static text *xml_xmlnodetoxmltype(xmlNodePtr cur, PgXmlErrorContext *xmlerrcxt);
+static xmlChar *xml_xmlnodetostring(xmlDocPtr doc, const xmlNode const* element);
 static int xml_xpathobjtoxmlarray(xmlXPathObjectPtr xpathobj,
 					   ArrayBuildState *astate,
 					   PgXmlErrorContext *xmlerrcxt);
@@ -3682,7 +3683,7 @@ xml_xmlnodetoxmltype(xmlNodePtr cur, PgXmlErrorContext *xmlerrcxt)
 {
 	xmltype    *result;
 
-	if (cur->type == XML_ELEMENT_NODE)
+	if (cur->type == XML_ELEMENT_NODE || cur->type == XML_COMMENT_NODE || cur->type == XML_CDATA_SECTION_NODE || cur->type == XML_PI_NODE)
 	{
 		xmlBufferPtr buf;
 		xmlNodePtr	cur_copy;
@@ -3741,6 +3742,52 @@ xml_xmlnodetoxmltype(xmlNodePtr cur, PgXmlErrorContext *xmlerrcxt)
 }
 
 /*
+ * Convert XML node to a concatenation of nested text() nodes.
+ * Inline CDATA, expand entity references.
+ *
+ * Returns NULL or a string that must be xmlFree()-ed by the caller.
+ */
+static xmlChar *
+xml_xmlnodetostring (xmlDocPtr doc, const xmlNode *const node) {
+	const xmlNode *n = node->xmlChildrenNode;
+	const xmlNode *deep;
+	const xmlEntity *ent;
+	xmlChar *ret = NULL;
+
+	while (n != NULL) {
+		if (n->type == XML_ELEMENT_NODE ||
+			n->type == XML_ENTITY_REF_NODE ||
+			n->type == XML_TEXT_NODE ||
+			n->type == XML_CDATA_SECTION_NODE)
+		{
+			deep = NULL;
+			if (n->type == XML_ELEMENT_NODE) {
+				deep = n;
+			} else if (n->type == XML_ENTITY_REF_NODE) {
+				ent = xmlGetDocEntity(doc, n->name);
+				if (ent != NULL) {
+					deep = ent->children;
+				}
+			}
+
+			if (deep == NULL) {
+				ret = xmlStrcat(ret, n->content);
+			} else {
+				xmlChar *buffer;
+				buffer = xml_xmlnodetostring(doc, deep);
+				if (buffer != NULL) {
+					ret = xmlStrcat(ret, buffer);
+					xmlFree(buffer);
+				}
+			}
+		}
+
+		n = n->next;
+	}
+	return ret;
+}
+
+/*
  * Convert an XML XPath object (the result of evaluating an XPath expression)
  * to an array of xml values, which are appended to astate.  The function
  * result value is the number of elements in the array.
@@ -4475,9 +4522,9 @@ XmlTableGetValue(TableFuncScanState *state, int colnum,
 		/*
 		 * There are four possible cases, depending on the number of nodes
 		 * returned by the XPath expression and the type of the target column:
-		 * a) XPath returns no nodes.  b) One node is returned, and column is
-		 * of type XML.  c) One node, column type other than XML.  d) Multiple
-		 * nodes are returned.
+		 * a) XPath returns no nodes.  b) The target type is XML (return all
+		 * as XML). For non-XML types: c) One node (return content).
+		 * d) Multiple nodes (error).
 		 */
 		if (xpathobj->type == XPATH_NODESET)
 		{
@@ -4490,33 +4537,49 @@ XmlTableGetValue(TableFuncScanState *state, int colnum,
 			{
 				*isnull = true;
 			}
-			else if (count == 1 && typid == XMLOID)
+			else if (typid == XMLOID)
 			{
-				text	   *textstr;
+				StringInfoData str;
+				int			i;
 
-				/* simple case, result is one value */
-				textstr = xml_xmlnodetoxmltype(xpathobj->nodesetval->nodeTab[0],
-											   xtCxt->xmlerrcxt);
-				cstr = text_to_cstring(textstr);
+				/* Concatenate serialized values */
+				initStringInfo(&str);
+				for (i = 0; i < count; i++)
+				{
+					appendStringInfoText(&str,
+										 xml_xmlnodetoxmltype(xpathobj->nodesetval->nodeTab[i],
+															  xtCxt->xmlerrcxt));
+				}
+				cstr = str.data;
 			}
 			else if (count == 1)
 			{
 				xmlChar    *str;
 				xmlNodePtr	node;
 
-				/*
-				 * Most nodes (elements and even attributes) store their data
-				 * in children nodes. If they don't have children nodes, it
-				 * means that they are empty (e.g. <element/>). Text nodes and
-				 * CDATA sections are an exception: they don't have children
-				 * but have content in the Text/CDATA node itself.
-				 */
 				node = xpathobj->nodesetval->nodeTab[0];
-				if (node->type != XML_CDATA_SECTION_NODE &&
-					node->type != XML_TEXT_NODE)
-					node = node->xmlChildrenNode;
+				if (node->type == XML_ELEMENT_NODE)
+				{
+					/* XML elements need recursive processing */
+					str = xml_xmlnodetostring(xtCxt->doc, node);
+				}
+				else  if (node->type == XML_CDATA_SECTION_NODE ||
+					node->type == XML_TEXT_NODE ||
+					node->type == XML_PI_NODE ||
+					node->type == XML_COMMENT_NODE)
+				{
+					/* Node types that store their data in "content" */
+					str = xmlStrdup(node->content);
+				}
+				else
+				{
+					/*
+					 * The remaining types (e.g. attributes)
+					 * store their data in the child.
+					 */
+					str = xmlStrdup(node->xmlChildrenNode->content);
+				}
 
-				str = xmlNodeListGetString(xtCxt->doc, node, 1);
 				if (str != NULL)
 				{
 					PG_TRY();
@@ -4539,30 +4602,15 @@ XmlTableGetValue(TableFuncScanState *state, int colnum,
 			}
 			else
 			{
-				StringInfoData str;
-				int			i;
-
-				Assert(count > 1);
-
 				/*
 				 * When evaluating the XPath expression returns multiple
-				 * nodes, the result is the concatenation of them all. The
-				 * target type must be XML.
+				 * nodes, and the tartget type is not XML, yield an error.
 				 */
 				if (typid != XMLOID)
 					ereport(ERROR,
 							(errcode(ERRCODE_CARDINALITY_VIOLATION),
 							 errmsg("more than one value returned by column XPath expression")));
 
-				/* Concatenate serialized values */
-				initStringInfo(&str);
-				for (i = 0; i < count; i++)
-				{
-					appendStringInfoText(&str,
-										 xml_xmlnodetoxmltype(xpathobj->nodesetval->nodeTab[i],
-															  xtCxt->xmlerrcxt));
-				}
-				cstr = str.data;
 			}
 		}
 		else if (xpathobj->type == XPATH_STRING)
diff --git a/src/test/regress/expected/xml_2.out b/src/test/regress/expected/xml_2.out
index 3ec56e4..ca1911f 100644
--- a/src/test/regress/expected/xml_2.out
+++ b/src/test/regress/expected/xml_2.out
@@ -1189,13 +1189,13 @@ SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan"
   5 | Japan        |         3 | <COUNTRY_ID>JP</COUNTRY_ID><COUNTRY_NAME>Japan</COUNTRY_NAME><REGION_ID>3</REGION_ID><PREMIER_NAME>Sinzo Abe</PREMIER_NAME>
 (2 rows)
 
-SELECT * FROM xmltable('/root' passing '<root><element>a1a<!-- aaaa -->a2a<?aaaaa?> <!--z-->  bbbb<x>xxx</x>cccc</element></root>' COLUMNS element text);
-      element      
--------------------
- a1aa2a   bbbbcccc
+SELECT * FROM xmltable('/root' passing '<root><element>a1a<!-- aaaa -->a2a<?aaaaa pi?> <!--z-->  bbbb<x>xxx</x>cccc</element></root>' COLUMNS element text, cmnt text PATH 'element/comment()[2]', pi text PATH 'element/processing-instruction()', t1 text PATH 'element/text()[1]', x xml PATH 'element');
+       element        | cmnt | pi | t1  |                                        x                                        
+----------------------+------+----+-----+---------------------------------------------------------------------------------
+ a1aa2a   bbbbxxxcccc | z    | pi | a1a | <element>a1a<!-- aaaa -->a2a<?aaaaa pi?> <!--z-->  bbbb<x>xxx</x>cccc</element>
 (1 row)
 
-SELECT * FROM xmltable('/root' passing '<root><element>a1a<!-- aaaa -->a2a<?aaaaa?> <!--z-->  bbbb<x>xxx</x>cccc</element></root>' COLUMNS element text PATH 'element/text()'); -- should fail
+SELECT * FROM xmltable('/root' passing '<root><element>a1a<!-- aaaa -->a2a<?aaaaa pi?> <!--z-->  bbbb<x>xxx</x>cccc</element></root>' COLUMNS element text PATH 'element/text()'); -- should fail
 ERROR:  more than one value returned by column XPath expression
 -- CDATA test
 select * from xmltable('d/r' passing '<d><r><c><![CDATA[<hello> &"<>!<a>foo</a>]]></c></r><r><c>2</c></r></d>' columns c text);
diff --git a/src/test/regress/sql/xml.sql b/src/test/regress/sql/xml.sql
index 3b91b56..e274e47 100644
--- a/src/test/regress/sql/xml.sql
+++ b/src/test/regress/sql/xml.sql
@@ -420,8 +420,8 @@ SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan"
 SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS id int PATH '@id', "COUNTRY_NAME" text, "REGION_ID" int, rawdata xml PATH '.');
 SELECT xmltable.* FROM xmldata, LATERAL xmltable('/ROWS/ROW[COUNTRY_NAME="Japan" or COUNTRY_NAME="India"]' PASSING data COLUMNS id int PATH '@id', "COUNTRY_NAME" text, "REGION_ID" int, rawdata xml PATH './*');
 
-SELECT * FROM xmltable('/root' passing '<root><element>a1a<!-- aaaa -->a2a<?aaaaa?> <!--z-->  bbbb<x>xxx</x>cccc</element></root>' COLUMNS element text);
-SELECT * FROM xmltable('/root' passing '<root><element>a1a<!-- aaaa -->a2a<?aaaaa?> <!--z-->  bbbb<x>xxx</x>cccc</element></root>' COLUMNS element text PATH 'element/text()'); -- should fail
+SELECT * FROM xmltable('/root' passing '<root><element>a1a<!-- aaaa -->a2a<?aaaaa pi?> <!--z-->  bbbb<x>xxx</x>cccc</element></root>' COLUMNS element text, cmnt text PATH 'element/comment()[2]', pi text PATH 'element/processing-instruction()', t1 text PATH 'element/text()[1]', x xml PATH 'element');
+SELECT * FROM xmltable('/root' passing '<root><element>a1a<!-- aaaa -->a2a<?aaaaa pi?> <!--z-->  bbbb<x>xxx</x>cccc</element></root>' COLUMNS element text PATH 'element/text()'); -- should fail
 
 -- CDATA test
 select * from xmltable('d/r' passing '<d><r><c><![CDATA[<hello> &"<>!<a>foo</a>]]></c></r><r><c>2</c></r></d>' columns c text);
-- 
1.9.1

