0001-full-xmlvalidate-text-schema-implementation.patch
application/octet-stream
Filename: 0001-full-xmlvalidate-text-schema-implementation.patch
Type: application/octet-stream
Part: 0
Patch
Same data as JSON:
GET /api/v1/attachments/:id/patch
the parsed metadata as JSON — format, series position, per-file stats; never the diff bytes.
API reference →
Format: format-patch
Series: patch 0001
Subject: full xmlvalidate text schema implementation
| File | + | − |
|---|---|---|
| doc/src/sgml/func/func-xml.sgml | 119 | 0 |
| src/backend/executor/execExprInterp.c | 24 | 0 |
| src/backend/nodes/nodeFuncs.c | 2 | 0 |
| src/backend/parser/gram.y | 19 | 2 |
| src/backend/parser/parse_expr.c | 8 | 0 |
| src/backend/parser/parse_target.c | 3 | 0 |
| src/backend/utils/adt/ruleutils.c | 15 | 1 |
| src/backend/utils/adt/xml.c | 108 | 1 |
| src/include/nodes/primnodes.h | 1 | 0 |
| src/include/parser/kwlist.h | 3 | 0 |
| src/include/utils/xml.h | 1 | 0 |
| src/test/regress/expected/xml.out | 280 | 0 |
| src/test/regress/sql/xml.sql | 216 | 0 |
From fb56c90da3cab9d85e2f6262fdaf1f16d5778d28 Mon Sep 17 00:00:00 2001
From: Marcos Magueta <maguetamarcos@gmail.com>
Date: Thu, 18 Dec 2025 23:17:26 -0300
Subject: [PATCH 1/1] full xmlvalidate text schema implementation
---
doc/src/sgml/func/func-xml.sgml | 119 +++++++++++
src/backend/executor/execExprInterp.c | 24 +++
src/backend/nodes/nodeFuncs.c | 2 +
src/backend/parser/gram.y | 21 +-
src/backend/parser/parse_expr.c | 8 +
src/backend/parser/parse_target.c | 3 +
src/backend/utils/adt/ruleutils.c | 16 +-
src/backend/utils/adt/xml.c | 108 +++++++++-
src/include/nodes/primnodes.h | 1 +
src/include/parser/kwlist.h | 3 +
src/include/utils/xml.h | 1 +
src/test/regress/expected/xml.out | 280 ++++++++++++++++++++++++++
src/test/regress/sql/xml.sql | 216 ++++++++++++++++++++
13 files changed, 798 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/func/func-xml.sgml b/doc/src/sgml/func/func-xml.sgml
index 511bc90852a..b02db6c9b5b 100644
--- a/doc/src/sgml/func/func-xml.sgml
+++ b/doc/src/sgml/func/func-xml.sgml
@@ -1010,6 +1010,125 @@ SELECT xmltable.*
]]></screen>
</para>
</sect3>
+
+ <sect3 id="functions-xml-processing-xmlvalidate">
+ <title><literal>xmlvalidate</literal></title>
+
+ <indexterm>
+ <primary>xmlvalidate</primary>
+ </indexterm>
+
+<synopsis>
+<function>XMLVALIDATE</function> ( {<literal>DOCUMENT</literal>|<literal>CONTENT</literal>} <replaceable>xml_value</replaceable> <literal>ACCORDING TO XMLSCHEMA</literal> <replaceable>schema_text</replaceable> ) <returnvalue>boolean</returnvalue>
+</synopsis>
+
+ <para>
+ The <function>xmlvalidate</function> function validates an XML value
+ against an XML Schema (XSD). It returns <literal>true</literal> if the
+ XML is valid according to the schema, <literal>false</literal> if it is
+ invalid, or <literal>NULL</literal> if either argument is
+ <literal>NULL</literal>.
+ </para>
+
+ <para>
+ The first argument specifies whether to validate the XML as a
+ <literal>DOCUMENT</literal> (a complete XML document with a single root
+ element) or as <literal>CONTENT</literal> (an XML content fragment).
+ </para>
+
+ <para>
+ The <replaceable>schema_text</replaceable> argument should be a
+ <type>text</type> value containing a valid XML Schema Definition (XSD).
+ For security reasons, the schema is treated as plain text and parsed
+ in-memory only. This prevents malicious imports or external file access
+ through schema location references (such as <literal>xs:import</literal>,
+ <literal>xs:include</literal>, or <literal>schemaLocation</literal>
+ attributes). Any attempts to reference external resources will be
+ ignored, resulting in an empty schema reference.
+ </para>
+
+ <para>
+ Examples:
+<screen><![CDATA[
+SELECT xmlvalidate(DOCUMENT '<person><name>John</name><age>30</age></person>'
+ ACCORDING TO XMLSCHEMA '<?xml version="1.0"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+ <xs:element name="person">
+ <xs:complexType>
+ <xs:sequence>
+ <xs:element name="name" type="xs:string"/>
+ <xs:element name="age" type="xs:integer"/>
+ </xs:sequence>
+ </xs:complexType>
+ </xs:element>
+</xs:schema>');
+
+ xmlvalidate
+-------------
+ t
+(1 row)
+]]></screen>
+ </para>
+
+ <para>
+ This example shows a validation failure due to a missing required element:
+<screen><![CDATA[
+SELECT xmlvalidate(DOCUMENT '<person><name>John</name></person>'
+ ACCORDING TO XMLSCHEMA '<?xml version="1.0"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+ <xs:element name="person">
+ <xs:complexType>
+ <xs:sequence>
+ <xs:element name="name" type="xs:string"/>
+ <xs:element name="age" type="xs:integer"/>
+ </xs:sequence>
+ </xs:complexType>
+ </xs:element>
+</xs:schema>');
+
+ xmlvalidate
+-------------
+ f
+(1 row)
+]]></screen>
+ </para>
+
+ <para>
+ The schema can also validate attributes:
+<screen><![CDATA[
+SELECT xmlvalidate(DOCUMENT '<product id="123"><name>Widget</name><price>9.99</price></product>'
+ ACCORDING TO XMLSCHEMA '<?xml version="1.0"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+ <xs:element name="product">
+ <xs:complexType>
+ <xs:sequence>
+ <xs:element name="name" type="xs:string"/>
+ <xs:element name="price" type="xs:decimal"/>
+ </xs:sequence>
+ <xs:attribute name="id" type="xs:string" use="required"/>
+ </xs:complexType>
+ </xs:element>
+</xs:schema>');
+
+ xmlvalidate
+-------------
+ t
+(1 row)
+]]></screen>
+ </para>
+
+ <note>
+ <para>
+ For security, <function>xmlvalidate</function> processes the XML Schema
+ as an in-memory text value. External resource references such as
+ <literal>schemaLocation</literal> in <literal>xs:import</literal> or
+ <literal>xs:include</literal> directives are not followed and are
+ treated as empty schema references. This design prevents potential
+ security vulnerabilities where malicious schemas could attempt to
+ access external files or network resources.
+ </para>
+ </note>
+ </sect3>
</sect2>
<sect2 id="functions-xml-mapping">
diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
index 5e7bd933afc..e1aed4f7ff4 100644
--- a/src/backend/executor/execExprInterp.c
+++ b/src/backend/executor/execExprInterp.c
@@ -4626,6 +4626,30 @@ ExecEvalXmlExpr(ExprState *state, ExprEvalStep *op)
}
break;
+ case IS_XMLVALIDATE:
+ {
+ Datum *argvalue = op->d.xmlexpr.argvalue;
+ bool *argnull = op->d.xmlexpr.argnull;
+ xmltype *data;
+ text *schema;
+
+ /* arguments are known to be xml, text */
+ Assert(list_length(xexpr->args) == 2);
+
+ if (argnull[0] || argnull[1])
+ {
+ *op->resnull = true;
+ return;
+ }
+
+ data = DatumGetXmlP(argvalue[0]);
+ schema = DatumGetTextPP(argvalue[1]);
+
+ *op->resvalue = BoolGetDatum(xmlvalidate_text_schema(data, schema));
+ *op->resnull = false;
+ }
+ break;
+
case IS_DOCUMENT:
{
Datum *argvalue = op->d.xmlexpr.argvalue;
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index 024a2b2fd84..acb67f38b82 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -218,6 +218,8 @@ exprType(const Node *expr)
case T_XmlExpr:
if (((const XmlExpr *) expr)->op == IS_DOCUMENT)
type = BOOLOID;
+ else if (((const XmlExpr *) expr)->op == IS_XMLVALIDATE)
+ type = BOOLOID;
else if (((const XmlExpr *) expr)->op == IS_XMLSERIALIZE)
type = TEXTOID;
else
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 7856ce9d78f..d1e878a96ed 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -704,7 +704,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
*/
/* ordinary key words in alphabetical order */
-%token <keyword> ABORT_P ABSENT ABSOLUTE_P ACCESS ACTION ADD_P ADMIN AFTER
+%token <keyword> ABORT_P ABSENT ABSOLUTE_P ACCESS ACCORDING ACTION ADD_P ADMIN AFTER
AGGREGATE ALL ALSO ALTER ALWAYS ANALYSE ANALYZE AND ANY ARRAY AS ASC
ASENSITIVE ASSERTION ASSIGNMENT ASYMMETRIC ATOMIC AT ATTACH ATTRIBUTE AUTHORIZATION
@@ -795,7 +795,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
WAIT WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE
XML_P XMLATTRIBUTES XMLCONCAT XMLELEMENT XMLEXISTS XMLFOREST XMLNAMESPACES
- XMLPARSE XMLPI XMLROOT XMLSERIALIZE XMLTABLE
+ XMLPARSE XMLPI XMLROOT XMLSCHEMA XMLSERIALIZE XMLTABLE XMLVALIDATE
YEAR_P YES_P
@@ -16231,6 +16231,17 @@ func_expr_common_subexpr:
n->location = @1;
$$ = (Node *) n;
}
+ | XMLVALIDATE '(' document_or_content a_expr ACCORDING TO XMLSCHEMA a_expr ')'
+ {
+ XmlExpr *x = (XmlExpr *)
+ makeXmlExpr(IS_XMLVALIDATE, NULL, NIL,
+ list_make2($4, $8),
+ @1);
+
+ x->xmloption = $3;
+ x->location = @1;
+ $$ = (Node *) x;
+ }
| JSON_OBJECT '(' func_arg_list ')'
{
/* Support for legacy (non-standard) json_object() */
@@ -17841,6 +17852,7 @@ unreserved_keyword:
| ABSENT
| ABSOLUTE_P
| ACCESS
+ | ACCORDING
| ACTION
| ADD_P
| ADMIN
@@ -18168,6 +18180,7 @@ unreserved_keyword:
| WRAPPER
| WRITE
| XML_P
+ | XMLSCHEMA
| YEAR_P
| YES_P
| ZONE
@@ -18247,6 +18260,7 @@ col_name_keyword:
| XMLROOT
| XMLSERIALIZE
| XMLTABLE
+ | XMLVALIDATE
;
/* Type/function identifier --- keywords that can be type or function names.
@@ -18386,6 +18400,7 @@ bare_label_keyword:
| ABSENT
| ABSOLUTE_P
| ACCESS
+ | ACCORDING
| ACTION
| ADD_P
| ADMIN
@@ -18835,8 +18850,10 @@ bare_label_keyword:
| XMLPARSE
| XMLPI
| XMLROOT
+ | XMLSCHEMA
| XMLSERIALIZE
| XMLTABLE
+ | XMLVALIDATE
| YES_P
| ZONE
;
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 6b8fa15fca3..97c84acc17c 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -2471,6 +2471,14 @@ transformXmlExpr(ParseState *pstate, XmlExpr *x)
/* not handled here */
Assert(false);
break;
+ case IS_XMLVALIDATE:
+ if (i == 0)
+ newe = coerce_to_specific_type(pstate, newe, XMLOID,
+ "XMLVALIDATE");
+ else
+ newe = coerce_to_specific_type(pstate, newe, TEXTOID,
+ "XMLVALIDATE");
+ break;
case IS_DOCUMENT:
newe = coerce_to_specific_type(pstate, newe, XMLOID,
"IS DOCUMENT");
diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c
index 905c975d83b..931df1e58b9 100644
--- a/src/backend/parser/parse_target.c
+++ b/src/backend/parser/parse_target.c
@@ -1976,6 +1976,9 @@ FigureColnameInternal(Node *node, char **name)
case IS_XMLSERIALIZE:
*name = "xmlserialize";
return 2;
+ case IS_XMLVALIDATE:
+ *name = "xmlvalidate";
+ return 2;
case IS_DOCUMENT:
/* nothing */
break;
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 9f85eb86da1..3c41ceae610 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -10119,10 +10119,13 @@ get_rule_expr(Node *node, deparse_context *context,
case IS_XMLSERIALIZE:
appendStringInfoString(buf, "XMLSERIALIZE(");
break;
+ case IS_XMLVALIDATE:
+ appendStringInfoString(buf, "XMLVALIDATE(");
+ break;
case IS_DOCUMENT:
break;
}
- if (xexpr->op == IS_XMLPARSE || xexpr->op == IS_XMLSERIALIZE)
+ if (xexpr->op == IS_XMLPARSE || xexpr->op == IS_XMLSERIALIZE || xexpr->op == IS_XMLVALIDATE)
{
if (xexpr->xmloption == XMLOPTION_DOCUMENT)
appendStringInfoString(buf, "DOCUMENT ");
@@ -10226,6 +10229,17 @@ get_rule_expr(Node *node, deparse_context *context,
}
}
break;
+ case IS_XMLVALIDATE:
+ Assert(list_length(xexpr->args) == 2);
+
+ get_rule_expr((Node *) linitial(xexpr->args),
+ context, true);
+
+ appendStringInfoString(buf, " ACCORDING TO XMLSCHEMA ");
+
+ get_rule_expr((Node *) lsecond(xexpr->args),
+ context, true);
+ break;
case IS_DOCUMENT:
get_rule_expr_paren((Node *) xexpr->args, context, false, node);
break;
diff --git a/src/backend/utils/adt/xml.c b/src/backend/utils/adt/xml.c
index c8ab9d61c68..7293c31dc97 100644
--- a/src/backend/utils/adt/xml.c
+++ b/src/backend/utils/adt/xml.c
@@ -58,6 +58,7 @@
#include <libxml/xmlwriter.h>
#include <libxml/xpath.h>
#include <libxml/xpathInternals.h>
+#include <libxml/xmlschemas.h>
/*
* We used to check for xmlStructuredErrorContext via a configure test; but
@@ -1158,10 +1159,116 @@ xmlvalidate(PG_FUNCTION_ARGS)
{
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("xmlvalidate is not implemented")));
+ errmsg("xmlvalidate is not implemented against generalized schema definitions")));
return 0;
}
+/*
+ * xmlvalidate - validate XML against an XML provided Schema (XSD)
+ *
+ * Returns true if the XML is valid according to the xml schema,
+ * false if it doesn't and NULL if any (xml schema or xml) are NULL.
+ * This implements the SQL:2008 XMLVALIDATE function with the limitation of
+ * not looking at import references. This function relies on the protections
+ * enforced by pg_xml_init.
+ */
+bool
+xmlvalidate_text_schema(xmltype *data, text *schema)
+{
+#ifdef USE_LIBXML
+ xmlDocPtr doc = NULL;
+ xmlSchemaParserCtxtPtr schema_parser_ctxt = NULL;
+ xmlSchemaPtr schema_ptr = NULL;
+ xmlSchemaValidCtxtPtr valid_ctxt = NULL;
+ char *datastr;
+ char *schemastr;
+ int result;
+ PgXmlErrorContext *xmlerrcxt;
+
+ datastr = text_to_cstring((text *) data);
+ schemastr = text_to_cstring(schema);
+ xmlerrcxt = pg_xml_init(PG_XML_STRICTNESS_WELLFORMED);
+
+ PG_TRY();
+ {
+ doc = xmlReadMemory(datastr, strlen(datastr), NULL, NULL, 0);
+ if (doc == NULL)
+ {
+ xml_ereport(xmlerrcxt, ERROR, ERRCODE_INVALID_XML_DOCUMENT,
+ "invalid XML document");
+ }
+
+ schema_parser_ctxt = xmlSchemaNewMemParserCtxt(schemastr, strlen(schemastr));
+ if (schema_parser_ctxt == NULL)
+ {
+ xml_ereport(xmlerrcxt, ERROR, ERRCODE_INVALID_XML_DOCUMENT,
+ "failed to create schema parser context");
+ }
+
+ schema_ptr = xmlSchemaParse(schema_parser_ctxt);
+ if (schema_ptr == NULL)
+ {
+ xml_ereport(xmlerrcxt, ERROR, ERRCODE_INVALID_XML_DOCUMENT,
+ "failed to parse XML schema");
+ }
+
+ valid_ctxt = xmlSchemaNewValidCtxt(schema_ptr);
+ if (valid_ctxt == NULL)
+ {
+ xml_ereport(xmlerrcxt, ERROR, ERRCODE_OUT_OF_MEMORY,
+ "failed to create schema validation context");
+ }
+
+ /* Validate the document - returns 0 if valid,
+ greater than 0 if invalid and < 0 if error */
+ result = xmlSchemaValidateDoc(valid_ctxt, doc);
+ if (result < 0)
+ {
+ xml_ereport(xmlerrcxt, ERROR, ERRCODE_INTERNAL_ERROR,
+ "internal error during schema validation");
+ } else if (result == 0) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+ PG_CATCH();
+ {
+ if (valid_ctxt)
+ xmlSchemaFreeValidCtxt(valid_ctxt);
+ if (schema_ptr)
+ xmlSchemaFree(schema_ptr);
+ if (schema_parser_ctxt)
+ xmlSchemaFreeParserCtxt(schema_parser_ctxt);
+ if (doc)
+ xmlFreeDoc(doc);
+ pg_xml_done(xmlerrcxt, true);
+ pfree(datastr);
+ pfree(schemastr);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ if (valid_ctxt)
+ xmlSchemaFreeValidCtxt(valid_ctxt);
+ if (schema_ptr)
+ xmlSchemaFree(schema_ptr);
+ if (schema_parser_ctxt)
+ xmlSchemaFreeParserCtxt(schema_parser_ctxt);
+ if (doc)
+ xmlFreeDoc(doc);
+
+ pg_xml_done(xmlerrcxt, false);
+
+ pfree(datastr);
+ pfree(schemastr);
+ // Default case since nothing got returned
+ // out of the normal path for validation calls to libxml
+ return false;
+#else
+ NO_XML_SUPPORT();
+ return NULL;
+#endif
+}
bool
xml_is_document(xmltype *arg)
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index 1b4436f2ff6..5365a0012c1 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -1610,6 +1610,7 @@ typedef enum XmlExprOp
IS_XMLROOT, /* XMLROOT(xml, version, standalone) */
IS_XMLSERIALIZE, /* XMLSERIALIZE(is_document, xmlval, indent) */
IS_DOCUMENT, /* xmlval IS DOCUMENT */
+ IS_XMLVALIDATE, /* XMLVALIDATE(xmlval, schema_text) */
} XmlExprOp;
typedef enum XmlOptionType
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 5d4fe27ef96..be7e8bab3f6 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -29,6 +29,7 @@ PG_KEYWORD("abort", ABORT_P, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("absent", ABSENT, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("absolute", ABSOLUTE_P, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("access", ACCESS, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("according", ACCORDING, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("action", ACTION, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("add", ADD_P, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("admin", ADMIN, UNRESERVED_KEYWORD, BARE_LABEL)
@@ -518,8 +519,10 @@ PG_KEYWORD("xmlnamespaces", XMLNAMESPACES, COL_NAME_KEYWORD, BARE_LABEL)
PG_KEYWORD("xmlparse", XMLPARSE, COL_NAME_KEYWORD, BARE_LABEL)
PG_KEYWORD("xmlpi", XMLPI, COL_NAME_KEYWORD, BARE_LABEL)
PG_KEYWORD("xmlroot", XMLROOT, COL_NAME_KEYWORD, BARE_LABEL)
+PG_KEYWORD("xmlschema", XMLSCHEMA, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("xmlserialize", XMLSERIALIZE, COL_NAME_KEYWORD, BARE_LABEL)
PG_KEYWORD("xmltable", XMLTABLE, COL_NAME_KEYWORD, BARE_LABEL)
+PG_KEYWORD("xmlvalidate", XMLVALIDATE, COL_NAME_KEYWORD, BARE_LABEL)
PG_KEYWORD("year", YEAR_P, UNRESERVED_KEYWORD, AS_LABEL)
PG_KEYWORD("yes", YES_P, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("zone", ZONE, UNRESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/include/utils/xml.h b/src/include/utils/xml.h
index 732dac47bc4..6a64fd092a0 100644
--- a/src/include/utils/xml.h
+++ b/src/include/utils/xml.h
@@ -76,6 +76,7 @@ extern xmltype *xmlelement(XmlExpr *xexpr,
extern xmltype *xmlparse(text *data, XmlOptionType xmloption_arg, bool preserve_whitespace);
extern xmltype *xmlpi(const char *target, text *arg, bool arg_is_null, bool *result_is_null);
extern xmltype *xmlroot(xmltype *data, text *version, int standalone);
+extern bool xmlvalidate_text_schema(xmltype *data, text *schema);
extern bool xml_is_document(xmltype *arg);
extern text *xmltotext_with_options(xmltype *data, XmlOptionType xmloption_arg,
bool indent);
diff --git a/src/test/regress/expected/xml.out b/src/test/regress/expected/xml.out
index 103a22a3b1d..7ea802412a5 100644
--- a/src/test/regress/expected/xml.out
+++ b/src/test/regress/expected/xml.out
@@ -1881,3 +1881,283 @@ SELECT xmltext('x'|| '<P>73</P>'::xml || .42 || true || 'j'::char);
x<P>73</P>0.42truej
(1 row)
+SELECT xmlvalidate(DOCUMENT '<person><name>John</name><age>30</age></person>'
+ ACCORDING TO XMLSCHEMA '<?xml version="1.0"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+ <xs:element name="person">
+ <xs:complexType>
+ <xs:sequence>
+ <xs:element name="name" type="xs:string"/>
+ <xs:element name="age" type="xs:integer"/>
+ </xs:sequence>
+ </xs:complexType>
+ </xs:element>
+</xs:schema>');
+ xmlvalidate
+-------------
+ t
+(1 row)
+
+SELECT xmlvalidate(DOCUMENT '<person><name>John</name></person>'
+ ACCORDING TO XMLSCHEMA '<?xml version="1.0"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+ <xs:element name="person">
+ <xs:complexType>
+ <xs:sequence>
+ <xs:element name="name" type="xs:string"/>
+ <xs:element name="age" type="xs:integer"/>
+ </xs:sequence>
+ </xs:complexType>
+ </xs:element>
+</xs:schema>');
+ xmlvalidate
+-------------
+ f
+(1 row)
+
+SELECT xmlvalidate(DOCUMENT '<person><name>John</name><age>not-a-number</age></person>'
+ ACCORDING TO XMLSCHEMA '<?xml version="1.0"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+ <xs:element name="person">
+ <xs:complexType>
+ <xs:sequence>
+ <xs:element name="name" type="xs:string"/>
+ <xs:element name="age" type="xs:integer"/>
+ </xs:sequence>
+ </xs:complexType>
+ </xs:element>
+</xs:schema>');
+ xmlvalidate
+-------------
+ f
+(1 row)
+
+SELECT xmlvalidate(CONTENT '<book><title>PostgreSQL Internals</title></book>'
+ ACCORDING TO XMLSCHEMA '<?xml version="1.0"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+ <xs:element name="book">
+ <xs:complexType>
+ <xs:sequence>
+ <xs:element name="title" type="xs:string"/>
+ </xs:sequence>
+ </xs:complexType>
+ </xs:element>
+</xs:schema>');
+ xmlvalidate
+-------------
+ t
+(1 row)
+
+SELECT xmlvalidate(DOCUMENT NULL
+ ACCORDING TO XMLSCHEMA '<?xml version="1.0"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+ <xs:element name="test" type="xs:string"/>
+</xs:schema>');
+ xmlvalidate
+-------------
+
+(1 row)
+
+SELECT xmlvalidate(DOCUMENT '<test>value</test>'
+ ACCORDING TO XMLSCHEMA NULL);
+ xmlvalidate
+-------------
+
+(1 row)
+
+SELECT xmlvalidate(DOCUMENT NULL ACCORDING TO XMLSCHEMA NULL);
+ xmlvalidate
+-------------
+
+(1 row)
+
+SELECT xmlvalidate(DOCUMENT '<product id="123"><name>Widget</name><price>9.99</price></product>'
+ ACCORDING TO XMLSCHEMA '<?xml version="1.0"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+ <xs:element name="product">
+ <xs:complexType>
+ <xs:sequence>
+ <xs:element name="name" type="xs:string"/>
+ <xs:element name="price" type="xs:decimal"/>
+ </xs:sequence>
+ <xs:attribute name="id" type="xs:string" use="required"/>
+ </xs:complexType>
+ </xs:element>
+</xs:schema>');
+ xmlvalidate
+-------------
+ t
+(1 row)
+
+SELECT xmlvalidate(DOCUMENT '<product><name>Widget</name><price>9.99</price></product>'
+ ACCORDING TO XMLSCHEMA '<?xml version="1.0"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+ <xs:element name="product">
+ <xs:complexType>
+ <xs:sequence>
+ <xs:element name="name" type="xs:string"/>
+ <xs:element name="price" type="xs:decimal"/>
+ </xs:sequence>
+ <xs:attribute name="id" type="xs:string" use="required"/>
+ </xs:complexType>
+ </xs:element>
+</xs:schema>');
+ xmlvalidate
+-------------
+ f
+(1 row)
+
+SELECT xmlvalidate(DOCUMENT
+ '<company>
+ <employee>
+ <name>Alice</name>
+ <position>Developer</position>
+ <salary>75000</salary>
+ </employee>
+ </company>'
+ ACCORDING TO XMLSCHEMA '<?xml version="1.0"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+ <xs:element name="company">
+ <xs:complexType>
+ <xs:sequence>
+ <xs:element name="employee" maxOccurs="unbounded">
+ <xs:complexType>
+ <xs:sequence>
+ <xs:element name="name" type="xs:string"/>
+ <xs:element name="position" type="xs:string"/>
+ <xs:element name="salary" type="xs:decimal"/>
+ </xs:sequence>
+ </xs:complexType>
+ </xs:element>
+ </xs:sequence>
+ </xs:complexType>
+ </xs:element>
+</xs:schema>');
+ xmlvalidate
+-------------
+ t
+(1 row)
+
+CREATE TABLE xml_validation_test (
+ xml_data xml,
+ xsd_schema text
+);
+INSERT INTO xml_validation_test VALUES
+ ('<number>42</number>',
+ '<?xml version="1.0"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+ <xs:element name="number" type="xs:integer"/>
+</xs:schema>'),
+ ('<number>not-a-number</number>',
+ '<?xml version="1.0"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+ <xs:element name="number" type="xs:integer"/>
+</xs:schema>'),
+ ('<text>Hello World</text>',
+ '<?xml version="1.0"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+ <xs:element name="text" type="xs:string"/>
+</xs:schema>');
+SELECT xmlvalidate(DOCUMENT xml_data ACCORDING TO XMLSCHEMA xsd_schema) AS is_valid
+FROM xml_validation_test;
+ is_valid
+----------
+ t
+ f
+ t
+(3 rows)
+
+DROP TABLE xml_validation_test;
+SELECT xmlvalidate(DOCUMENT '<test>value</test>'
+ ACCORDING TO XMLSCHEMA '<this-is-not-valid-xsd>');
+ERROR: failed to parse XML schema
+DETAIL: line 1: Premature end of data in tag this-is-not-valid-xsd line 1
+<this-is-not-valid-xsd>
+ ^
+SELECT xmlvalidate(DOCUMENT '<unclosed-tag>'
+ ACCORDING TO XMLSCHEMA '<?xml version="1.0"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+ <xs:element name="test" type="xs:string"/>
+</xs:schema>');
+ERROR: invalid XML content
+LINE 1: SELECT xmlvalidate(DOCUMENT '<unclosed-tag>'
+ ^
+DETAIL: line 1: Premature end of data in tag unclosed-tag line 1
+<unclosed-tag>
+ ^
+-- xs:import with external URL should be blocked
+SELECT xmlvalidate(
+ DOCUMENT '<test>value</test>'
+ ACCORDING TO XMLSCHEMA '<?xml version="1.0"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+ <xs:import namespace="http://example.com/external" schemaLocation="http://example.com/malicious.xsd"/>
+ <xs:element name="test" type="xs:string"/>
+</xs:schema>');
+ERROR: failed to parse XML schema
+DETAIL: line 1: Document is empty
+
+^
+-- xs:include with external URL should be blocked
+SELECT xmlvalidate(
+ DOCUMENT '<test>value</test>'
+ ACCORDING TO XMLSCHEMA '<?xml version="1.0"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+ <xs:include schemaLocation="http://example.com/external-schema.xsd"/>
+ <xs:element name="test" type="xs:string"/>
+</xs:schema>');
+ERROR: failed to parse XML schema
+DETAIL: line 1: Document is empty
+
+^
+-- file:// URL should be blocked
+SELECT xmlvalidate(
+ DOCUMENT '<test>value</test>'
+ ACCORDING TO XMLSCHEMA '<?xml version="1.0"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+ <xs:include schemaLocation="file:///etc/passwd"/>
+ <xs:element name="test" type="xs:string"/>
+</xs:schema>');
+ERROR: failed to parse XML schema
+DETAIL: line 1: Document is empty
+
+^
+-- Test a_expr with subqueries
+CREATE TEMP TABLE xml_docs (id smallint, doc xml);
+CREATE TEMP TABLE xsd_schemas (id smallint, schema text);
+INSERT INTO xml_docs VALUES
+ (1, '<product><name>Widget</name><price>19.99</price></product>'),
+ (2, '<product><name>Gadget</name><price>not-a-number</price></product>');
+INSERT INTO xsd_schemas VALUES
+ (1, '<?xml version="1.0"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+ <xs:element name="product">
+ <xs:complexType>
+ <xs:sequence>
+ <xs:element name="name" type="xs:string"/>
+ <xs:element name="price" type="xs:decimal"/>
+ </xs:sequence>
+ </xs:complexType>
+ </xs:element>
+</xs:schema>');
+-- subquery for document argument
+SELECT xmlvalidate(
+ DOCUMENT (SELECT doc FROM xml_docs WHERE id = 1)
+ ACCORDING TO XMLSCHEMA (SELECT schema FROM xsd_schemas WHERE id = 1)
+) AS subquery_test_valid;
+ subquery_test_valid
+---------------------
+ t
+(1 row)
+
+-- subquery for an invalid document
+SELECT xmlvalidate(
+ DOCUMENT (SELECT doc FROM xml_docs WHERE id = 2)
+ ACCORDING TO XMLSCHEMA (SELECT schema FROM xsd_schemas WHERE id = 1)
+) AS subquery_test_invalid;
+ subquery_test_invalid
+-----------------------
+ f
+(1 row)
+
+DROP TABLE xml_docs;
+DROP TABLE xsd_schemas;
diff --git a/src/test/regress/sql/xml.sql b/src/test/regress/sql/xml.sql
index 0ea4f508837..c8ea2994eb2 100644
--- a/src/test/regress/sql/xml.sql
+++ b/src/test/regress/sql/xml.sql
@@ -679,3 +679,219 @@ SELECT xmltext(' ');
SELECT xmltext('foo `$_-+?=*^%!|/\()[]{}');
SELECT xmltext('foo & <"bar">');
SELECT xmltext('x'|| '<P>73</P>'::xml || .42 || true || 'j'::char);
+
+SELECT xmlvalidate(DOCUMENT '<person><name>John</name><age>30</age></person>'
+ ACCORDING TO XMLSCHEMA '<?xml version="1.0"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+ <xs:element name="person">
+ <xs:complexType>
+ <xs:sequence>
+ <xs:element name="name" type="xs:string"/>
+ <xs:element name="age" type="xs:integer"/>
+ </xs:sequence>
+ </xs:complexType>
+ </xs:element>
+</xs:schema>');
+
+SELECT xmlvalidate(DOCUMENT '<person><name>John</name></person>'
+ ACCORDING TO XMLSCHEMA '<?xml version="1.0"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+ <xs:element name="person">
+ <xs:complexType>
+ <xs:sequence>
+ <xs:element name="name" type="xs:string"/>
+ <xs:element name="age" type="xs:integer"/>
+ </xs:sequence>
+ </xs:complexType>
+ </xs:element>
+</xs:schema>');
+
+SELECT xmlvalidate(DOCUMENT '<person><name>John</name><age>not-a-number</age></person>'
+ ACCORDING TO XMLSCHEMA '<?xml version="1.0"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+ <xs:element name="person">
+ <xs:complexType>
+ <xs:sequence>
+ <xs:element name="name" type="xs:string"/>
+ <xs:element name="age" type="xs:integer"/>
+ </xs:sequence>
+ </xs:complexType>
+ </xs:element>
+</xs:schema>');
+
+SELECT xmlvalidate(CONTENT '<book><title>PostgreSQL Internals</title></book>'
+ ACCORDING TO XMLSCHEMA '<?xml version="1.0"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+ <xs:element name="book">
+ <xs:complexType>
+ <xs:sequence>
+ <xs:element name="title" type="xs:string"/>
+ </xs:sequence>
+ </xs:complexType>
+ </xs:element>
+</xs:schema>');
+
+SELECT xmlvalidate(DOCUMENT NULL
+ ACCORDING TO XMLSCHEMA '<?xml version="1.0"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+ <xs:element name="test" type="xs:string"/>
+</xs:schema>');
+
+SELECT xmlvalidate(DOCUMENT '<test>value</test>'
+ ACCORDING TO XMLSCHEMA NULL);
+
+SELECT xmlvalidate(DOCUMENT NULL ACCORDING TO XMLSCHEMA NULL);
+
+SELECT xmlvalidate(DOCUMENT '<product id="123"><name>Widget</name><price>9.99</price></product>'
+ ACCORDING TO XMLSCHEMA '<?xml version="1.0"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+ <xs:element name="product">
+ <xs:complexType>
+ <xs:sequence>
+ <xs:element name="name" type="xs:string"/>
+ <xs:element name="price" type="xs:decimal"/>
+ </xs:sequence>
+ <xs:attribute name="id" type="xs:string" use="required"/>
+ </xs:complexType>
+ </xs:element>
+</xs:schema>');
+
+SELECT xmlvalidate(DOCUMENT '<product><name>Widget</name><price>9.99</price></product>'
+ ACCORDING TO XMLSCHEMA '<?xml version="1.0"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+ <xs:element name="product">
+ <xs:complexType>
+ <xs:sequence>
+ <xs:element name="name" type="xs:string"/>
+ <xs:element name="price" type="xs:decimal"/>
+ </xs:sequence>
+ <xs:attribute name="id" type="xs:string" use="required"/>
+ </xs:complexType>
+ </xs:element>
+</xs:schema>');
+
+SELECT xmlvalidate(DOCUMENT
+ '<company>
+ <employee>
+ <name>Alice</name>
+ <position>Developer</position>
+ <salary>75000</salary>
+ </employee>
+ </company>'
+ ACCORDING TO XMLSCHEMA '<?xml version="1.0"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+ <xs:element name="company">
+ <xs:complexType>
+ <xs:sequence>
+ <xs:element name="employee" maxOccurs="unbounded">
+ <xs:complexType>
+ <xs:sequence>
+ <xs:element name="name" type="xs:string"/>
+ <xs:element name="position" type="xs:string"/>
+ <xs:element name="salary" type="xs:decimal"/>
+ </xs:sequence>
+ </xs:complexType>
+ </xs:element>
+ </xs:sequence>
+ </xs:complexType>
+ </xs:element>
+</xs:schema>');
+
+CREATE TABLE xml_validation_test (
+ xml_data xml,
+ xsd_schema text
+);
+
+INSERT INTO xml_validation_test VALUES
+ ('<number>42</number>',
+ '<?xml version="1.0"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+ <xs:element name="number" type="xs:integer"/>
+</xs:schema>'),
+ ('<number>not-a-number</number>',
+ '<?xml version="1.0"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+ <xs:element name="number" type="xs:integer"/>
+</xs:schema>'),
+ ('<text>Hello World</text>',
+ '<?xml version="1.0"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+ <xs:element name="text" type="xs:string"/>
+</xs:schema>');
+
+SELECT xmlvalidate(DOCUMENT xml_data ACCORDING TO XMLSCHEMA xsd_schema) AS is_valid
+FROM xml_validation_test;
+
+DROP TABLE xml_validation_test;
+
+SELECT xmlvalidate(DOCUMENT '<test>value</test>'
+ ACCORDING TO XMLSCHEMA '<this-is-not-valid-xsd>');
+
+SELECT xmlvalidate(DOCUMENT '<unclosed-tag>'
+ ACCORDING TO XMLSCHEMA '<?xml version="1.0"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+ <xs:element name="test" type="xs:string"/>
+</xs:schema>');
+
+-- xs:import with external URL should be blocked
+SELECT xmlvalidate(
+ DOCUMENT '<test>value</test>'
+ ACCORDING TO XMLSCHEMA '<?xml version="1.0"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+ <xs:import namespace="http://example.com/external" schemaLocation="http://example.com/malicious.xsd"/>
+ <xs:element name="test" type="xs:string"/>
+</xs:schema>');
+
+-- xs:include with external URL should be blocked
+SELECT xmlvalidate(
+ DOCUMENT '<test>value</test>'
+ ACCORDING TO XMLSCHEMA '<?xml version="1.0"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+ <xs:include schemaLocation="http://example.com/external-schema.xsd"/>
+ <xs:element name="test" type="xs:string"/>
+</xs:schema>');
+
+-- file:// URL should be blocked
+SELECT xmlvalidate(
+ DOCUMENT '<test>value</test>'
+ ACCORDING TO XMLSCHEMA '<?xml version="1.0"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+ <xs:include schemaLocation="file:///etc/passwd"/>
+ <xs:element name="test" type="xs:string"/>
+</xs:schema>');
+
+-- Test a_expr with subqueries
+CREATE TEMP TABLE xml_docs (id smallint, doc xml);
+CREATE TEMP TABLE xsd_schemas (id smallint, schema text);
+
+INSERT INTO xml_docs VALUES
+ (1, '<product><name>Widget</name><price>19.99</price></product>'),
+ (2, '<product><name>Gadget</name><price>not-a-number</price></product>');
+
+INSERT INTO xsd_schemas VALUES
+ (1, '<?xml version="1.0"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+ <xs:element name="product">
+ <xs:complexType>
+ <xs:sequence>
+ <xs:element name="name" type="xs:string"/>
+ <xs:element name="price" type="xs:decimal"/>
+ </xs:sequence>
+ </xs:complexType>
+ </xs:element>
+</xs:schema>');
+
+-- subquery for document argument
+SELECT xmlvalidate(
+ DOCUMENT (SELECT doc FROM xml_docs WHERE id = 1)
+ ACCORDING TO XMLSCHEMA (SELECT schema FROM xsd_schemas WHERE id = 1)
+) AS subquery_test_valid;
+
+-- subquery for an invalid document
+SELECT xmlvalidate(
+ DOCUMENT (SELECT doc FROM xml_docs WHERE id = 2)
+ ACCORDING TO XMLSCHEMA (SELECT schema FROM xsd_schemas WHERE id = 1)
+) AS subquery_test_invalid;
+
+DROP TABLE xml_docs;
+DROP TABLE xsd_schemas;
--
2.51.2