diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c index 5e7bd933afc..db1f19ade6e 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_local_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 d228318dc72..030564ae7fd 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 c3a0a354a9c..43f03cd424c 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 ABORT_P ABSENT ABSOLUTE_P ACCESS ACTION ADD_P ADMIN AFTER +%token 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,16 @@ 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; + $$ = (Node *) x; + } | JSON_OBJECT '(' func_arg_list ')' { /* Support for legacy (non-standard) json_object() */ @@ -17841,6 +17851,7 @@ unreserved_keyword: | ABSENT | ABSOLUTE_P | ACCESS + | ACCORDING | ACTION | ADD_P | ADMIN @@ -18168,6 +18179,7 @@ unreserved_keyword: | WRAPPER | WRITE | XML_P + | XMLSCHEMA | YEAR_P | YES_P | ZONE @@ -18247,6 +18259,7 @@ col_name_keyword: | XMLROOT | XMLSERIALIZE | XMLTABLE + | XMLVALIDATE ; /* Type/function identifier --- keywords that can be type or function names. @@ -18386,6 +18399,7 @@ bare_label_keyword: | ABSENT | ABSOLUTE_P | ACCESS + | ACCORDING | ACTION | ADD_P | ADMIN @@ -18835,8 +18849,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 4524e49c326..bb4db6f303d 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 6cf90be40bb..1be814299bd 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 41e775570ec..f76ade2d83d 100644 --- a/src/backend/utils/adt/xml.c +++ b/src/backend/utils/adt/xml.c @@ -58,6 +58,7 @@ #include #include #include +#include /* * We used to check for xmlStructuredErrorContext via a configure test; but @@ -1158,10 +1159,115 @@ xmlvalidate(PG_FUNCTION_ARGS) { ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("xmlvalidate is not implemented"))); + errmsg("xmlvalidate against DTD is not implemented"))); 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 that looks for local + * XML schemas, therefore no arbitrary reads are made over external files. + */ +bool +xmlvalidate_local_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..05cad75e38d 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_local_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..3b29b964036 100644 --- a/src/test/regress/expected/xml.out +++ b/src/test/regress/expected/xml.out @@ -1881,3 +1881,214 @@ SELECT xmltext('x'|| '

73

'::xml || .42 || true || 'j'::char); x<P>73</P>0.42truej (1 row) +SELECT xmlvalidate(DOCUMENT 'John30' + ACCORDING TO XMLSCHEMA ' + + + + + + + + + +'); + xmlvalidate +------------- + t +(1 row) + +SELECT xmlvalidate(DOCUMENT 'John' + ACCORDING TO XMLSCHEMA ' + + + + + + + + + +'); + xmlvalidate +------------- + f +(1 row) + +SELECT xmlvalidate(DOCUMENT 'Johnnot-a-number' + ACCORDING TO XMLSCHEMA ' + + + + + + + + + +'); + xmlvalidate +------------- + f +(1 row) + +SELECT xmlvalidate(CONTENT 'PostgreSQL Internals' + ACCORDING TO XMLSCHEMA ' + + + + + + + + +'); + xmlvalidate +------------- + t +(1 row) + +SELECT xmlvalidate(DOCUMENT NULL + ACCORDING TO XMLSCHEMA ' + + +'); + xmlvalidate +------------- + +(1 row) + +SELECT xmlvalidate(DOCUMENT 'value' + ACCORDING TO XMLSCHEMA NULL); + xmlvalidate +------------- + +(1 row) + +SELECT xmlvalidate(DOCUMENT NULL ACCORDING TO XMLSCHEMA NULL); + xmlvalidate +------------- + +(1 row) + +SELECT xmlvalidate(DOCUMENT 'Widget9.99' + ACCORDING TO XMLSCHEMA ' + + + + + + + + + + +'); + xmlvalidate +------------- + t +(1 row) + +SELECT xmlvalidate(DOCUMENT 'Widget9.99' + ACCORDING TO XMLSCHEMA ' + + + + + + + + + + +'); + xmlvalidate +------------- + f +(1 row) + +SELECT xmlvalidate(DOCUMENT + ' + + Alice + Developer + 75000 + + ' + ACCORDING TO XMLSCHEMA ' + + + + + + + + + + + + + + + + +'); + xmlvalidate +------------- + t +(1 row) + +CREATE TABLE xml_validation_test ( + xml_data xml, + xsd_schema text +); +CREATE TABLE + +INSERT INTO xml_validation_test VALUES + ('42', + ' + + +'), + ('not-a-number', + ' + + +'), + ('Hello World', + ' + + +'); +INSERT 0 3 + +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; +DROP TABLE + +SELECT xmlvalidate(DOCUMENT 'value' + ACCORDING TO XMLSCHEMA ''); +ERROR: failed to parse XML schema +DETAIL: line 1: Premature end of data in tag this-is-not-valid-xsd line 1 + + ^ + +SELECT xmlvalidate(DOCUMENT '' + ACCORDING TO XMLSCHEMA ' + + +'); +ERROR: invalid XML content +LINE 1: SELECT xmlvalidate(DOCUMENT '' + ^ +DETAIL: line 1: Premature end of data in tag unclosed-tag line 1 + + ^ \ No newline at end of file diff --git a/src/test/regress/sql/xml.sql b/src/test/regress/sql/xml.sql index 0ea4f508837..27a4e642b24 100644 --- a/src/test/regress/sql/xml.sql +++ b/src/test/regress/sql/xml.sql @@ -679,3 +679,156 @@ SELECT xmltext(' '); SELECT xmltext('foo `$_-+?=*^%!|/\()[]{}'); SELECT xmltext('foo & <"bar">'); SELECT xmltext('x'|| '

73

'::xml || .42 || true || 'j'::char); + +SELECT xmlvalidate(DOCUMENT 'John30' + ACCORDING TO XMLSCHEMA ' + + + + + + + + + +'); + +SELECT xmlvalidate(DOCUMENT 'John' + ACCORDING TO XMLSCHEMA ' + + + + + + + + + +'); + +SELECT xmlvalidate(DOCUMENT 'Johnnot-a-number' + ACCORDING TO XMLSCHEMA ' + + + + + + + + + +'); + +SELECT xmlvalidate(CONTENT 'PostgreSQL Internals' + ACCORDING TO XMLSCHEMA ' + + + + + + + + +'); + +SELECT xmlvalidate(DOCUMENT NULL + ACCORDING TO XMLSCHEMA ' + + +'); + +SELECT xmlvalidate(DOCUMENT 'value' + ACCORDING TO XMLSCHEMA NULL); + +SELECT xmlvalidate(DOCUMENT NULL ACCORDING TO XMLSCHEMA NULL); + +SELECT xmlvalidate(DOCUMENT 'Widget9.99' + ACCORDING TO XMLSCHEMA ' + + + + + + + + + + +'); + +SELECT xmlvalidate(DOCUMENT 'Widget9.99' + ACCORDING TO XMLSCHEMA ' + + + + + + + + + + +'); + +SELECT xmlvalidate(DOCUMENT + ' + + Alice + Developer + 75000 + + ' + ACCORDING TO XMLSCHEMA ' + + + + + + + + + + + + + + + + +'); + +CREATE TABLE xml_validation_test ( + xml_data xml, + xsd_schema text +); + +INSERT INTO xml_validation_test VALUES + ('42', + ' + + +'), + ('not-a-number', + ' + + +'), + ('Hello World', + ' + + +'); + +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 'value' + ACCORDING TO XMLSCHEMA ''); + +SELECT xmlvalidate(DOCUMENT '' + ACCORDING TO XMLSCHEMA ' + + +');