From dd0110ed51276c8432b76b71008a7183ae5cb6da Mon Sep 17 00:00:00 2001
From: Marcos Magueta <maguetamarcos@gmail.com>
Date: Tue, 13 Jan 2026 22:08:04 -0300
Subject: [PATCH] xmlschema catalog and xmlvalidate

---
 doc/src/sgml/func/func-xml.sgml        | 119 ++++++++++
 src/backend/catalog/Makefile           |   1 +
 src/backend/catalog/aclchk.c           |  17 ++
 src/backend/catalog/dependency.c       |  31 +++
 src/backend/catalog/meson.build        |   1 +
 src/backend/catalog/namespace.c        |  56 +++++
 src/backend/catalog/objectaddress.c    |  84 +++++++
 src/backend/catalog/pg_xmlschema.c     | 191 ++++++++++++++++
 src/backend/commands/Makefile          |   3 +-
 src/backend/commands/alter.c           |   8 +
 src/backend/commands/dropcmds.c        |   7 +
 src/backend/commands/event_trigger.c   |   2 +
 src/backend/commands/meson.build       |   1 +
 src/backend/commands/seclabel.c        |   1 +
 src/backend/commands/xmlschemacmds.c   | 119 ++++++++++
 src/backend/executor/execExprInterp.c  |  27 +++
 src/backend/parser/gram.y              |  83 ++++++-
 src/backend/parser/parse_expr.c        |  42 ++++
 src/backend/parser/parse_target.c      |   3 +
 src/backend/tcop/utility.c             |  17 ++
 src/backend/utils/adt/acl.c            |   4 +
 src/backend/utils/adt/ruleutils.c      |  16 +-
 src/backend/utils/adt/xml.c            | 141 +++++++++++-
 src/include/catalog/Makefile           |   1 +
 src/include/catalog/meson.build        |   1 +
 src/include/catalog/namespace.h        |   1 +
 src/include/catalog/pg_xmlschema.h     |  43 ++++
 src/include/commands/xmlschemacmds.h   |  10 +
 src/include/nodes/parsenodes.h         |   1 +
 src/include/nodes/primnodes.h          |   1 +
 src/include/parser/kwlist.h            |   3 +
 src/include/tcop/cmdtaglist.h          |   3 +
 src/include/utils/acl.h                |   1 +
 src/include/utils/xml.h                |   1 +
 src/test/regress/expected/oidjoins.out |   2 +
 src/test/regress/expected/xml.out      | 297 +++++++++++++++++++++++++
 src/test/regress/expected/xml_2.out    | 297 +++++++++++++++++++++++++
 src/test/regress/sql/xml.sql           | 274 +++++++++++++++++++++++
 38 files changed, 1902 insertions(+), 8 deletions(-)
 create mode 100644 src/backend/catalog/pg_xmlschema.c
 create mode 100644 src/backend/commands/xmlschemacmds.c
 create mode 100644 src/include/catalog/pg_xmlschema.h
 create mode 100644 src/include/commands/xmlschemacmds.h

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/catalog/Makefile b/src/backend/catalog/Makefile
index 26fa0c9b18c..ed9414ba638 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -46,6 +46,7 @@ OBJS = \
 	pg_subscription.o \
 	pg_tablespace.o \
 	pg_type.o \
+	pg_xmlschema.o \
 	storage.o \
 	toasting.o
 
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index a431fc0926f..c5198ea6ec6 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -64,6 +64,7 @@
 #include "catalog/pg_proc.h"
 #include "catalog/pg_tablespace.h"
 #include "catalog/pg_type.h"
+#include "catalog/pg_xmlschema.h"
 #include "commands/defrem.h"
 #include "commands/event_trigger.h"
 #include "commands/extension.h"
@@ -290,6 +291,9 @@ restrict_and_check_grant(bool is_grant, AclMode avail_goptions, bool all_privs,
 		case OBJECT_PARAMETER_ACL:
 			whole_mask = ACL_ALL_RIGHTS_PARAMETER_ACL;
 			break;
+		case OBJECT_XMLSCHEMA:
+			whole_mask = ACL_ALL_RIGHTS_XMLSCHEMA;
+			break;
 		default:
 			elog(ERROR, "unrecognized object type: %d", objtype);
 			/* not reached, but keep compiler quiet */
@@ -534,6 +538,10 @@ ExecuteGrantStmt(GrantStmt *stmt)
 			all_privileges = ACL_ALL_RIGHTS_PARAMETER_ACL;
 			errormsg = gettext_noop("invalid privilege type %s for parameter");
 			break;
+		case OBJECT_XMLSCHEMA:
+			all_privileges = ACL_ALL_RIGHTS_XMLSCHEMA;
+			errormsg = gettext_noop("invalid privilege type %s for XML schema");
+			break;
 		default:
 			elog(ERROR, "unrecognized GrantStmt.objtype: %d",
 				 (int) stmt->objtype);
@@ -639,6 +647,9 @@ ExecGrantStmt_oids(InternalGrant *istmt)
 		case OBJECT_PARAMETER_ACL:
 			ExecGrant_Parameter(istmt);
 			break;
+		case OBJECT_XMLSCHEMA:
+			ExecGrant_common(istmt, XmlSchemaRelationId, ACL_ALL_RIGHTS_XMLSCHEMA, NULL);
+			break;
 		default:
 			elog(ERROR, "unrecognized GrantStmt.objtype: %d",
 				 (int) istmt->objtype);
@@ -2677,6 +2688,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_CONVERSION:
 						msg = gettext_noop("permission denied for conversion %s");
 						break;
+					case OBJECT_XMLSCHEMA:
+						msg = gettext_noop("permission denied for XML schema %s");
+						break;
 					case OBJECT_DATABASE:
 						msg = gettext_noop("permission denied for database %s");
 						break;
@@ -2809,6 +2823,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_CONVERSION:
 						msg = gettext_noop("must be owner of conversion %s");
 						break;
+					case OBJECT_XMLSCHEMA:
+						msg = gettext_noop("must be owner of XML schema %s");
+						break;
 					case OBJECT_DATABASE:
 						msg = gettext_noop("must be owner of database %s");
 						break;
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index f89267f0342..50ce3871e78 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -66,6 +66,7 @@
 #include "catalog/pg_ts_template.h"
 #include "catalog/pg_type.h"
 #include "catalog/pg_user_mapping.h"
+#include "catalog/pg_xmlschema.h"
 #include "commands/comment.h"
 #include "commands/defrem.h"
 #include "commands/event_trigger.h"
@@ -79,6 +80,7 @@
 #include "funcapi.h"
 #include "miscadmin.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/primnodes.h"
 #include "parser/parsetree.h"
 #include "rewrite/rewriteRemove.h"
 #include "storage/lmgr.h"
@@ -1514,6 +1516,7 @@ doDeletion(const ObjectAddress *object, int flags)
 		case EventTriggerRelationId:
 		case TransformRelationId:
 		case AuthMemRelationId:
+		case XmlSchemaRelationId:
 			DropObjectById(object);
 			break;
 
@@ -2416,6 +2419,34 @@ find_expr_references_walker(Node *node,
 						   context->addrs);
 		/* fall through to examine arguments */
 	}
+	else if (IsA(node, XmlExpr))
+	{
+		XmlExpr    *xmlexpr = (XmlExpr *) node;
+
+		/*
+		 * XMLVALIDATE's second argument is a Const containing the schema OID.
+		 * Record a dependency on it.
+		 */
+		if (xmlexpr->op == IS_XMLVALIDATE && list_length(xmlexpr->args) == 2)
+		{
+			Node	   *schema_arg = (Node *) lsecond(xmlexpr->args);
+
+			if (IsA(schema_arg, Const))
+			{
+				Const	   *schema_const = (Const *) schema_arg;
+				Oid			schema_oid;
+
+				if (!schema_const->constisnull &&
+					schema_const->consttype == OIDOID)
+				{
+					schema_oid = DatumGetObjectId(schema_const->constvalue);
+					add_object_address(XmlSchemaRelationId, schema_oid, 0,
+									   context->addrs);
+				}
+			}
+		}
+		/* fall through to examine arguments */
+	}
 
 	return expression_tree_walker(node, find_expr_references_walker,
 								  context);
diff --git a/src/backend/catalog/meson.build b/src/backend/catalog/meson.build
index 11d21c5ad6b..9ba15e71b72 100644
--- a/src/backend/catalog/meson.build
+++ b/src/backend/catalog/meson.build
@@ -33,6 +33,7 @@ backend_sources += files(
   'pg_subscription.c',
   'pg_tablespace.c',
   'pg_type.c',
+  'pg_xmlschema.c',
   'storage.c',
   'toasting.c',
 )
diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c
index c3b79a2ba48..89e87a06572 100644
--- a/src/backend/catalog/namespace.c
+++ b/src/backend/catalog/namespace.c
@@ -30,6 +30,7 @@
 #include "catalog/pg_collation.h"
 #include "catalog/pg_conversion.h"
 #include "catalog/pg_database.h"
+#include "catalog/pg_xmlschema.h"
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
 #include "catalog/pg_operator.h"
@@ -4143,6 +4144,61 @@ get_conversion_oid(List *conname, bool missing_ok)
 	return conoid;
 }
 
+/*
+ * get_xmlschema_oid - find an XML schema by possibly qualified name
+ */
+Oid
+get_xmlschema_oid(List *schemaname, bool missing_ok)
+{
+	char	   *nspname;
+	char	   *schema_name;
+	Oid			namespaceId;
+	Oid			schema_oid = InvalidOid;
+	ListCell   *l;
+
+	/* deconstruct the name list */
+	DeconstructQualifiedName(schemaname, &nspname, &schema_name);
+
+	if (nspname)
+	{
+		/* use exact schema given */
+		namespaceId = LookupExplicitNamespace(nspname, missing_ok);
+		if (missing_ok && !OidIsValid(namespaceId))
+			schema_oid = InvalidOid;
+		else
+			schema_oid = GetSysCacheOid2(XMLSCHEMANAMENSP, Anum_pg_xmlschema_oid,
+										 PointerGetDatum(schema_name),
+										 ObjectIdGetDatum(namespaceId));
+	}
+	else
+	{
+		/* search for it in search path */
+		recomputeNamespacePath();
+
+		foreach(l, activeSearchPath)
+		{
+			namespaceId = lfirst_oid(l);
+
+			if (namespaceId == myTempNamespace)
+				continue; /* do not look in temp namespace */
+
+			schema_oid = GetSysCacheOid2(XMLSCHEMANAMENSP, Anum_pg_xmlschema_oid,
+										 PointerGetDatum(schema_name),
+										 ObjectIdGetDatum(namespaceId));
+			if (OidIsValid(schema_oid))
+				return schema_oid;
+		}
+	}
+
+	/* Not found in path */
+	if (!OidIsValid(schema_oid) && !missing_ok)
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("XML schema \"%s\" does not exist",
+						NameListToString(schemaname))));
+	return schema_oid;
+}
+
 /*
  * FindDefaultConversionProc - find default encoding conversion proc
  */
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 02af64b82c6..6306b9c8f6e 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -62,6 +62,7 @@
 #include "catalog/pg_ts_template.h"
 #include "catalog/pg_type.h"
 #include "catalog/pg_user_mapping.h"
+#include "catalog/pg_xmlschema.h"
 #include "commands/defrem.h"
 #include "commands/event_trigger.h"
 #include "commands/extension.h"
@@ -187,6 +188,20 @@ static const ObjectPropertyType ObjectProperty[] =
 		OBJECT_COLLATION,
 		true
 	},
+	{
+		"xmlschema",
+		XmlSchemaRelationId,
+		XmlSchemaOidIndexId,
+		XMLSCHEMAOID,
+		XMLSCHEMANAMENSP,
+		Anum_pg_xmlschema_oid,
+		Anum_pg_xmlschema_schemaname,
+		Anum_pg_xmlschema_schemanamespace,
+		Anum_pg_xmlschema_schemaowner,
+		Anum_pg_xmlschema_schemaacl,
+		OBJECT_XMLSCHEMA,
+		true
+	},
 	{
 		"constraint",
 		ConstraintRelationId,
@@ -720,6 +735,9 @@ static const struct object_type_map
 	{
 		"collation", OBJECT_COLLATION
 	},
+	{
+		"xmlschema", OBJECT_XMLSCHEMA
+	},
 	{
 		"table constraint", OBJECT_TABCONSTRAINT
 	},
@@ -1029,6 +1047,11 @@ get_object_address(ObjectType objtype, Node *object,
 				address.objectId = get_collation_oid(castNode(List, object), missing_ok);
 				address.objectSubId = 0;
 				break;
+			case OBJECT_XMLSCHEMA:
+				address.classId = XmlSchemaRelationId;
+				address.objectId = get_xmlschema_oid(castNode(List, object), missing_ok);
+				address.objectSubId = 0;
+				break;
 			case OBJECT_CONVERSION:
 				address.classId = ConversionRelationId;
 				address.objectId = get_conversion_oid(castNode(List, object), missing_ok);
@@ -2282,6 +2305,7 @@ pg_get_object_address(PG_FUNCTION_ARGS)
 		case OBJECT_COLUMN:
 		case OBJECT_ATTRIBUTE:
 		case OBJECT_COLLATION:
+		case OBJECT_XMLSCHEMA:
 		case OBJECT_CONVERSION:
 		case OBJECT_STATISTIC_EXT:
 		case OBJECT_TSPARSER:
@@ -2460,6 +2484,7 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
 							   strVal(object));
 			break;
 		case OBJECT_COLLATION:
+		case OBJECT_XMLSCHEMA:
 		case OBJECT_CONVERSION:
 		case OBJECT_OPCLASS:
 		case OBJECT_OPFAMILY:
@@ -4067,6 +4092,34 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok)
 				break;
 			}
 
+		case XmlSchemaRelationId:
+			{
+				HeapTuple	schemaTup;
+				Form_pg_xmlschema schema;
+				char	   *nspname;
+
+				schemaTup = SearchSysCache1(XMLSCHEMAOID,
+											ObjectIdGetDatum(object->objectId));
+				if (!HeapTupleIsValid(schemaTup))
+				{
+					if (!missing_ok)
+						elog(ERROR, "cache lookup failed for XML schema %u",
+							 object->objectId);
+					break;
+				}
+
+				schema = (Form_pg_xmlschema) GETSTRUCT(schemaTup);
+
+				/* Qualify the name if not visible in search path */
+				nspname = get_namespace_name(schema->schemanamespace);
+
+				appendStringInfo(&buffer, _("XML schema %s"),
+								 quote_qualified_identifier(nspname,
+															NameStr(schema->schemaname)));
+				ReleaseSysCache(schemaTup);
+				break;
+			}
+
 		default:
 			elog(ERROR, "unsupported object class: %u", object->classId);
 	}
@@ -4669,6 +4722,10 @@ getObjectTypeDescription(const ObjectAddress *object, bool missing_ok)
 			appendStringInfoString(&buffer, "transform");
 			break;
 
+		case XmlSchemaRelationId:
+			appendStringInfoString(&buffer, "XML schema");
+			break;
+
 		default:
 			elog(ERROR, "unsupported object class: %u", object->classId);
 	}
@@ -6019,6 +6076,33 @@ getObjectIdentityParts(const ObjectAddress *object,
 			}
 			break;
 
+		case XmlSchemaRelationId:
+			{
+				HeapTuple	schemaTup;
+				Form_pg_xmlschema schema;
+				char	   *nspname;
+
+				schemaTup = SearchSysCache1(XMLSCHEMAOID,
+											ObjectIdGetDatum(object->objectId));
+				if (!HeapTupleIsValid(schemaTup))
+				{
+					if (!missing_ok)
+						elog(ERROR, "cache lookup failed for XML schema %u",
+							 object->objectId);
+					break;
+				}
+				schema = (Form_pg_xmlschema) GETSTRUCT(schemaTup);
+				nspname = get_namespace_name_or_temp(schema->schemanamespace);
+				appendStringInfoString(&buffer,
+									   quote_qualified_identifier(nspname,
+																  NameStr(schema->schemaname)));
+				if (objname)
+					*objname = list_make2(nspname,
+										  pstrdup(NameStr(schema->schemaname)));
+				ReleaseSysCache(schemaTup);
+				break;
+			}
+
 		default:
 			elog(ERROR, "unsupported object class: %u", object->classId);
 	}
diff --git a/src/backend/catalog/pg_xmlschema.c b/src/backend/catalog/pg_xmlschema.c
new file mode 100644
index 00000000000..4d44a4ef10b
--- /dev/null
+++ b/src/backend/catalog/pg_xmlschema.c
@@ -0,0 +1,191 @@
+#include "postgres.h"
+
+#include "access/htup_details.h"
+#include "access/table.h"
+#include "catalog/catalog.h"
+#include "catalog/dependency.h"
+#include "catalog/indexing.h"
+#include "catalog/objectaccess.h"
+#include "catalog/pg_xmlschema.h"
+#include "catalog/pg_namespace.h"
+#include "utils/acl.h"
+#include "utils/builtins.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/xml.h"
+
+#ifdef USE_LIBXML
+#include <libxml/xmlschemas.h>
+#endif
+
+/*
+ * XmlSchemaCreate
+ *
+ * Add a new tuple to pg_xmlschema.
+ *
+ * if_not_exists: if true, don't fail on duplicate name, just print a notice
+ * and return InvalidOid.
+ * quiet: if true, don't fail on duplicate name, just silently return
+ * InvalidOid (which overides if_not_exists).
+ */
+Oid
+XmlSchemaCreate(const char *schemaname,
+                Oid schemanamespace,
+                Oid schemaowner,
+                const char *schemadata,
+                bool if_not_exists,
+                bool quiet)
+{
+    Relation      rel;
+    TupleDesc     tupDesc;
+    HeapTuple     tup;
+    Datum         values[Natts_pg_xmlschema];
+    bool          nulls[Natts_pg_xmlschema];
+    NameData      name_name;
+    Oid           oid;
+    ObjectAddress myself,
+                  referenced;
+
+    Assert(schemaname);
+    Assert(schemanamespace);
+    Assert(schemaowner);
+    Assert(schemadata);
+
+#ifdef USE_LIBXML
+    /* Validate the XML Schema before storing it */
+    {
+        xmlSchemaParserCtxtPtr parser_ctxt;
+        xmlSchemaPtr schema_ptr;
+        PgXmlErrorContext *xmlerrcxt;
+
+        xmlerrcxt = pg_xml_init(PG_XML_STRICTNESS_WELLFORMED);
+
+        PG_TRY();
+        {
+            parser_ctxt = xmlSchemaNewMemParserCtxt(schemadata, strlen(schemadata));
+            if (parser_ctxt == NULL)
+                xml_ereport(xmlerrcxt, ERROR, ERRCODE_INVALID_XML_DOCUMENT,
+                            "failed to create schema parser context");
+
+            schema_ptr = xmlSchemaParse(parser_ctxt);
+            if (schema_ptr == NULL)
+                xml_ereport(xmlerrcxt, ERROR, ERRCODE_INVALID_XML_DOCUMENT,
+                            "invalid XML schema definition");
+
+            /* Clean up */
+            xmlSchemaFree(schema_ptr);
+            xmlSchemaFreeParserCtxt(parser_ctxt);
+        }
+        PG_CATCH();
+        {
+            pg_xml_done(xmlerrcxt, true);
+            PG_RE_THROW();
+        }
+        PG_END_TRY();
+
+        pg_xml_done(xmlerrcxt, false);
+    }
+#else
+    ereport(ERROR,
+            (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+             errmsg("xmlschema support requires libxml")));
+#endif
+
+    /*
+     * Make sure there is no existing XML schema of same name in the namespace.
+     *
+     * This would be caught by the unique index anyway; we're just giving a
+     * friendlier error message.  The unique index provides a backstop against
+     * race conditions.
+     */
+    oid = GetSysCacheOid2(XMLSCHEMANAMENSP,
+                          Anum_pg_xmlschema_oid,
+                          PointerGetDatum(schemaname),
+                          ObjectIdGetDatum(schemanamespace));
+    if (OidIsValid(oid))
+    {
+        if (quiet)
+            return InvalidOid;
+        else if (if_not_exists)
+        {
+            /*
+             * If we are in an extension script, insist that the pre-existing
+             * object be a member of the extension, to avoid security risks.
+             */
+            ObjectAddressSet(myself, XmlSchemaRelationId, oid);
+            checkMembershipInCurrentExtension(&myself);
+
+            /* OK to skip */
+            ereport(NOTICE,
+                    (errcode(ERRCODE_DUPLICATE_OBJECT),
+                     errmsg("XML schema \"%s\" already exists, skipping",
+                            schemaname)));
+            return InvalidOid;
+        }
+        else
+            ereport(ERROR,
+                    (errcode(ERRCODE_DUPLICATE_OBJECT),
+                     errmsg("XML schema \"%s\" already exists",
+                            schemaname)));
+    }
+
+    /* open pg_xmlschema; lock to protect against concurrent changes */
+    rel = table_open(XmlSchemaRelationId, ShareRowExclusiveLock);
+
+    tupDesc = RelationGetDescr(rel);
+
+    /* form a tuple */
+    memset(nulls, 0, sizeof(nulls));
+
+    namestrcpy(&name_name, schemaname);
+    oid = GetNewOidWithIndex(rel, XmlSchemaOidIndexId,
+                                  Anum_pg_xmlschema_oid);
+    values[Anum_pg_xmlschema_oid - 1] = ObjectIdGetDatum(oid);
+    values[Anum_pg_xmlschema_schemaname - 1] = NameGetDatum(&name_name);
+    values[Anum_pg_xmlschema_schemanamespace - 1] = ObjectIdGetDatum(schemanamespace);
+    values[Anum_pg_xmlschema_schemaowner - 1] = ObjectIdGetDatum(schemaowner);
+    values[Anum_pg_xmlschema_schemadata - 1] = CStringGetTextDatum(schemadata);
+
+    /* Set up default ACL */
+    {
+        Acl *schemaacl;
+
+        schemaacl = get_user_default_acl(OBJECT_XMLSCHEMA, schemaowner,
+                                         schemanamespace);
+        if (schemaacl != NULL)
+            values[Anum_pg_xmlschema_schemaacl - 1] = PointerGetDatum(schemaacl);
+        else
+            nulls[Anum_pg_xmlschema_schemaacl - 1] = true;
+    }
+
+    tup = heap_form_tuple(tupDesc, values, nulls);
+
+    /* insert a new tuple */
+    CatalogTupleInsert(rel, tup);
+    Assert(OidIsValid(oid));
+
+    /* set up dependencies for the new XML schema */
+    myself.classId = XmlSchemaRelationId;
+    myself.objectId = oid;
+    myself.objectSubId = 0;
+
+    /* create dependency on namespace */
+    referenced.classId = NamespaceRelationId;
+    referenced.objectId = schemanamespace;
+    referenced.objectSubId = 0;
+    recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+
+    /* create dependency on owner */
+    recordDependencyOnOwner(XmlSchemaRelationId, oid, schemaowner);
+
+    /* dependency on extension */
+    recordDependencyOnCurrentExtension(&myself, false);
+
+    /* Post creation hook for new XML schema */
+    InvokeObjectPostCreateHook(XmlSchemaRelationId, oid, 0);
+
+    heap_freetuple(tup);
+    table_close(rel, NoLock);
+
+    return oid;
+}
diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile
index 64cb6278409..07f04eafdab 100644
--- a/src/backend/commands/Makefile
+++ b/src/backend/commands/Makefile
@@ -66,6 +66,7 @@ OBJS = \
 	vacuumparallel.o \
 	variable.o \
 	view.o \
-	wait.o
+	wait.o \
+	xmlschemacmds.o
 
 include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c
index f7b2389b019..778f54bdae6 100644
--- a/src/backend/commands/alter.c
+++ b/src/backend/commands/alter.c
@@ -41,6 +41,7 @@
 #include "catalog/pg_ts_dict.h"
 #include "catalog/pg_ts_parser.h"
 #include "catalog/pg_ts_template.h"
+#include "catalog/pg_xmlschema.h"
 #include "commands/alter.h"
 #include "commands/collationcmds.h"
 #include "commands/dbcommands.h"
@@ -140,6 +141,10 @@ report_namespace_conflict(Oid classId, const char *name, Oid nspOid)
 			Assert(OidIsValid(nspOid));
 			msgfmt = gettext_noop("text search configuration \"%s\" already exists in schema \"%s\"");
 			break;
+		case XmlSchemaRelationId:
+			Assert(OidIsValid(nspOid));
+			msgfmt = gettext_noop("XML schema \"%s\" already exists in schema \"%s\"");
+			break;
 		default:
 			elog(ERROR, "unsupported object class: %u", classId);
 			break;
@@ -423,6 +428,7 @@ ExecRenameStmt(RenameStmt *stmt)
 		case OBJECT_FDW:
 		case OBJECT_FOREIGN_SERVER:
 		case OBJECT_FUNCTION:
+		case OBJECT_XMLSCHEMA:
 		case OBJECT_OPCLASS:
 		case OBJECT_OPFAMILY:
 		case OBJECT_LANGUAGE:
@@ -567,6 +573,7 @@ ExecAlterObjectSchemaStmt(AlterObjectSchemaStmt *stmt,
 		case OBJECT_FUNCTION:
 		case OBJECT_OPERATOR:
 		case OBJECT_OPCLASS:
+		case OBJECT_XMLSCHEMA:
 		case OBJECT_OPFAMILY:
 		case OBJECT_PROCEDURE:
 		case OBJECT_ROUTINE:
@@ -877,6 +884,7 @@ ExecAlterOwnerStmt(AlterOwnerStmt *stmt)
 		case OBJECT_CONVERSION:
 		case OBJECT_FUNCTION:
 		case OBJECT_LANGUAGE:
+		case OBJECT_XMLSCHEMA:
 		case OBJECT_LARGEOBJECT:
 		case OBJECT_OPERATOR:
 		case OBJECT_OPCLASS:
diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c
index 92526012d2a..2e893968ab3 100644
--- a/src/backend/commands/dropcmds.c
+++ b/src/backend/commands/dropcmds.c
@@ -278,6 +278,13 @@ does_not_exist_skipping(ObjectType objtype, Node *object)
 				name = NameListToString(castNode(List, object));
 			}
 			break;
+		case OBJECT_XMLSCHEMA:
+			if (!schema_does_not_exist_skipping(castNode(List, object), &msg, &name))
+			{
+				msg = gettext_noop("XML schema \"%s\" does not exist, skipping");
+				name = NameListToString(castNode(List, object));
+			}
+			break;
 		case OBJECT_SCHEMA:
 			msg = gettext_noop("schema \"%s\" does not exist, skipping");
 			name = strVal(object);
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index 028f9e2de90..4a087935f16 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -2318,6 +2318,7 @@ stringify_grant_objtype(ObjectType objtype)
 		case OBJECT_TSTEMPLATE:
 		case OBJECT_USER_MAPPING:
 		case OBJECT_VIEW:
+		case OBJECT_XMLSCHEMA:
 			elog(ERROR, "unsupported object type: %d", (int) objtype);
 	}
 
@@ -2402,6 +2403,7 @@ stringify_adefprivs_objtype(ObjectType objtype)
 		case OBJECT_TSTEMPLATE:
 		case OBJECT_USER_MAPPING:
 		case OBJECT_VIEW:
+		case OBJECT_XMLSCHEMA:
 			elog(ERROR, "unsupported object type: %d", (int) objtype);
 	}
 
diff --git a/src/backend/commands/meson.build b/src/backend/commands/meson.build
index ca3f53c6213..3363797ecee 100644
--- a/src/backend/commands/meson.build
+++ b/src/backend/commands/meson.build
@@ -55,4 +55,5 @@ backend_sources += files(
   'variable.c',
   'view.c',
   'wait.c',
+  'xmlschemacmds.c',
 )
diff --git a/src/backend/commands/seclabel.c b/src/backend/commands/seclabel.c
index 4160f5b6855..83c6cdd9bb3 100644
--- a/src/backend/commands/seclabel.c
+++ b/src/backend/commands/seclabel.c
@@ -92,6 +92,7 @@ SecLabelSupportsObjectType(ObjectType objtype)
 		case OBJECT_TSPARSER:
 		case OBJECT_TSTEMPLATE:
 		case OBJECT_USER_MAPPING:
+		case OBJECT_XMLSCHEMA:
 			return false;
 
 			/*
diff --git a/src/backend/commands/xmlschemacmds.c b/src/backend/commands/xmlschemacmds.c
new file mode 100644
index 00000000000..13a4d4b7831
--- /dev/null
+++ b/src/backend/commands/xmlschemacmds.c
@@ -0,0 +1,119 @@
+#include "postgres.h"
+
+#include "access/htup_details.h"
+#include "access/table.h"
+#include "catalog/namespace.h"
+#include "catalog/objectaccess.h"
+#include "catalog/pg_xmlschema.h"
+#include "catalog/pg_namespace.h"
+#include "commands/xmlschemacmds.h"
+#include "commands/defrem.h"
+#include "miscadmin.h"
+#include "utils/acl.h"
+#include "utils/builtins.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+
+/*
+ * CREATE XMLSCHEMA
+ */
+ObjectAddress
+DefineXmlSchema(ParseState *pstate, List *names, List *parameters, bool if_not_exists)
+{
+	char	   *schemaName;
+	Oid			schemaNamespace;
+	AclResult	aclresult;
+	ListCell   *pl;
+	DefElem    *schemaDataEl = NULL;
+	char	   *schemaData;
+	Oid			newoid;
+	ObjectAddress address;
+
+	schemaNamespace = QualifiedNameGetCreationNamespace(names, &schemaName);
+
+	aclresult = object_aclcheck(NamespaceRelationId, schemaNamespace, GetUserId(), ACL_CREATE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, OBJECT_SCHEMA,
+					   get_namespace_name(schemaNamespace));
+
+	/* Parse parameters */
+	foreach(pl, parameters)
+	{
+		DefElem    *defel = lfirst_node(DefElem, pl);
+		DefElem   **defelp;
+
+		if (strcmp(defel->defname, "schema") == 0)
+			defelp = &schemaDataEl;
+		else
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("XML schema attribute \"%s\" not recognized",
+							defel->defname),
+					 parser_errposition(pstate, defel->location)));
+			break;
+		}
+		if (*defelp != NULL)
+			errorConflictingDefElem(defel, pstate);
+		*defelp = defel;
+	}
+
+	if (!schemaDataEl)
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg("parameter \"schema\" must be specified")));
+
+	schemaData = defGetString(schemaDataEl);
+
+	newoid = XmlSchemaCreate(schemaName,
+							 schemaNamespace,
+							 GetUserId(),
+							 schemaData,
+							 if_not_exists,
+							 false);
+
+	if (!OidIsValid(newoid))
+	{
+		/*
+		 * When IF NOT EXISTS was specified and the object already existed,
+		 * XmlSchemaCreate returned InvalidOid. Report an invalid object
+		 * address.
+		 */
+		address.classId = XmlSchemaRelationId;
+		address.objectId = InvalidOid;
+		address.objectSubId = 0;
+		return address;
+	}
+
+	/*
+	 * Check that there is not already an XML schema with same name in schema.
+	 * This is really just a friendlier error message than the unique index
+	 * violation.
+	 */
+	IsThereXmlSchemaInNamespace(schemaName, schemaNamespace);
+
+	ObjectAddressSet(address, XmlSchemaRelationId, newoid);
+
+	return address;
+}
+
+/*
+ * IsThereXmlSchemaInNamespace
+ *
+ * Check if there is an XML schema with the given name in the given namespace.
+ * If so, raise an appropriate error.
+ */
+void
+IsThereXmlSchemaInNamespace(const char *schemaname, Oid nspOid)
+{
+	/* make sure the name doesn't conflict */
+	if (SearchSysCacheExists2(XMLSCHEMANAMENSP,
+							  PointerGetDatum(schemaname),
+							  ObjectIdGetDatum(nspOid)))
+		ereport(ERROR,
+				(errcode(ERRCODE_DUPLICATE_OBJECT),
+				 errmsg("XML schema \"%s\" for schema \"%s\" already exists",
+						schemaname, get_namespace_name(nspOid))));
+}
diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
index a7a5ac1e83b..2b416a4d903 100644
--- a/src/backend/executor/execExprInterp.c
+++ b/src/backend/executor/execExprInterp.c
@@ -4626,6 +4626,33 @@ ExecEvalXmlExpr(ExprState *state, ExprEvalStep *op)
 			}
 			break;
 
+		case IS_XMLVALIDATE:
+			{
+				Datum	   *argvalue = op->d.xmlexpr.argvalue;
+				bool	   *argnull = op->d.xmlexpr.argnull;
+				xmltype    *data;
+				Oid			schema_oid;
+				xmltype    *result;
+
+				/* Two arguments: XML data and schema OID */
+				Assert(list_length(xexpr->args) == 2);
+
+				if (argnull[0] || argnull[1])
+				{
+					*op->resnull = true;
+					return;
+				}
+
+				data = DatumGetXmlP(argvalue[0]);
+				schema_oid = DatumGetObjectId(argvalue[1]);
+
+				result = xmlvalidate_schema(data, schema_oid);
+
+				*op->resvalue = PointerGetDatum(result);
+				*op->resnull = false;
+			}
+			break;
+
 		case IS_DOCUMENT:
 			{
 				Datum	   *argvalue = op->d.xmlexpr.argvalue;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 713ee5c10a2..2404f2a849e 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -707,7 +707,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
 
@@ -728,7 +728,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	DETACH DICTIONARY DISABLE_P DISCARD DISTINCT DO DOCUMENT_P DOMAIN_P
 	DOUBLE_P DROP
 
-	EACH ELSE EMPTY_P ENABLE_P ENCODING ENCRYPTED END_P ENFORCED ENUM_P ERROR_P
+	EACH ELSE ELEMENT EMPTY_P ENABLE_P ENCODING ENCRYPTED END_P ENFORCED ENUM_P ERROR_P
 	ESCAPE EVENT EXCEPT EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN
 	EXPRESSION EXTENSION EXTERNAL EXTRACT
 
@@ -798,7 +798,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
 
@@ -6654,6 +6654,27 @@ DefineStmt:
 					n->if_not_exists = true;
 					$$ = (Node *) n;
 				}
+			| CREATE XMLSCHEMA any_name AS Sconst
+				{
+					DefineStmt *n = makeNode(DefineStmt);
+
+					n->kind = OBJECT_XMLSCHEMA;
+					n->args = NIL;
+					n->defnames = $3;
+					n->definition = list_make1(makeDefElem("schema", (Node *) makeString($5), @5));
+					$$ = (Node *) n;
+				}
+			| CREATE XMLSCHEMA IF_P NOT EXISTS any_name AS Sconst
+				{
+					DefineStmt *n = makeNode(DefineStmt);
+
+					n->kind = OBJECT_XMLSCHEMA;
+					n->args = NIL;
+					n->defnames = $6;
+					n->definition = list_make1(makeDefElem("schema", (Node *) makeString($8), @8));
+					n->if_not_exists = true;
+					$$ = (Node *) n;
+				}
 		;
 
 definition: '(' def_list ')'						{ $$ = $2; }
@@ -7193,6 +7214,7 @@ object_type_any_name:
 			| INDEX									{ $$ = OBJECT_INDEX; }
 			| FOREIGN TABLE							{ $$ = OBJECT_FOREIGN_TABLE; }
 			| COLLATION								{ $$ = OBJECT_COLLATION; }
+			| XMLSCHEMA								{ $$ = OBJECT_XMLSCHEMA; }
 			| CONVERSION_P							{ $$ = OBJECT_CONVERSION; }
 			| STATISTICS							{ $$ = OBJECT_STATISTIC_EXT; }
 			| TEXT_P SEARCH PARSER					{ $$ = OBJECT_TSPARSER; }
@@ -8107,6 +8129,15 @@ privilege_target:
 					n->objs = $2;
 					$$ = n;
 				}
+			| XMLSCHEMA any_name_list
+				{
+					PrivTarget *n = palloc_object(PrivTarget);
+
+					n->targtype = ACL_TARGET_OBJECT;
+					n->objtype = OBJECT_XMLSCHEMA;
+					n->objs = $2;
+					$$ = n;
+				}
 			| ALL TABLES IN_P SCHEMA name_list
 				{
 					PrivTarget *n = palloc_object(PrivTarget);
@@ -9573,6 +9604,16 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name
 					n->missing_ok = false;
 					$$ = (Node *) n;
 				}
+			| ALTER XMLSCHEMA any_name RENAME TO name
+				{
+					RenameStmt *n = makeNode(RenameStmt);
+
+					n->renameType = OBJECT_XMLSCHEMA;
+					n->object = (Node *) $3;
+					n->newname = $6;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
 			| ALTER CONVERSION_P any_name RENAME TO name
 				{
 					RenameStmt *n = makeNode(RenameStmt);
@@ -10250,6 +10291,16 @@ AlterObjectSchemaStmt:
 					n->missing_ok = false;
 					$$ = (Node *) n;
 				}
+			| ALTER XMLSCHEMA any_name SET SCHEMA name
+				{
+					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
+
+					n->objectType = OBJECT_XMLSCHEMA;
+					n->object = (Node *) $3;
+					n->newschema = $6;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
 			| ALTER CONVERSION_P any_name SET SCHEMA name
 				{
 					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
@@ -10583,6 +10634,15 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
 					n->newowner = $6;
 					$$ = (Node *) n;
 				}
+			| ALTER XMLSCHEMA any_name OWNER TO RoleSpec
+				{
+					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
+
+					n->objectType = OBJECT_XMLSCHEMA;
+					n->object = (Node *) $3;
+					n->newowner = $6;
+					$$ = (Node *) n;
+				}
 			| ALTER CONVERSION_P any_name OWNER TO RoleSpec
 				{
 					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
@@ -16288,6 +16348,17 @@ func_expr_common_subexpr:
 					n->location = @1;
 					$$ = (Node *) n;
 				}
+			| XMLVALIDATE '(' DOCUMENT_P a_expr ACCORDING TO XMLSCHEMA any_name ')'
+				{
+					XmlExpr *x = (XmlExpr *)
+						makeXmlExpr(IS_XMLVALIDATE, NULL, $8,
+									list_make1($4),
+									@1);
+
+					x->xmloption = XMLOPTION_DOCUMENT;
+					x->location = @1;
+					$$ = (Node *) x;
+				}
 			| JSON_OBJECT '(' func_arg_list ')'
 				{
 					/* Support for legacy (non-standard) json_object() */
@@ -17898,6 +17969,7 @@ unreserved_keyword:
 			| ABSENT
 			| ABSOLUTE_P
 			| ACCESS
+			| ACCORDING
 			| ACTION
 			| ADD_P
 			| ADMIN
@@ -18227,6 +18299,7 @@ unreserved_keyword:
 			| WRAPPER
 			| WRITE
 			| XML_P
+			| XMLSCHEMA
 			| YEAR_P
 			| YES_P
 			| ZONE
@@ -18306,6 +18379,7 @@ col_name_keyword:
 			| XMLROOT
 			| XMLSERIALIZE
 			| XMLTABLE
+			| XMLVALIDATE
 		;
 
 /* Type/function identifier --- keywords that can be type or function names.
@@ -18445,6 +18519,7 @@ bare_label_keyword:
 			| ABSENT
 			| ABSOLUTE_P
 			| ACCESS
+			| ACCORDING
 			| ACTION
 			| ADD_P
 			| ADMIN
@@ -18896,8 +18971,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 56826db4c26..d1c66c53aa8 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -16,6 +16,7 @@
 #include "postgres.h"
 
 #include "access/htup_details.h"
+#include "catalog/namespace.h"
 #include "catalog/pg_aggregate.h"
 #include "catalog/pg_type.h"
 #include "miscadmin.h"
@@ -2361,6 +2362,7 @@ transformXmlExpr(ParseState *pstate, XmlExpr *x)
 	XmlExpr    *newx;
 	ListCell   *lc;
 	int			i;
+	Oid			xmlvalidate_schema_oid = InvalidOid;
 
 	newx = makeNode(XmlExpr);
 	newx->op = x->op;
@@ -2373,6 +2375,21 @@ transformXmlExpr(ParseState *pstate, XmlExpr *x)
 	newx->typmod = -1;
 	newx->location = x->location;
 
+	/*
+	 * XMLVALIDATE stores the schema name list in named_args, not ResTargets.
+	 * Extract it before processing named arguments.
+	 */
+	if (x->op == IS_XMLVALIDATE && x->named_args != NIL)
+	{
+		List *schema_name_list;
+		schema_name_list = x->named_args;
+		xmlvalidate_schema_oid = get_xmlschema_oid(schema_name_list, false);
+		/* Preserve schema name for deparsing */
+		newx->name = NameListToString(schema_name_list);
+		/* Clear to avoid processing as ResTargets */
+		x->named_args = NIL;
+	}
+
 	/*
 	 * gram.y built the named args as a list of ResTarget.  Transform each,
 	 * and break the names out as a separate list.
@@ -2472,6 +2489,11 @@ transformXmlExpr(ParseState *pstate, XmlExpr *x)
 				/* not handled here */
 				Assert(false);
 				break;
+			case IS_XMLVALIDATE:
+				/* First argument is the XML data */
+				newe = coerce_to_specific_type(pstate, newe, XMLOID,
+											   "XMLVALIDATE");
+				break;
 			case IS_DOCUMENT:
 				newe = coerce_to_specific_type(pstate, newe, XMLOID,
 											   "IS DOCUMENT");
@@ -2481,6 +2503,26 @@ transformXmlExpr(ParseState *pstate, XmlExpr *x)
 		i++;
 	}
 
+	/* For XMLVALIDATE, add the schema OID as second argument */
+	if (x->op == IS_XMLVALIDATE)
+	{
+		Const *schema_oid_const;
+
+		Assert(OidIsValid(xmlvalidate_schema_oid));
+
+		schema_oid_const = makeConst(OIDOID,
+									 -1,
+									 InvalidOid,
+									 sizeof(Oid),
+									 ObjectIdGetDatum(xmlvalidate_schema_oid),
+									 false,
+									 true);
+		newx->args = lappend(newx->args, schema_oid_const);
+
+		/* Return type is XML */
+		newx->type = XMLOID;
+	}
+
 	return (Node *) newx;
 }
 
diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c
index b5a2f915b67..b380bb39eb1 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/tcop/utility.c b/src/backend/tcop/utility.c
index 34dd6e18df5..60e0a0f7e7c 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -57,6 +57,7 @@
 #include "commands/vacuum.h"
 #include "commands/view.h"
 #include "commands/wait.h"
+#include "commands/xmlschemacmds.h"
 #include "miscadmin.h"
 #include "parser/parse_utilcmd.h"
 #include "postmaster/bgwriter.h"
@@ -1443,6 +1444,13 @@ ProcessUtilitySlow(ParseState *pstate,
 													  stmt->definition,
 													  stmt->if_not_exists);
 							break;
+						case OBJECT_XMLSCHEMA:
+							Assert(stmt->args == NIL);
+							address = DefineXmlSchema(pstate,
+													  stmt->defnames,
+													  stmt->definition,
+													  stmt->if_not_exists);
+							break;
 						default:
 							elog(ERROR, "unrecognized define stmt type: %d",
 								 (int) stmt->kind);
@@ -2238,6 +2246,9 @@ AlterObjectTypeCommandTag(ObjectType objtype)
 		case OBJECT_COLLATION:
 			tag = CMDTAG_ALTER_COLLATION;
 			break;
+		case OBJECT_XMLSCHEMA:
+			tag = CMDTAG_ALTER_XMLSCHEMA;
+			break;
 		case OBJECT_COLUMN:
 			tag = CMDTAG_ALTER_TABLE;
 			break;
@@ -2575,6 +2586,9 @@ CreateCommandTag(Node *parsetree)
 				case OBJECT_COLLATION:
 					tag = CMDTAG_DROP_COLLATION;
 					break;
+				case OBJECT_XMLSCHEMA:
+					tag = CMDTAG_DROP_XMLSCHEMA;
+					break;
 				case OBJECT_CONVERSION:
 					tag = CMDTAG_DROP_CONVERSION;
 					break;
@@ -2776,6 +2790,9 @@ CreateCommandTag(Node *parsetree)
 				case OBJECT_COLLATION:
 					tag = CMDTAG_CREATE_COLLATION;
 					break;
+				case OBJECT_XMLSCHEMA:
+					tag = CMDTAG_CREATE_XMLSCHEMA;
+					break;
 				case OBJECT_ACCESS_METHOD:
 					tag = CMDTAG_CREATE_ACCESS_METHOD;
 					break;
diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c
index 3a6905f9546..412a0450430 100644
--- a/src/backend/utils/adt/acl.c
+++ b/src/backend/utils/adt/acl.c
@@ -867,6 +867,10 @@ acldefault(ObjectType objtype, Oid ownerId)
 			world_default = ACL_NO_RIGHTS;
 			owner_default = ACL_ALL_RIGHTS_PARAMETER_ACL;
 			break;
+		case OBJECT_XMLSCHEMA:
+			world_default = ACL_NO_RIGHTS;
+			owner_default = ACL_ALL_RIGHTS_XMLSCHEMA;
+			break;
 		default:
 			elog(ERROR, "unrecognized object type: %d", (int) objtype);
 			world_default = ACL_NO_RIGHTS;	/* keep compiler quiet */
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 033b625f3fc..1c400e8dcbd 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -10119,17 +10119,20 @@ 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 ");
 					else
 						appendStringInfoString(buf, "CONTENT ");
 				}
-				if (xexpr->name)
+				if (xexpr->name && xexpr->op != IS_XMLVALIDATE)
 				{
 					appendStringInfo(buf, "NAME %s",
 									 quote_identifier(map_xml_name_to_sql_identifier(xexpr->name)));
@@ -10226,6 +10229,15 @@ 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 ");
+							appendStringInfoString(buf, xexpr->name);
+							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 f69dc68286c..e179d61f923 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
@@ -84,6 +85,7 @@
 #include "catalog/namespace.h"
 #include "catalog/pg_class.h"
 #include "catalog/pg_type.h"
+#include "catalog/pg_xmlschema.h"
 #include "executor/spi.h"
 #include "executor/tablefunc.h"
 #include "fmgr.h"
@@ -94,6 +96,7 @@
 #include "nodes/execnodes.h"
 #include "nodes/miscnodes.h"
 #include "nodes/nodeFuncs.h"
+#include "utils/acl.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
 #include "utils/date.h"
@@ -1158,10 +1161,144 @@ 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_schema - validate XML document against a registered XML Schema
+ *
+ * Validates the given XML document against the schema identified by a schema_oid.
+ * Returns the validated XML value, or raises an error if the validation fails.
+ */
+xmltype *
+xmlvalidate_schema(xmltype *data, Oid schema_oid)
+{
+#ifdef USE_LIBXML
+	HeapTuple	tuple;
+	Datum		schema_datum;
+	bool		isnull;
+	text	   *schema_text;
+	char	   *schemastr;
+	volatile xmlDocPtr doc = NULL;
+	volatile xmlSchemaParserCtxtPtr schema_parser_ctxt = NULL;
+	volatile xmlSchemaPtr schema_ptr = NULL;
+	volatile xmlSchemaValidCtxtPtr valid_ctxt = NULL;
+	int			result;
+	PgXmlErrorContext *xmlerrcxt;
+	AclResult	aclresult;
+
+	/* Check usage permission first */
+	aclresult = object_aclcheck(XmlSchemaRelationId, schema_oid,
+								GetUserId(), ACL_USAGE);
+	if (aclresult != ACLCHECK_OK)
+	{
+		/* Fetch tuple only to get name for the error message */
+		Form_pg_xmlschema schema_form;
+
+		tuple = SearchSysCache1(XMLSCHEMAOID, ObjectIdGetDatum(schema_oid));
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "cache lookup failed for XML schema %u", schema_oid);
+
+		schema_form = (Form_pg_xmlschema) GETSTRUCT(tuple);
+		ReleaseSysCache(tuple);
+
+		aclcheck_error(aclresult, OBJECT_XMLSCHEMA,
+					   NameStr(schema_form->schemaname));
+	}
+
+	tuple = SearchSysCache1(XMLSCHEMAOID, ObjectIdGetDatum(schema_oid));
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for XML schema %u", schema_oid);
+
+	schema_datum = SysCacheGetAttr(XMLSCHEMAOID, tuple,
+								   Anum_pg_xmlschema_schemadata, &isnull);
+	if (isnull)
+		elog(ERROR, "null schemadata for XML schema %u", schema_oid);
+
+	schema_text = DatumGetTextPP(schema_datum);
+	schemastr = text_to_cstring(schema_text);
+	ReleaseSysCache(tuple);
+
+	xmlerrcxt = pg_xml_init(PG_XML_STRICTNESS_WELLFORMED);
+
+	PG_TRY();
+	{
+		ErrorSaveContext escontext = {T_ErrorSaveContext};
+		doc = xml_parse((text *) data, XMLOPTION_DOCUMENT, true,
+						GetDatabaseEncoding(), NULL, NULL, (Node *) &escontext);
+
+		if (escontext.error_occurred || doc == NULL)
+		{
+			if (escontext.error_occurred && escontext.error_data)
+			{
+				ErrorData *edata = escontext.error_data;
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_XML_DOCUMENT),
+						 errmsg("invalid XML document"),
+						 errdetail_internal("%s", edata->message ? edata->message : "unknown error")));
+			}
+			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_INTERNAL_ERROR,
+						"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");
+
+		xmlSchemaSetValidStructuredErrors(valid_ctxt, xml_errorHandler, xmlerrcxt);
+
+		result = xmlSchemaValidateDoc(valid_ctxt, doc);
+		if (result < 0)
+			xml_ereport(xmlerrcxt, ERROR, ERRCODE_INTERNAL_ERROR,
+						"internal error during schema validation");
+		if (result > 0)
+			xml_ereport(xmlerrcxt, ERROR, ERRCODE_INVALID_XML_DOCUMENT,
+						"XML validation failed");
+	}
+	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(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(schemastr);
+	return data;
+#else
+	NO_XML_SUPPORT();
+	return NULL;
+#endif
+}
 
 bool
 xml_is_document(xmltype *arg)
@@ -1181,7 +1318,7 @@ xml_is_document(xmltype *arg)
 	return !escontext.error_occurred;
 #else							/* not USE_LIBXML */
 	NO_XML_SUPPORT();
-	return false;
+	return NULL;
 #endif							/* not USE_LIBXML */
 }
 
diff --git a/src/include/catalog/Makefile b/src/include/catalog/Makefile
index c90022f7c57..dcb1d896d9f 100644
--- a/src/include/catalog/Makefile
+++ b/src/include/catalog/Makefile
@@ -72,6 +72,7 @@ CATALOG_HEADERS := \
 	pg_seclabel.h \
 	pg_shseclabel.h \
 	pg_collation.h \
+	pg_xmlschema.h \
 	pg_parameter_acl.h \
 	pg_partitioned_table.h \
 	pg_range.h \
diff --git a/src/include/catalog/meson.build b/src/include/catalog/meson.build
index b63cd584068..5846919f626 100644
--- a/src/include/catalog/meson.build
+++ b/src/include/catalog/meson.build
@@ -59,6 +59,7 @@ catalog_headers = [
   'pg_seclabel.h',
   'pg_shseclabel.h',
   'pg_collation.h',
+  'pg_xmlschema.h',
   'pg_parameter_acl.h',
   'pg_partitioned_table.h',
   'pg_range.h',
diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h
index 1a25973685c..a0f9585c53a 100644
--- a/src/include/catalog/namespace.h
+++ b/src/include/catalog/namespace.h
@@ -191,6 +191,7 @@ extern bool SearchPathMatchesCurrentEnvironment(SearchPathMatcher *path);
 
 extern Oid	get_collation_oid(List *collname, bool missing_ok);
 extern Oid	get_conversion_oid(List *conname, bool missing_ok);
+extern Oid	get_xmlschema_oid(List *schemaname, bool missing_ok);
 extern Oid	FindDefaultConversionProc(int32 for_encoding, int32 to_encoding);
 
 
diff --git a/src/include/catalog/pg_xmlschema.h b/src/include/catalog/pg_xmlschema.h
new file mode 100644
index 00000000000..86d228ad15f
--- /dev/null
+++ b/src/include/catalog/pg_xmlschema.h
@@ -0,0 +1,43 @@
+#ifndef PG_XMLSCHEMA_H
+#define PG_XMLSCHEMA_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_xmlschema_d.h"
+
+CATALOG(pg_xmlschema,6434,XmlSchemaRelationId)
+{
+    Oid      oid;        /* oid */
+    NameData schemaname; /* XML schema name */
+    /* OID of namespace containing this XML schema */
+    Oid schemanamespace BKI_DEFAULT(pg_catalog) BKI_LOOKUP(pg_namespace);
+    /* owner of XML schema */
+    Oid schemaowner BKI_DEFAULT(POSTGRES) BKI_LOOKUP(pg_authid);
+#ifdef CATALOG_VARLEN
+    text schemadata BKI_FORCE_NOT_NULL; /* XSD schema definition text */
+    /* Access privileges */
+    aclitem schemaacl[1] BKI_DEFAULT(_null_);
+#endif
+} FormData_pg_xmlschema;
+
+/* ----------------
+ *        Form_pg_xmlschema maps to a pointer to a row with
+ *        the format of pg_xmlschema relation.
+ * ----------------
+ */
+typedef FormData_pg_xmlschema *Form_pg_xmlschema;
+
+DECLARE_TOAST(pg_xmlschema, 6435, 6436);
+DECLARE_UNIQUE_INDEX(pg_xmlschema_name_nsp_index, 6437, XmlSchemaNameNspIndexId, pg_xmlschema, btree(schemaname name_ops, schemanamespace oid_ops));
+DECLARE_UNIQUE_INDEX_PKEY(pg_xmlschema_oid_index, 6438, XmlSchemaOidIndexId, pg_xmlschema, btree(oid oid_ops));
+
+MAKE_SYSCACHE(XMLSCHEMANAMENSP, pg_xmlschema_name_nsp_index, 8);
+MAKE_SYSCACHE(XMLSCHEMAOID, pg_xmlschema_oid_index, 8);
+
+extern Oid    XmlSchemaCreate(const char *schemaname,
+                            Oid schemanamespace,
+                            Oid schemaowner,
+                            const char *schemadata,
+                            bool if_not_exists,
+                            bool quiet);
+
+#endif /* PG_XMLSCHEMA_H */
diff --git a/src/include/commands/xmlschemacmds.h b/src/include/commands/xmlschemacmds.h
new file mode 100644
index 00000000000..db8c169452e
--- /dev/null
+++ b/src/include/commands/xmlschemacmds.h
@@ -0,0 +1,10 @@
+#ifndef XMLSCHEMACMDS_H
+#define XMLSCHEMACMDS_H
+
+#include "catalog/objectaddress.h"
+#include "parser/parse_node.h"
+
+extern ObjectAddress DefineXmlSchema(ParseState *pstate, List *names, List *parameters, bool if_not_exists);
+extern void IsThereXmlSchemaInNamespace(const char *schemaname, Oid nspOid);
+
+#endif /* XMLSCHEMACMDS_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index aac4bfc70d9..b1227f1a7b7 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2401,6 +2401,7 @@ typedef enum ObjectType
 	OBJECT_TYPE,
 	OBJECT_USER_MAPPING,
 	OBJECT_VIEW,
+	OBJECT_XMLSCHEMA,
 } ObjectType;
 
 /* ----------------------
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index 5211cadc258..c40cbc8981a 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) */
 } XmlExprOp;
 
 typedef enum XmlOptionType
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index f7753c5c8a8..4e8b5e1d7e7 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)
@@ -520,8 +521,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/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index 1290c9bab68..1cd35452acd 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -31,6 +31,7 @@ PG_CMDTAG(CMDTAG_ALTER_CAST, "ALTER CAST", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_COLLATION, "ALTER COLLATION", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_CONSTRAINT, "ALTER CONSTRAINT", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_CONVERSION, "ALTER CONVERSION", true, false, false)
+PG_CMDTAG(CMDTAG_ALTER_XMLSCHEMA, "ALTER XMLSCHEMA", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_DATABASE, "ALTER DATABASE", false, false, false)
 PG_CMDTAG(CMDTAG_ALTER_DEFAULT_PRIVILEGES, "ALTER DEFAULT PRIVILEGES", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_DOMAIN, "ALTER DOMAIN", true, false, false)
@@ -88,6 +89,7 @@ PG_CMDTAG(CMDTAG_CREATE_CAST, "CREATE CAST", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_COLLATION, "CREATE COLLATION", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_CONSTRAINT, "CREATE CONSTRAINT", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_CONVERSION, "CREATE CONVERSION", true, false, false)
+PG_CMDTAG(CMDTAG_CREATE_XMLSCHEMA, "CREATE XMLSCHEMA", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_DATABASE, "CREATE DATABASE", false, false, false)
 PG_CMDTAG(CMDTAG_CREATE_DOMAIN, "CREATE DOMAIN", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_EVENT_TRIGGER, "CREATE EVENT TRIGGER", false, false, false)
@@ -140,6 +142,7 @@ PG_CMDTAG(CMDTAG_DROP_CAST, "DROP CAST", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_COLLATION, "DROP COLLATION", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_CONSTRAINT, "DROP CONSTRAINT", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_CONVERSION, "DROP CONVERSION", true, false, false)
+PG_CMDTAG(CMDTAG_DROP_XMLSCHEMA, "DROP XMLSCHEMA", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_DATABASE, "DROP DATABASE", false, false, false)
 PG_CMDTAG(CMDTAG_DROP_DOMAIN, "DROP DOMAIN", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_EVENT_TRIGGER, "DROP EVENT TRIGGER", false, false, false)
diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h
index ec01fd581cf..e24427c9663 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -169,6 +169,7 @@ typedef struct ArrayType Acl;
 #define ACL_ALL_RIGHTS_SCHEMA		(ACL_USAGE|ACL_CREATE)
 #define ACL_ALL_RIGHTS_TABLESPACE	(ACL_CREATE)
 #define ACL_ALL_RIGHTS_TYPE			(ACL_USAGE)
+#define ACL_ALL_RIGHTS_XMLSCHEMA	(ACL_USAGE)
 
 /* operation codes for pg_*_aclmask */
 typedef enum
diff --git a/src/include/utils/xml.h b/src/include/utils/xml.h
index 03acb255449..dc6a4d37840 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 xmltype *xmlvalidate_schema(xmltype *data, Oid schema_oid);
 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/oidjoins.out b/src/test/regress/expected/oidjoins.out
index 215eb899be3..544b3ef31ed 100644
--- a/src/test/regress/expected/oidjoins.out
+++ b/src/test/regress/expected/oidjoins.out
@@ -239,6 +239,8 @@ NOTICE:  checking pg_seclabel {classoid} => pg_class {oid}
 NOTICE:  checking pg_shseclabel {classoid} => pg_class {oid}
 NOTICE:  checking pg_collation {collnamespace} => pg_namespace {oid}
 NOTICE:  checking pg_collation {collowner} => pg_authid {oid}
+NOTICE:  checking pg_xmlschema {schemanamespace} => pg_namespace {oid}
+NOTICE:  checking pg_xmlschema {schemaowner} => pg_authid {oid}
 NOTICE:  checking pg_partitioned_table {partrelid} => pg_class {oid}
 NOTICE:  checking pg_partitioned_table {partdefid} => pg_class {oid}
 NOTICE:  checking pg_partitioned_table {partclass} => pg_opclass {oid}
diff --git a/src/test/regress/expected/xml.out b/src/test/regress/expected/xml.out
index 103a22a3b1d..0362f31752d 100644
--- a/src/test/regress/expected/xml.out
+++ b/src/test/regress/expected/xml.out
@@ -1881,3 +1881,300 @@ SELECT xmltext('x'|| '<P>73</P>'::xml || .42 || true || 'j'::char);
  x&lt;P&gt;73&lt;/P&gt;0.42truej
 (1 row)
 
+CREATE XMLSCHEMA person_schema AS '<?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>';
+CREATE XMLSCHEMA IF NOT EXISTS person_schema AS '<?xml version="1.0"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+  <xs:element name="other" type="xs:string"/>
+</xs:schema>';
+NOTICE:  XML schema "person_schema" already exists, skipping
+CREATE XMLSCHEMA person_schema AS '<?xml version="1.0"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+  <xs:element name="duplicate" type="xs:string"/>
+</xs:schema>';
+ERROR:  XML schema "person_schema" already exists
+CREATE SCHEMA test_xmlschema_ns;
+CREATE XMLSCHEMA test_xmlschema_ns.product_schema AS '<?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>';
+CREATE XMLSCHEMA bad_schema AS '<this-is-not-valid-xsd>';
+ERROR:  invalid XML schema definition
+DETAIL:  line 1: Premature end of data in tag this-is-not-valid-xsd line 1
+<this-is-not-valid-xsd>
+                       ^
+CREATE XMLSCHEMA bad_xml_schema AS 'not even xml';
+ERROR:  invalid XML schema definition
+DETAIL:  line 1: Start tag expected, '<' not found
+not even xml
+^
+CREATE XMLSCHEMA book_schema AS '<?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:element name="author" maxOccurs="unbounded">
+          <xs:complexType>
+            <xs:sequence>
+              <xs:element name="firstname" type="xs:string"/>
+              <xs:element name="lastname" type="xs:string"/>
+            </xs:sequence>
+          </xs:complexType>
+        </xs:element>
+        <xs:element name="year" type="xs:integer"/>
+      </xs:sequence>
+      <xs:attribute name="isbn" type="xs:string" use="required"/>
+    </xs:complexType>
+  </xs:element>
+</xs:schema>';
+SELECT XMLVALIDATE(DOCUMENT '<person><name>John</name><age>30</age></person>'
+  ACCORDING TO XMLSCHEMA person_schema);
+                   xmlvalidate                   
+-------------------------------------------------
+ <person><name>John</name><age>30</age></person>
+(1 row)
+
+SELECT XMLVALIDATE(DOCUMENT '<product id="P123"><name>Widget</name><price>9.99</price></product>'
+  ACCORDING TO XMLSCHEMA test_xmlschema_ns.product_schema);
+                             xmlvalidate                             
+---------------------------------------------------------------------
+ <product id="P123"><name>Widget</name><price>9.99</price></product>
+(1 row)
+
+SELECT XMLVALIDATE(DOCUMENT
+  '<book isbn="978-0-123456-78-9">
+    <title>PostgreSQL Internals</title>
+    <author><firstname>John</firstname><lastname>Titor</lastname></author>
+    <author><firstname>Jane</firstname></author>
+    <year>2024</year>
+  </book>'
+  ACCORDING TO XMLSCHEMA book_schema);
+ERROR:  XML validation failed
+SELECT XMLSERIALIZE(DOCUMENT
+  XMLVALIDATE(DOCUMENT '<person><name>Alice</name><age>25</age></person>'
+    ACCORDING TO XMLSCHEMA person_schema) AS text);
+                   xmlserialize                   
+--------------------------------------------------
+ <person><name>Alice</name><age>25</age></person>
+(1 row)
+
+SELECT XMLVALIDATE(DOCUMENT NULL ACCORDING TO XMLSCHEMA person_schema);
+ xmlvalidate 
+-------------
+ 
+(1 row)
+
+SELECT XMLVALIDATE(DOCUMENT NULL ACCORDING TO XMLSCHEMA person_schema) IS NULL AS is_null;
+ is_null 
+---------
+ t
+(1 row)
+
+SELECT XMLVALIDATE(DOCUMENT '<person><name>John</name></person>'
+  ACCORDING TO XMLSCHEMA person_schema);
+ERROR:  XML validation failed
+SELECT XMLVALIDATE(DOCUMENT '<person><name>John</name><age>not-a-number</age></person>'
+  ACCORDING TO XMLSCHEMA person_schema);
+ERROR:  XML validation failed
+SELECT XMLVALIDATE(DOCUMENT '<product><name>Widget</name><price>9.99</price></product>'
+  ACCORDING TO XMLSCHEMA test_xmlschema_ns.product_schema);
+ERROR:  XML validation failed
+SELECT XMLVALIDATE(DOCUMENT '<person><name>John</name><age>30</age><extra>data</extra></person>'
+  ACCORDING TO XMLSCHEMA person_schema);
+ERROR:  XML validation failed
+SELECT XMLVALIDATE(DOCUMENT '<notperson><name>John</name><age>30</age></notperson>'
+  ACCORDING TO XMLSCHEMA person_schema);
+ERROR:  XML validation failed
+SELECT XMLVALIDATE(DOCUMENT '<test>value</test>'
+  ACCORDING TO XMLSCHEMA nonexistent_schema);
+ERROR:  XML schema "nonexistent_schema" does not exist
+CREATE VIEW validated_people AS
+  SELECT XMLVALIDATE(DOCUMENT data ACCORDING TO XMLSCHEMA person_schema) AS validated_xml
+  FROM xmltest WHERE id = 1;
+SELECT pg_get_viewdef('validated_people'::regclass, true);
+                                      pg_get_viewdef                                      
+------------------------------------------------------------------------------------------
+  SELECT XMLVALIDATE(DOCUMENT data ACCORDING TO XMLSCHEMA person_schema) AS validated_xml+
+    FROM xmltest                                                                         +
+   WHERE id = 1;
+(1 row)
+
+DROP VIEW validated_people;
+CREATE VIEW validated_products AS
+  SELECT XMLVALIDATE(DOCUMENT '<product id="123"><name>Test</name><price>1.99</price></product>'
+    ACCORDING TO XMLSCHEMA test_xmlschema_ns.product_schema) AS validated_xml;
+SELECT pg_get_viewdef('validated_products'::regclass, true);
+                                                                                 pg_get_viewdef                                                                                  
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  SELECT XMLVALIDATE(DOCUMENT '<product id="123"><name>Test</name><price>1.99</price></product>'::xml ACCORDING TO XMLSCHEMA test_xmlschema_ns.product_schema) AS validated_xml;
+(1 row)
+
+DROP VIEW validated_products;
+ALTER XMLSCHEMA book_schema RENAME TO library_book_schema;
+SELECT XMLVALIDATE(DOCUMENT '<book isbn="123"><title>Test</title><author><firstname>A</firstname><lastname>B</lastname></author><year>2024</year></book>'
+  ACCORDING TO XMLSCHEMA book_schema);
+ERROR:  XML schema "book_schema" does not exist
+SELECT XMLVALIDATE(DOCUMENT '<book isbn="123"><title>Test</title><author><firstname>A</firstname><lastname>B</lastname></author><year>2024</year></book>'
+  ACCORDING TO XMLSCHEMA library_book_schema);
+                                                         xmlvalidate                                                         
+-----------------------------------------------------------------------------------------------------------------------------
+ <book isbn="123"><title>Test</title><author><firstname>A</firstname><lastname>B</lastname></author><year>2024</year></book>
+(1 row)
+
+ALTER XMLSCHEMA library_book_schema SET SCHEMA test_xmlschema_ns;
+SELECT XMLVALIDATE(DOCUMENT '<book isbn="123"><title>Test</title><author><firstname>A</firstname><lastname>B</lastname></author><year>2024</year></book>'
+  ACCORDING TO XMLSCHEMA library_book_schema);
+ERROR:  XML schema "library_book_schema" does not exist
+SELECT XMLVALIDATE(DOCUMENT '<book isbn="123"><title>Test</title><author><firstname>A</firstname><lastname>B</lastname></author><year>2024</year></book>'
+  ACCORDING TO XMLSCHEMA test_xmlschema_ns.library_book_schema);
+                                                         xmlvalidate                                                         
+-----------------------------------------------------------------------------------------------------------------------------
+ <book isbn="123"><title>Test</title><author><firstname>A</firstname><lastname>B</lastname></author><year>2024</year></book>
+(1 row)
+
+CREATE ROLE regress_xmlschema_test_role;
+ALTER XMLSCHEMA test_xmlschema_ns.library_book_schema OWNER TO regress_xmlschema_test_role;
+SELECT schemaname, schemanamespace::regnamespace, schemaowner::regrole
+FROM pg_xmlschema
+WHERE schemaname = 'library_book_schema';
+     schemaname      |  schemanamespace  |         schemaowner         
+---------------------+-------------------+-----------------------------
+ library_book_schema | test_xmlschema_ns | regress_xmlschema_test_role
+(1 row)
+
+CREATE VIEW book_view AS
+  SELECT XMLVALIDATE(DOCUMENT '<book isbn="456"><title>Dep Test</title><author><firstname>X</firstname><lastname>Y</lastname></author><year>2025</year></book>'
+    ACCORDING TO XMLSCHEMA test_xmlschema_ns.library_book_schema) AS validated_book;
+DROP XMLSCHEMA test_xmlschema_ns.library_book_schema;
+ERROR:  cannot drop XML schema test_xmlschema_ns.library_book_schema because other objects depend on it
+DETAIL:  view book_view depends on XML schema test_xmlschema_ns.library_book_schema
+HINT:  Use DROP ... CASCADE to drop the dependent objects too.
+DROP XMLSCHEMA test_xmlschema_ns.library_book_schema CASCADE;
+NOTICE:  drop cascades to view book_view
+SELECT * FROM book_view;
+ERROR:  relation "book_view" does not exist
+LINE 1: SELECT * FROM book_view;
+                      ^
+DROP XMLSCHEMA person_schema;
+DROP XMLSCHEMA IF EXISTS person_schema;
+NOTICE:  XML schema "person_schema" does not exist, skipping
+DROP XMLSCHEMA person_schema;
+ERROR:  XML schema "person_schema" does not exist
+DROP XMLSCHEMA test_xmlschema_ns.product_schema;
+SET ROLE regress_xmlschema_test_role;
+CREATE XMLSCHEMA public.should_fail_schema AS '<?xml version="1.0"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+  <xs:element name="test" type="xs:string"/>
+</xs:schema>';
+RESET ROLE;
+GRANT CREATE ON SCHEMA test_xmlschema_ns TO regress_xmlschema_test_role;
+SET ROLE regress_xmlschema_test_role;
+CREATE XMLSCHEMA test_xmlschema_ns.role_schema AS '<?xml version="1.0"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+  <xs:element name="test" type="xs:string"/>
+</xs:schema>';
+RESET ROLE;
+DROP XMLSCHEMA test_xmlschema_ns.role_schema;
+DROP ROLE regress_xmlschema_test_role;
+ERROR:  role "regress_xmlschema_test_role" cannot be dropped because some objects depend on it
+DETAIL:  privileges for schema test_xmlschema_ns
+owner of XML schema public.should_fail_schema
+DROP SCHEMA test_xmlschema_ns CASCADE;
+CREATE ROLE regress_xmlschema_user1;
+CREATE ROLE regress_xmlschema_user2;
+CREATE XMLSCHEMA permission_test_schema AS '<?xml version="1.0"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+  <xs:element name="test" type="xs:string"/>
+</xs:schema>';
+SET ROLE regress_xmlschema_user1;
+SELECT XMLVALIDATE(DOCUMENT '<test>data</test>'
+                   ACCORDING TO XMLSCHEMA permission_test_schema);
+ERROR:  permission denied for XML schema permission_test_schema
+RESET ROLE;
+GRANT USAGE ON XMLSCHEMA permission_test_schema TO regress_xmlschema_user1;
+SET ROLE regress_xmlschema_user1;
+SELECT XMLVALIDATE(DOCUMENT '<test>data</test>'
+                   ACCORDING TO XMLSCHEMA permission_test_schema);
+    xmlvalidate    
+-------------------
+ <test>data</test>
+(1 row)
+
+RESET ROLE;
+SET ROLE regress_xmlschema_user2;
+SELECT XMLVALIDATE(DOCUMENT '<test>data</test>'
+                   ACCORDING TO XMLSCHEMA permission_test_schema);
+ERROR:  permission denied for XML schema permission_test_schema
+RESET ROLE;
+GRANT USAGE ON XMLSCHEMA permission_test_schema TO regress_xmlschema_user2;
+SET ROLE regress_xmlschema_user2;
+SELECT XMLVALIDATE(DOCUMENT '<test>data</test>'
+                   ACCORDING TO XMLSCHEMA permission_test_schema);
+    xmlvalidate    
+-------------------
+ <test>data</test>
+(1 row)
+
+RESET ROLE;
+REVOKE USAGE ON XMLSCHEMA permission_test_schema FROM regress_xmlschema_user1;
+SET ROLE regress_xmlschema_user1;
+SELECT XMLVALIDATE(DOCUMENT '<test>data</test>'
+                   ACCORDING TO XMLSCHEMA permission_test_schema);
+ERROR:  permission denied for XML schema permission_test_schema
+RESET ROLE;
+DROP XMLSCHEMA permission_test_schema;
+DROP ROLE regress_xmlschema_user1;
+DROP ROLE regress_xmlschema_user2;
+CREATE TABLE validated_xml_data (
+  id serial PRIMARY KEY,
+  data xml
+);
+CREATE XMLSCHEMA data_schema AS '<?xml version="1.0"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+  <xs:element name="data">
+    <xs:complexType>
+      <xs:sequence>
+        <xs:element name="value" type="xs:integer"/>
+      </xs:sequence>
+    </xs:complexType>
+  </xs:element>
+</xs:schema>';
+INSERT INTO validated_xml_data (data)
+VALUES (XMLVALIDATE(DOCUMENT '<data><value>42</value></data>'
+  ACCORDING TO XMLSCHEMA data_schema));
+INSERT INTO validated_xml_data (data)
+VALUES (XMLVALIDATE(DOCUMENT '<data><value>100</value></data>'
+  ACCORDING TO XMLSCHEMA data_schema));
+SELECT id, data FROM validated_xml_data ORDER BY id;
+ id |              data               
+----+---------------------------------
+  1 | <data><value>42</value></data>
+  2 | <data><value>100</value></data>
+(2 rows)
+
+INSERT INTO validated_xml_data (data)
+VALUES (XMLVALIDATE(DOCUMENT '<data><value>not-an-int</value></data>'
+  ACCORDING TO XMLSCHEMA data_schema));
+ERROR:  XML validation failed
+DROP TABLE validated_xml_data;
+DROP XMLSCHEMA data_schema;
+DROP XMLSCHEMA should_fail_schema;
+DROP ROLE regress_xmlschema_test_role;
diff --git a/src/test/regress/expected/xml_2.out b/src/test/regress/expected/xml_2.out
index a85d95358d9..5116ca73e4b 100644
--- a/src/test/regress/expected/xml_2.out
+++ b/src/test/regress/expected/xml_2.out
@@ -1867,3 +1867,300 @@ SELECT xmltext('x'|| '<P>73</P>'::xml || .42 || true || 'j'::char);
  x&lt;P&gt;73&lt;/P&gt;0.42truej
 (1 row)
 
+CREATE XMLSCHEMA person_schema AS '<?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>';
+CREATE XMLSCHEMA IF NOT EXISTS person_schema AS '<?xml version="1.0"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+  <xs:element name="other" type="xs:string"/>
+</xs:schema>';
+NOTICE:  XML schema "person_schema" already exists, skipping
+CREATE XMLSCHEMA person_schema AS '<?xml version="1.0"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+  <xs:element name="duplicate" type="xs:string"/>
+</xs:schema>';
+ERROR:  XML schema "person_schema" already exists
+CREATE SCHEMA test_xmlschema_ns;
+CREATE XMLSCHEMA test_xmlschema_ns.product_schema AS '<?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>';
+CREATE XMLSCHEMA bad_schema AS '<this-is-not-valid-xsd>';
+ERROR:  invalid XML schema definition
+DETAIL:  line 1: Premature end of data in tag this-is-not-valid-xsd line 1
+<this-is-not-valid-xsd>
+                       ^
+CREATE XMLSCHEMA bad_xml_schema AS 'not even xml';
+ERROR:  invalid XML schema definition
+DETAIL:  line 1: Start tag expected, '<' not found
+not even xml
+^
+CREATE XMLSCHEMA book_schema AS '<?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:element name="author" maxOccurs="unbounded">
+          <xs:complexType>
+            <xs:sequence>
+              <xs:element name="firstname" type="xs:string"/>
+              <xs:element name="lastname" type="xs:string"/>
+            </xs:sequence>
+          </xs:complexType>
+        </xs:element>
+        <xs:element name="year" type="xs:integer"/>
+      </xs:sequence>
+      <xs:attribute name="isbn" type="xs:string" use="required"/>
+    </xs:complexType>
+  </xs:element>
+</xs:schema>';
+SELECT XMLVALIDATE(DOCUMENT '<person><name>John</name><age>30</age></person>'
+  ACCORDING TO XMLSCHEMA person_schema);
+                   xmlvalidate                   
+-------------------------------------------------
+ <person><name>John</name><age>30</age></person>
+(1 row)
+
+SELECT XMLVALIDATE(DOCUMENT '<product id="P123"><name>Widget</name><price>9.99</price></product>'
+  ACCORDING TO XMLSCHEMA test_xmlschema_ns.product_schema);
+                             xmlvalidate                             
+---------------------------------------------------------------------
+ <product id="P123"><name>Widget</name><price>9.99</price></product>
+(1 row)
+
+SELECT XMLVALIDATE(DOCUMENT
+  '<book isbn="978-0-123456-78-9">
+    <title>PostgreSQL Internals</title>
+    <author><firstname>John</firstname><lastname>Titor</lastname></author>
+    <author><firstname>Jane</firstname></author>
+    <year>2024</year>
+  </book>'
+  ACCORDING TO XMLSCHEMA book_schema);
+ERROR:  XML validation failed
+SELECT XMLSERIALIZE(DOCUMENT
+  XMLVALIDATE(DOCUMENT '<person><name>Alice</name><age>25</age></person>'
+    ACCORDING TO XMLSCHEMA person_schema) AS text);
+                   xmlserialize                   
+--------------------------------------------------
+ <person><name>Alice</name><age>25</age></person>
+(1 row)
+
+SELECT XMLVALIDATE(DOCUMENT NULL ACCORDING TO XMLSCHEMA person_schema);
+ xmlvalidate 
+-------------
+ 
+(1 row)
+
+SELECT XMLVALIDATE(DOCUMENT NULL ACCORDING TO XMLSCHEMA person_schema) IS NULL AS is_null;
+ is_null 
+---------
+ t
+(1 row)
+
+SELECT XMLVALIDATE(DOCUMENT '<person><name>John</name></person>'
+  ACCORDING TO XMLSCHEMA person_schema);
+ERROR:  XML validation failed
+SELECT XMLVALIDATE(DOCUMENT '<person><name>John</name><age>not-a-number</age></person>'
+  ACCORDING TO XMLSCHEMA person_schema);
+ERROR:  XML validation failed
+SELECT XMLVALIDATE(DOCUMENT '<product><name>Widget</name><price>9.99</price></product>'
+  ACCORDING TO XMLSCHEMA test_xmlschema_ns.product_schema);
+ERROR:  XML validation failed
+SELECT XMLVALIDATE(DOCUMENT '<person><name>John</name><age>30</age><extra>data</extra></person>'
+  ACCORDING TO XMLSCHEMA person_schema);
+ERROR:  XML validation failed
+SELECT XMLVALIDATE(DOCUMENT '<notperson><name>John</name><age>30</age></notperson>'
+  ACCORDING TO XMLSCHEMA person_schema);
+ERROR:  XML validation failed
+SELECT XMLVALIDATE(DOCUMENT '<test>value</test>'
+  ACCORDING TO XMLSCHEMA nonexistent_schema);
+ERROR:  XML schema "nonexistent_schema" does not exist
+CREATE VIEW validated_people AS
+  SELECT XMLVALIDATE(DOCUMENT data ACCORDING TO XMLSCHEMA person_schema) AS validated_xml
+  FROM xmltest WHERE id = 1;
+SELECT pg_get_viewdef('validated_people'::regclass, true);
+                                      pg_get_viewdef                                      
+------------------------------------------------------------------------------------------
+  SELECT XMLVALIDATE(DOCUMENT data ACCORDING TO XMLSCHEMA person_schema) AS validated_xml+
+    FROM xmltest                                                                         +
+   WHERE id = 1;
+(1 row)
+
+DROP VIEW validated_people;
+CREATE VIEW validated_products AS
+  SELECT XMLVALIDATE(DOCUMENT '<product id="123"><name>Test</name><price>1.99</price></product>'
+    ACCORDING TO XMLSCHEMA test_xmlschema_ns.product_schema) AS validated_xml;
+SELECT pg_get_viewdef('validated_products'::regclass, true);
+                                                                                 pg_get_viewdef                                                                                  
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  SELECT XMLVALIDATE(DOCUMENT '<product id="123"><name>Test</name><price>1.99</price></product>'::xml ACCORDING TO XMLSCHEMA test_xmlschema_ns.product_schema) AS validated_xml;
+(1 row)
+
+DROP VIEW validated_products;
+ALTER XMLSCHEMA book_schema RENAME TO library_book_schema;
+SELECT XMLVALIDATE(DOCUMENT '<book isbn="123"><title>Test</title><author><firstname>A</firstname><lastname>B</lastname></author><year>2024</year></book>'
+  ACCORDING TO XMLSCHEMA book_schema);
+ERROR:  XML schema "book_schema" does not exist
+SELECT XMLVALIDATE(DOCUMENT '<book isbn="123"><title>Test</title><author><firstname>A</firstname><lastname>B</lastname></author><year>2024</year></book>'
+  ACCORDING TO XMLSCHEMA library_book_schema);
+                                                         xmlvalidate                                                         
+-----------------------------------------------------------------------------------------------------------------------------
+ <book isbn="123"><title>Test</title><author><firstname>A</firstname><lastname>B</lastname></author><year>2024</year></book>
+(1 row)
+
+ALTER XMLSCHEMA library_book_schema SET SCHEMA test_xmlschema_ns;
+SELECT XMLVALIDATE(DOCUMENT '<book isbn="123"><title>Test</title><author><firstname>A</firstname><lastname>B</lastname></author><year>2024</year></book>'
+  ACCORDING TO XMLSCHEMA library_book_schema);
+ERROR:  XML schema "library_book_schema" does not exist
+SELECT XMLVALIDATE(DOCUMENT '<book isbn="123"><title>Test</title><author><firstname>A</firstname><lastname>B</lastname></author><year>2024</year></book>'
+  ACCORDING TO XMLSCHEMA test_xmlschema_ns.library_book_schema);
+                                                         xmlvalidate                                                         
+-----------------------------------------------------------------------------------------------------------------------------
+ <book isbn="123"><title>Test</title><author><firstname>A</firstname><lastname>B</lastname></author><year>2024</year></book>
+(1 row)
+
+CREATE ROLE regress_xmlschema_test_role;
+ALTER XMLSCHEMA test_xmlschema_ns.library_book_schema OWNER TO regress_xmlschema_test_role;
+SELECT schemaname, schemanamespace::regnamespace, schemaowner::regrole
+FROM pg_xmlschema
+WHERE schemaname = 'library_book_schema';
+     schemaname      |  schemanamespace  |         schemaowner         
+---------------------+-------------------+-----------------------------
+ library_book_schema | test_xmlschema_ns | regress_xmlschema_test_role
+(1 row)
+
+CREATE VIEW book_view AS
+  SELECT XMLVALIDATE(DOCUMENT '<book isbn="456"><title>Dep Test</title><author><firstname>X</firstname><lastname>Y</lastname></author><year>2025</year></book>'
+    ACCORDING TO XMLSCHEMA test_xmlschema_ns.library_book_schema) AS validated_book;
+DROP XMLSCHEMA test_xmlschema_ns.library_book_schema;
+ERROR:  cannot drop XML schema test_xmlschema_ns.library_book_schema because other objects depend on it
+DETAIL:  view book_view depends on XML schema test_xmlschema_ns.library_book_schema
+HINT:  Use DROP ... CASCADE to drop the dependent objects too.
+DROP XMLSCHEMA test_xmlschema_ns.library_book_schema CASCADE;
+NOTICE:  drop cascades to view book_view
+SELECT * FROM book_view;
+ERROR:  relation "book_view" does not exist
+LINE 1: SELECT * FROM book_view;
+                      ^
+DROP XMLSCHEMA person_schema;
+DROP XMLSCHEMA IF EXISTS person_schema;
+NOTICE:  XML schema "person_schema" does not exist, skipping
+DROP XMLSCHEMA person_schema;
+ERROR:  XML schema "person_schema" does not exist
+DROP XMLSCHEMA test_xmlschema_ns.product_schema;
+SET ROLE regress_xmlschema_test_role;
+CREATE XMLSCHEMA public.should_fail_schema AS '<?xml version="1.0"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+  <xs:element name="test" type="xs:string"/>
+</xs:schema>';
+RESET ROLE;
+GRANT CREATE ON SCHEMA test_xmlschema_ns TO regress_xmlschema_test_role;
+SET ROLE regress_xmlschema_test_role;
+CREATE XMLSCHEMA test_xmlschema_ns.role_schema AS '<?xml version="1.0"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+  <xs:element name="test" type="xs:string"/>
+</xs:schema>';
+RESET ROLE;
+DROP XMLSCHEMA test_xmlschema_ns.role_schema;
+DROP ROLE regress_xmlschema_test_role;
+ERROR:  role "regress_xmlschema_test_role" cannot be dropped because some objects depend on it
+DETAIL:  privileges for schema test_xmlschema_ns
+owner of XML schema public.should_fail_schema
+DROP SCHEMA test_xmlschema_ns CASCADE;
+CREATE ROLE regress_xmlschema_user1;
+CREATE ROLE regress_xmlschema_user2;
+CREATE XMLSCHEMA permission_test_schema AS '<?xml version="1.0"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+  <xs:element name="test" type="xs:string"/>
+</xs:schema>';
+SET ROLE regress_xmlschema_user1;
+SELECT XMLVALIDATE(DOCUMENT '<test>data</test>'
+                   ACCORDING TO XMLSCHEMA permission_test_schema);
+ERROR:  permission denied for XML schema permission_test_schema
+RESET ROLE;
+GRANT USAGE ON XMLSCHEMA permission_test_schema TO regress_xmlschema_user1;
+SET ROLE regress_xmlschema_user1;
+SELECT XMLVALIDATE(DOCUMENT '<test>data</test>'
+                   ACCORDING TO XMLSCHEMA permission_test_schema);
+    xmlvalidate    
+-------------------
+ <test>data</test>
+(1 row)
+
+RESET ROLE;
+SET ROLE regress_xmlschema_user2;
+SELECT XMLVALIDATE(DOCUMENT '<test>data</test>'
+                   ACCORDING TO XMLSCHEMA permission_test_schema);
+ERROR:  permission denied for XML schema permission_test_schema
+RESET ROLE;
+GRANT USAGE ON XMLSCHEMA permission_test_schema TO regress_xmlschema_user2;
+SET ROLE regress_xmlschema_user2;
+SELECT XMLVALIDATE(DOCUMENT '<test>data</test>'
+                   ACCORDING TO XMLSCHEMA permission_test_schema);
+    xmlvalidate    
+-------------------
+ <test>data</test>
+(1 row)
+
+RESET ROLE;
+REVOKE USAGE ON XMLSCHEMA permission_test_schema FROM regress_xmlschema_user1;
+SET ROLE regress_xmlschema_user1;
+SELECT XMLVALIDATE(DOCUMENT '<test>data</test>'
+                   ACCORDING TO XMLSCHEMA permission_test_schema);
+ERROR:  permission denied for XML schema permission_test_schema
+RESET ROLE;
+DROP XMLSCHEMA permission_test_schema;
+DROP ROLE regress_xmlschema_user1;
+DROP ROLE regress_xmlschema_user2;
+CREATE TABLE validated_xml_data (
+  id serial PRIMARY KEY,
+  data xml
+);
+CREATE XMLSCHEMA data_schema AS '<?xml version="1.0"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+  <xs:element name="data">
+    <xs:complexType>
+      <xs:sequence>
+        <xs:element name="value" type="xs:integer"/>
+      </xs:sequence>
+    </xs:complexType>
+  </xs:element>
+</xs:schema>';
+INSERT INTO validated_xml_data (data)
+VALUES (XMLVALIDATE(DOCUMENT '<data><value>42</value></data>'
+  ACCORDING TO XMLSCHEMA data_schema));
+INSERT INTO validated_xml_data (data)
+VALUES (XMLVALIDATE(DOCUMENT '<data><value>100</value></data>'
+  ACCORDING TO XMLSCHEMA data_schema));
+SELECT id, data FROM validated_xml_data ORDER BY id;
+ id |              data               
+----+---------------------------------
+  1 | <data><value>42</value></data>
+  2 | <data><value>100</value></data>
+(2 rows)
+
+INSERT INTO validated_xml_data (data)
+VALUES (XMLVALIDATE(DOCUMENT '<data><value>not-an-int</value></data>'
+  ACCORDING TO XMLSCHEMA data_schema));
+ERROR:  XML validation failed
+DROP TABLE validated_xml_data;
+DROP XMLSCHEMA data_schema;
+DROP XMLSCHEMA should_fail_schema;
+DROP ROLE regress_xmlschema_test_role;
diff --git a/src/test/regress/sql/xml.sql b/src/test/regress/sql/xml.sql
index 0ea4f508837..b60cdcf4738 100644
--- a/src/test/regress/sql/xml.sql
+++ b/src/test/regress/sql/xml.sql
@@ -679,3 +679,277 @@ SELECT xmltext('  ');
 SELECT xmltext('foo `$_-+?=*^%!|/\()[]{}');
 SELECT xmltext('foo & <"bar">');
 SELECT xmltext('x'|| '<P>73</P>'::xml || .42 || true || 'j'::char);
+
+CREATE XMLSCHEMA person_schema AS '<?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>';
+
+
+CREATE XMLSCHEMA IF NOT EXISTS person_schema AS '<?xml version="1.0"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+  <xs:element name="other" type="xs:string"/>
+</xs:schema>';
+
+CREATE XMLSCHEMA person_schema AS '<?xml version="1.0"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+  <xs:element name="duplicate" type="xs:string"/>
+</xs:schema>';
+
+CREATE SCHEMA test_xmlschema_ns;
+
+CREATE XMLSCHEMA test_xmlschema_ns.product_schema AS '<?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>';
+
+CREATE XMLSCHEMA bad_schema AS '<this-is-not-valid-xsd>';
+
+CREATE XMLSCHEMA bad_xml_schema AS 'not even xml';
+
+CREATE XMLSCHEMA book_schema AS '<?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:element name="author" maxOccurs="unbounded">
+          <xs:complexType>
+            <xs:sequence>
+              <xs:element name="firstname" type="xs:string"/>
+              <xs:element name="lastname" type="xs:string"/>
+            </xs:sequence>
+          </xs:complexType>
+        </xs:element>
+        <xs:element name="year" type="xs:integer"/>
+      </xs:sequence>
+      <xs:attribute name="isbn" type="xs:string" use="required"/>
+    </xs:complexType>
+  </xs:element>
+</xs:schema>';
+
+SELECT XMLVALIDATE(DOCUMENT '<person><name>John</name><age>30</age></person>'
+  ACCORDING TO XMLSCHEMA person_schema);
+
+SELECT XMLVALIDATE(DOCUMENT '<product id="P123"><name>Widget</name><price>9.99</price></product>'
+  ACCORDING TO XMLSCHEMA test_xmlschema_ns.product_schema);
+
+SELECT XMLVALIDATE(DOCUMENT
+  '<book isbn="978-0-123456-78-9">
+    <title>PostgreSQL Internals</title>
+    <author><firstname>John</firstname><lastname>Titor</lastname></author>
+    <author><firstname>Jane</firstname></author>
+    <year>2024</year>
+  </book>'
+  ACCORDING TO XMLSCHEMA book_schema);
+
+SELECT XMLSERIALIZE(DOCUMENT
+  XMLVALIDATE(DOCUMENT '<person><name>Alice</name><age>25</age></person>'
+    ACCORDING TO XMLSCHEMA person_schema) AS text);
+
+SELECT XMLVALIDATE(DOCUMENT NULL ACCORDING TO XMLSCHEMA person_schema);
+
+SELECT XMLVALIDATE(DOCUMENT NULL ACCORDING TO XMLSCHEMA person_schema) IS NULL AS is_null;
+
+SELECT XMLVALIDATE(DOCUMENT '<person><name>John</name></person>'
+  ACCORDING TO XMLSCHEMA person_schema);
+
+SELECT XMLVALIDATE(DOCUMENT '<person><name>John</name><age>not-a-number</age></person>'
+  ACCORDING TO XMLSCHEMA person_schema);
+
+SELECT XMLVALIDATE(DOCUMENT '<product><name>Widget</name><price>9.99</price></product>'
+  ACCORDING TO XMLSCHEMA test_xmlschema_ns.product_schema);
+
+SELECT XMLVALIDATE(DOCUMENT '<person><name>John</name><age>30</age><extra>data</extra></person>'
+  ACCORDING TO XMLSCHEMA person_schema);
+
+SELECT XMLVALIDATE(DOCUMENT '<notperson><name>John</name><age>30</age></notperson>'
+  ACCORDING TO XMLSCHEMA person_schema);
+
+SELECT XMLVALIDATE(DOCUMENT '<test>value</test>'
+  ACCORDING TO XMLSCHEMA nonexistent_schema);
+
+CREATE VIEW validated_people AS
+  SELECT XMLVALIDATE(DOCUMENT data ACCORDING TO XMLSCHEMA person_schema) AS validated_xml
+  FROM xmltest WHERE id = 1;
+
+SELECT pg_get_viewdef('validated_people'::regclass, true);
+
+DROP VIEW validated_people;
+
+CREATE VIEW validated_products AS
+  SELECT XMLVALIDATE(DOCUMENT '<product id="123"><name>Test</name><price>1.99</price></product>'
+    ACCORDING TO XMLSCHEMA test_xmlschema_ns.product_schema) AS validated_xml;
+
+SELECT pg_get_viewdef('validated_products'::regclass, true);
+
+DROP VIEW validated_products;
+
+ALTER XMLSCHEMA book_schema RENAME TO library_book_schema;
+
+SELECT XMLVALIDATE(DOCUMENT '<book isbn="123"><title>Test</title><author><firstname>A</firstname><lastname>B</lastname></author><year>2024</year></book>'
+  ACCORDING TO XMLSCHEMA book_schema);
+
+SELECT XMLVALIDATE(DOCUMENT '<book isbn="123"><title>Test</title><author><firstname>A</firstname><lastname>B</lastname></author><year>2024</year></book>'
+  ACCORDING TO XMLSCHEMA library_book_schema);
+
+ALTER XMLSCHEMA library_book_schema SET SCHEMA test_xmlschema_ns;
+
+SELECT XMLVALIDATE(DOCUMENT '<book isbn="123"><title>Test</title><author><firstname>A</firstname><lastname>B</lastname></author><year>2024</year></book>'
+  ACCORDING TO XMLSCHEMA library_book_schema);
+
+SELECT XMLVALIDATE(DOCUMENT '<book isbn="123"><title>Test</title><author><firstname>A</firstname><lastname>B</lastname></author><year>2024</year></book>'
+  ACCORDING TO XMLSCHEMA test_xmlschema_ns.library_book_schema);
+
+CREATE ROLE regress_xmlschema_test_role;
+
+ALTER XMLSCHEMA test_xmlschema_ns.library_book_schema OWNER TO regress_xmlschema_test_role;
+
+SELECT schemaname, schemanamespace::regnamespace, schemaowner::regrole
+FROM pg_xmlschema
+WHERE schemaname = 'library_book_schema';
+
+CREATE VIEW book_view AS
+  SELECT XMLVALIDATE(DOCUMENT '<book isbn="456"><title>Dep Test</title><author><firstname>X</firstname><lastname>Y</lastname></author><year>2025</year></book>'
+    ACCORDING TO XMLSCHEMA test_xmlschema_ns.library_book_schema) AS validated_book;
+
+DROP XMLSCHEMA test_xmlschema_ns.library_book_schema;
+
+DROP XMLSCHEMA test_xmlschema_ns.library_book_schema CASCADE;
+
+SELECT * FROM book_view;
+
+DROP XMLSCHEMA person_schema;
+
+DROP XMLSCHEMA IF EXISTS person_schema;
+
+DROP XMLSCHEMA person_schema;
+
+DROP XMLSCHEMA test_xmlschema_ns.product_schema;
+
+SET ROLE regress_xmlschema_test_role;
+
+CREATE XMLSCHEMA public.should_fail_schema AS '<?xml version="1.0"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+  <xs:element name="test" type="xs:string"/>
+</xs:schema>';
+
+RESET ROLE;
+
+GRANT CREATE ON SCHEMA test_xmlschema_ns TO regress_xmlschema_test_role;
+
+SET ROLE regress_xmlschema_test_role;
+
+CREATE XMLSCHEMA test_xmlschema_ns.role_schema AS '<?xml version="1.0"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+  <xs:element name="test" type="xs:string"/>
+</xs:schema>';
+
+RESET ROLE;
+
+DROP XMLSCHEMA test_xmlschema_ns.role_schema;
+
+DROP ROLE regress_xmlschema_test_role;
+
+DROP SCHEMA test_xmlschema_ns CASCADE;
+
+CREATE ROLE regress_xmlschema_user1;
+CREATE ROLE regress_xmlschema_user2;
+
+CREATE XMLSCHEMA permission_test_schema AS '<?xml version="1.0"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+  <xs:element name="test" type="xs:string"/>
+</xs:schema>';
+
+SET ROLE regress_xmlschema_user1;
+SELECT XMLVALIDATE(DOCUMENT '<test>data</test>'
+                   ACCORDING TO XMLSCHEMA permission_test_schema);
+
+RESET ROLE;
+
+GRANT USAGE ON XMLSCHEMA permission_test_schema TO regress_xmlschema_user1;
+
+SET ROLE regress_xmlschema_user1;
+SELECT XMLVALIDATE(DOCUMENT '<test>data</test>'
+                   ACCORDING TO XMLSCHEMA permission_test_schema);
+
+RESET ROLE;
+
+SET ROLE regress_xmlschema_user2;
+SELECT XMLVALIDATE(DOCUMENT '<test>data</test>'
+                   ACCORDING TO XMLSCHEMA permission_test_schema);
+
+RESET ROLE;
+
+GRANT USAGE ON XMLSCHEMA permission_test_schema TO regress_xmlschema_user2;
+
+SET ROLE regress_xmlschema_user2;
+SELECT XMLVALIDATE(DOCUMENT '<test>data</test>'
+                   ACCORDING TO XMLSCHEMA permission_test_schema);
+
+RESET ROLE;
+
+REVOKE USAGE ON XMLSCHEMA permission_test_schema FROM regress_xmlschema_user1;
+
+SET ROLE regress_xmlschema_user1;
+SELECT XMLVALIDATE(DOCUMENT '<test>data</test>'
+                   ACCORDING TO XMLSCHEMA permission_test_schema);
+
+RESET ROLE;
+
+DROP XMLSCHEMA permission_test_schema;
+DROP ROLE regress_xmlschema_user1;
+DROP ROLE regress_xmlschema_user2;
+
+CREATE TABLE validated_xml_data (
+  id serial PRIMARY KEY,
+  data xml
+);
+
+CREATE XMLSCHEMA data_schema AS '<?xml version="1.0"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+  <xs:element name="data">
+    <xs:complexType>
+      <xs:sequence>
+        <xs:element name="value" type="xs:integer"/>
+      </xs:sequence>
+    </xs:complexType>
+  </xs:element>
+</xs:schema>';
+
+INSERT INTO validated_xml_data (data)
+VALUES (XMLVALIDATE(DOCUMENT '<data><value>42</value></data>'
+  ACCORDING TO XMLSCHEMA data_schema));
+
+INSERT INTO validated_xml_data (data)
+VALUES (XMLVALIDATE(DOCUMENT '<data><value>100</value></data>'
+  ACCORDING TO XMLSCHEMA data_schema));
+
+SELECT id, data FROM validated_xml_data ORDER BY id;
+
+INSERT INTO validated_xml_data (data)
+VALUES (XMLVALIDATE(DOCUMENT '<data><value>not-an-int</value></data>'
+  ACCORDING TO XMLSCHEMA data_schema));
+
+DROP TABLE validated_xml_data;
+
+DROP XMLSCHEMA data_schema;
+
+DROP XMLSCHEMA should_fail_schema;
+
+DROP ROLE regress_xmlschema_test_role;
-- 
2.51.2

