public inbox for [email protected]  
help / color / mirror / Atom feed
Re: WIP - xmlvalidate implementation from TODO list
9+ messages / 2 participants
[nested] [flat]

* Re: WIP - xmlvalidate implementation from TODO list
@ 2026-01-23 12:19 Jim Jones <[email protected]>
  2026-01-23 15:43 ` Re: WIP - xmlvalidate implementation from TODO list Jim Jones <[email protected]>
  0 siblings, 1 reply; 9+ messages in thread

From: Jim Jones @ 2026-01-23 12:19 UTC (permalink / raw)
  To: Marcos Magueta <[email protected]>; +Cc: Andrey Borodin <[email protected]>; Kirill Reshke <[email protected]>; PostgreSQL Hackers <[email protected]>



On 21/01/2026 21:44, Marcos Magueta wrote:
>> Any particular reason for that? If not, take a look at other options,
> e.g. a_expr
> No particular reason apart from it being simpler since I didn't need to
> invoke an execution at the cmd. Changed it now.
> 
>> Why did you choose text over xml for schemadata?
> My original thought was that XML schemas require additional validation
> in contrast to normal XML, but it being additive, we would have
> redundant checks. But in reconsideration, perhaps keeping the field with
> an XML type is more intuitive for anyone introspecting over the catalog.
> Also applied the change on the latest version of the patch.

Data type for schemadata in pg_xmlschema is now xml.

postgres=# \d pg_xmlschema
               Table "pg_catalog.pg_xmlschema"
     Column      |   Type    | Collation | Nullable | Default
-----------------+-----------+-----------+----------+---------
 oid             | oid       |           | not null |
 schemaname      | name      |           | not null |
 schemanamespace | oid       |           | not null |
 schemaowner     | oid       |           | not null |
 schemadata      | xml       |           | not null |
 schemaacl       | aclitem[] |           |          |
Indexes:
    "pg_xmlschema_oid_index" PRIMARY KEY, btree (oid)
    "pg_xmlschema_name_nsp_index" UNIQUE CONSTRAINT, btree (schemaname,
schemanamespace)

I agree it's more intuitive this way. It also facilitates function calls
that require the parameter to be xml, e.g. xmlserialize

postgres=# CREATE XMLSCHEMA x AS
 '<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"><xs:element
name="duplicate" type="xs:string"/></xs:schema>';
CREATE XMLSCHEMA

postgres=# SELECT xmlserialize(DOCUMENT schemadata AS text INDENT) FROM
pg_xmlschema;
                      xmlserialize
---------------------------------------------------------
 <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">+
   <xs:element name="duplicate" type="xs:string"/>      +
 </xs:schema>
(1 row)


> I noticed DefineXmlSchema() calls IsThereXmlSchemaInNamespace() right
> after XmlSchemaCreate() returns a valid OID. Since XmlSchemaCreate()
> already inserted the tuple into the catalog (via CatalogTupleInsert at
> pg_xmlschema.c:166), wouldn't SearchSysCacheExists2() find it and always
> throw "already exists"? We all tested the original code and it worked
> fine, so I'm missing something about syscache visibility or timing; that
> was an early function I did to check for duplicates that ended up in the
> wrong place. I removed the call (and function) as I judged it to be
> redundant (the duplicate check already happens inside
> XmlSchemaCreate()), but is there something subtle about intra-command
> visibility I'm not understanding? If anyone knows, please let me know.

I couldn't find any IsThereXmlSchemaInNamespace call in DefineXmlSchema
in the current version, so I cannot say much here. But I agree that the
a further check is not necessary, since XmlSchemaCreate is already doing it.

> Also, I added tab completion on psql and fixed pg_dump.

Nice. pg_dump now exports CREATE XMLSCHEMA statements.

Tab completion for CREATE, ALTER, and DROP XMLSCHEMA now also works.

A few other comments

== patch version ==

You forgot to include the version to the patch name.

For instance, instead of
0001-Add-CREATE-ALTER-DROP-XMLSCHEMA-DDL-commands.patch the file could
be named v3-0001-Add-CREATE-ALTER-DROP-XMLSCHEMA-DDL-commands.patch

== IS_XMLVALIDATE dependency ==

The patches 0001, 0002, and 0003 depend on IS_XMLVALIDATE, which is only
introduced in 0004, so they cannot be compiled and tested independently.

== permissions ==

In the tests I see you added a few GRANTs to set the visibility of
certain xmlschemas:

GRANT USAGE ON XMLSCHEMA permission_test_schema TO regress_xmlschema_user2

I could not find anything regarding this in the docs. If we are to
support it, shouldn't we add it to grant.sgml?

As I mentioned upthread, I believe that schema registration and usage
should be privilege-controlled, for example via dedicated roles

GRANT pg_read_xmlschemas TO u;
GRANT pg_write_xmlschemas TO u;

What do you think?

But being able to grant or revoke access to a certain xmlschema also has
its appeal :)

Best, Jim







^ permalink  raw  reply  [nested|flat] 9+ messages in thread

* Re: WIP - xmlvalidate implementation from TODO list
  2026-01-23 12:19 Re: WIP - xmlvalidate implementation from TODO list Jim Jones <[email protected]>
@ 2026-01-23 15:43 ` Jim Jones <[email protected]>
  2026-02-05 08:42   ` Re: WIP - xmlvalidate implementation from TODO list Jim Jones <[email protected]>
  0 siblings, 1 reply; 9+ messages in thread

From: Jim Jones @ 2026-01-23 15:43 UTC (permalink / raw)
  To: Marcos Magueta <[email protected]>; +Cc: Andrey Borodin <[email protected]>; Kirill Reshke <[email protected]>; PostgreSQL Hackers <[email protected]>



On 23/01/2026 13:19, Jim Jones wrote:
> On 21/01/2026 21:44, Marcos Magueta wrote:
>>> Any particular reason for that? If not, take a look at other options,
>> e.g. a_expr
>> No particular reason apart from it being simpler since I didn't need to
>> invoke an execution at the cmd. Changed it now.

On second thought, is there any scenario where we'll need a_expr for "y"
at all in "CREATE XMLSCHEMA x AS y"? Isn't it always going to be a
string? I see now that my example in the previous post was somewhat
misleading (sorry for the noise).

Best, Jim






^ permalink  raw  reply  [nested|flat] 9+ messages in thread

* Re: WIP - xmlvalidate implementation from TODO list
  2026-01-23 12:19 Re: WIP - xmlvalidate implementation from TODO list Jim Jones <[email protected]>
  2026-01-23 15:43 ` Re: WIP - xmlvalidate implementation from TODO list Jim Jones <[email protected]>
@ 2026-02-05 08:42   ` Jim Jones <[email protected]>
  2026-02-05 21:18     ` Re: WIP - xmlvalidate implementation from TODO list Marcos Magueta <[email protected]>
  0 siblings, 1 reply; 9+ messages in thread

From: Jim Jones @ 2026-02-05 08:42 UTC (permalink / raw)
  To: Marcos Magueta <[email protected]>; +Cc: Andrey Borodin <[email protected]>; Kirill Reshke <[email protected]>; PostgreSQL Hackers <[email protected]>

Hi Marcos,

I had some bandwidth this morning and revisited this patch. After
rebasing it, I realised that the interdependencies between the patch
files made it quite difficult to apply and test them individually. To
simplify things for now, I've split the patches into two separate files:

0001 - CREATE XMLSCHEMA
0002 - XMLVALIDATE

During the process, I also addressed several issues highlighted by the
CFbot[1]

I'd suggest to focus on 0001 for now. Feel free to revert it to the
state of v4 if you disagree with the current approach.

Best, Jim

1 - https://cirrus-ci.com/github/postgresql-cfbot/postgresql/cf%2F6372

Attachments:

  [text/x-patch] v5-0001-Add-CREATE-XMLSCHEMA.patch (77.1K, 2-v5-0001-Add-CREATE-XMLSCHEMA.patch)
  download | inline diff:
From 2a931611c344eb47ad75ae42034e644a600f97a6 Mon Sep 17 00:00:00 2001
From: Jim Jones <[email protected]>
Date: Wed, 4 Feb 2026 20:47:49 +0100
Subject: [PATCH v5 1/2] Add CREATE XMLSCHEMA

---
 doc/src/sgml/datatype.sgml             |   1 +
 doc/src/sgml/ref/allfiles.sgml         |   3 +
 doc/src/sgml/ref/alter_xmlschema.sgml  | 145 ++++++++++++++++++
 doc/src/sgml/ref/create_xmlschema.sgml | 182 +++++++++++++++++++++++
 doc/src/sgml/ref/drop_xmlschema.sgml   | 120 +++++++++++++++
 doc/src/sgml/reference.sgml            |   3 +
 src/backend/catalog/Makefile           |   1 +
 src/backend/catalog/aclchk.c           |  17 +++
 src/backend/catalog/dependency.c       |   3 +
 src/backend/catalog/meson.build        |   1 +
 src/backend/catalog/namespace.c        |  56 +++++++
 src/backend/catalog/objectaddress.c    |  84 +++++++++++
 src/backend/catalog/pg_xmlschema.c     | 194 +++++++++++++++++++++++++
 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   |  95 ++++++++++++
 src/backend/parser/gram.y              |  64 +++++++-
 src/backend/tcop/utility.c             |  17 +++
 src/backend/utils/adt/acl.c            |   4 +
 src/bin/pg_dump/common.c               |   3 +
 src/bin/pg_dump/pg_backup_archiver.c   |   1 +
 src/bin/pg_dump/pg_dump.c              | 128 ++++++++++++++++
 src/bin/pg_dump/pg_dump.h              |   8 +
 src/bin/pg_dump/pg_dump_sort.c         |   5 +
 src/bin/psql/tab-complete.in.c         |  22 +++
 src/include/catalog/Makefile           |   1 +
 src/include/catalog/meson.build        |   1 +
 src/include/catalog/namespace.h        |   1 +
 src/include/catalog/pg_xmlschema.h     |  48 ++++++
 src/include/commands/xmlschemacmds.h   |   9 ++
 src/include/nodes/parsenodes.h         |   1 +
 src/include/parser/kwlist.h            |   1 +
 src/include/tcop/cmdtaglist.h          |   3 +
 src/include/utils/acl.h                |   1 +
 src/test/regress/expected/oidjoins.out |   2 +
 src/test/regress/expected/xml.out      |  64 ++++++++
 src/test/regress/expected/xml_1.out    |  61 ++++++++
 src/test/regress/expected/xml_2.out    |  78 ++++++++++
 src/test/regress/pg_regress.c          |   2 +-
 src/test/regress/sql/xml.sql           |  63 ++++++++
 44 files changed, 1512 insertions(+), 3 deletions(-)
 create mode 100644 doc/src/sgml/ref/alter_xmlschema.sgml
 create mode 100644 doc/src/sgml/ref/create_xmlschema.sgml
 create mode 100644 doc/src/sgml/ref/drop_xmlschema.sgml
 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/datatype.sgml b/doc/src/sgml/datatype.sgml
index 3017c67404..f2ade57fe7 100644
--- a/doc/src/sgml/datatype.sgml
+++ b/doc/src/sgml/datatype.sgml
@@ -4649,6 +4649,7 @@ SET xmloption TO { DOCUMENT | CONTENT };
     distribution.
    </para>
    </sect2>
+
   </sect1>
 
   &json;
diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index e167406c74..87a8e6066e 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -48,6 +48,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY alterUser          SYSTEM "alter_user.sgml">
 <!ENTITY alterUserMapping   SYSTEM "alter_user_mapping.sgml">
 <!ENTITY alterView          SYSTEM "alter_view.sgml">
+<!ENTITY alterXmlSchema     SYSTEM "alter_xmlschema.sgml">
 <!ENTITY analyze            SYSTEM "analyze.sgml">
 <!ENTITY begin              SYSTEM "begin.sgml">
 <!ENTITY call               SYSTEM "call.sgml">
@@ -100,6 +101,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY createUser         SYSTEM "create_user.sgml">
 <!ENTITY createUserMapping  SYSTEM "create_user_mapping.sgml">
 <!ENTITY createView         SYSTEM "create_view.sgml">
+<!ENTITY createXmlSchema    SYSTEM "create_xmlschema.sgml">
 <!ENTITY deallocate         SYSTEM "deallocate.sgml">
 <!ENTITY declare            SYSTEM "declare.sgml">
 <!ENTITY delete             SYSTEM "delete.sgml">
@@ -148,6 +150,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY dropUser           SYSTEM "drop_user.sgml">
 <!ENTITY dropUserMapping    SYSTEM "drop_user_mapping.sgml">
 <!ENTITY dropView           SYSTEM "drop_view.sgml">
+<!ENTITY dropXmlSchema      SYSTEM "drop_xmlschema.sgml">
 <!ENTITY end                SYSTEM "end.sgml">
 <!ENTITY execute            SYSTEM "execute.sgml">
 <!ENTITY explain            SYSTEM "explain.sgml">
diff --git a/doc/src/sgml/ref/alter_xmlschema.sgml b/doc/src/sgml/ref/alter_xmlschema.sgml
new file mode 100644
index 0000000000..6a286ad00f
--- /dev/null
+++ b/doc/src/sgml/ref/alter_xmlschema.sgml
@@ -0,0 +1,145 @@
+<!--
+doc/src/sgml/ref/alter_xmlschema.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-alterxmlschema">
+ <indexterm zone="sql-alterxmlschema">
+  <primary>ALTER XMLSCHEMA</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>ALTER XMLSCHEMA</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>ALTER XMLSCHEMA</refname>
+  <refpurpose>change the definition of an XML schema</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+ALTER XMLSCHEMA [ IF EXISTS ] <replaceable class="parameter">name</replaceable> OWNER TO { <replaceable class="parameter">new_owner</replaceable> | CURRENT_ROLE | CURRENT_USER | SESSION_USER }
+ALTER XMLSCHEMA [ IF EXISTS ] <replaceable class="parameter">name</replaceable> RENAME TO <replaceable class="parameter">new_name</replaceable>
+ALTER XMLSCHEMA [ IF EXISTS ] <replaceable class="parameter">name</replaceable> SET SCHEMA <replaceable class="parameter">new_schema</replaceable>
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>ALTER XMLSCHEMA</command> changes the definition of an XML schema.
+  </para>
+
+  <para>
+   You must own the XML schema to use <command>ALTER XMLSCHEMA</command>.
+   To change an XML schema's schema, you must also have <literal>CREATE</literal>
+   privilege on the new schema.
+   To alter the owner, you must be able to <literal>SET ROLE</literal> to the
+   new owning role, and that role must have <literal>CREATE</literal>
+   privilege on the XML schema's schema.
+   (These restrictions enforce that altering the owner
+   doesn't do anything you couldn't do by dropping and recreating the XML schema.
+   However, a superuser can alter ownership of any XML schema anyway.)
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><replaceable class="parameter">name</replaceable></term>
+    <listitem>
+     <para>
+      The name (optionally schema-qualified) of an XML schema to be altered.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>IF EXISTS</literal></term>
+    <listitem>
+     <para>
+      Do not throw an error if the XML schema does not exist. A notice is issued
+      in this case.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_owner</replaceable></term>
+    <listitem>
+     <para>
+      The user name of the new owner of the XML schema.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_name</replaceable></term>
+    <listitem>
+     <para>
+      The new name of the XML schema.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_schema</replaceable></term>
+    <listitem>
+     <para>
+      The new schema for the XML schema.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To rename an XML schema:
+<programlisting>
+ALTER XMLSCHEMA person_schema RENAME TO people_schema;
+</programlisting>
+  </para>
+
+  <para>
+   To change the owner of an XML schema:
+<programlisting>
+ALTER XMLSCHEMA people_schema OWNER TO joe;
+</programlisting>
+  </para>
+
+  <para>
+   To move an XML schema to another schema:
+<programlisting>
+ALTER XMLSCHEMA people_schema SET SCHEMA myschema;
+</programlisting>
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   <command>ALTER XMLSCHEMA</command> is a
+   <productname>PostgreSQL</productname> extension.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-createxmlschema"/></member>
+   <member><xref linkend="sql-dropxmlschema"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/ref/create_xmlschema.sgml b/doc/src/sgml/ref/create_xmlschema.sgml
new file mode 100644
index 0000000000..573b9b7211
--- /dev/null
+++ b/doc/src/sgml/ref/create_xmlschema.sgml
@@ -0,0 +1,182 @@
+<!--
+doc/src/sgml/ref/create_xmlschema.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-createxmlschema">
+ <indexterm zone="sql-createxmlschema">
+  <primary>CREATE XMLSCHEMA</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>CREATE XMLSCHEMA</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>CREATE XMLSCHEMA</refname>
+  <refpurpose>define a new XML schema</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+CREATE XMLSCHEMA [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> AS <replaceable class="parameter">schema_definition</replaceable>
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>CREATE XMLSCHEMA</command> defines a new XML schema in the
+   current database. The schema definition is an XML Schema Definition (XSD)
+   document that can be used to validate XML documents using the
+   <function>XMLVALIDATE</function> function.
+  </para>
+
+  <para>
+   If a schema name is given then the XML schema is created in the
+   specified schema. Otherwise it is created in the current schema.
+   The XML schema name must be unique within the schema.
+  </para>
+
+  <para>
+   After an XML schema is created, you use the
+   <function>XMLVALIDATE</function> function to validate XML documents
+   against it.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><literal>IF NOT EXISTS</literal></term>
+    <listitem>
+     <para>
+      Do not throw an error if an XML schema with the same name already exists.
+      A notice is issued in this case. Note that there is no guarantee that
+      the existing XML schema is similar to the one that would have been created.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">name</replaceable></term>
+    <listitem>
+     <para>
+      The name (optionally schema-qualified) of the XML schema to be created.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">schema_definition</replaceable></term>
+    <listitem>
+     <para>
+      An XML Schema Definition (XSD) document provided as a string literal.
+      The schema definition must be a valid XSD document conforming to the
+      W3C XML Schema standard.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Notes</title>
+
+  <para>
+   To create an XML schema, you must have <literal>CREATE</literal> privilege
+   on the current schema.
+  </para>
+
+  <para>
+   Use <link linkend="sql-dropxmlschema"><command>DROP XMLSCHEMA</command></link> to remove an XML schema.
+  </para>
+
+  <para>
+   See also <link linkend="sql-alterxmlschema"><command>ALTER XMLSCHEMA</command></link>
+   to rename an XML schema or change its owner.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   Create an XML schema for validating person documents:
+<programlisting>
+CREATE XMLSCHEMA person_schema AS '&lt;?xml version="1.0"?&gt;
+&lt;xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"&gt;
+  &lt;xs:element name="person"&gt;
+    &lt;xs:complexType&gt;
+      &lt;xs:sequence&gt;
+        &lt;xs:element name="name" type="xs:string"/&gt;
+        &lt;xs:element name="age" type="xs:integer"/&gt;
+      &lt;/xs:sequence&gt;
+    &lt;/xs:complexType&gt;
+  &lt;/xs:element&gt;
+&lt;/xs:schema&gt;';
+</programlisting>
+  </para>
+
+  <para>
+   Create an XML schema with a schema-qualified name:
+<programlisting>
+CREATE SCHEMA myschema;
+CREATE XMLSCHEMA myschema.product_schema AS '&lt;?xml version="1.0"?&gt;
+&lt;xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"&gt;
+  &lt;xs:element name="product"&gt;
+    &lt;xs:complexType&gt;
+      &lt;xs:sequence&gt;
+        &lt;xs:element name="name" type="xs:string"/&gt;
+        &lt;xs:element name="price" type="xs:decimal"/&gt;
+      &lt;/xs:sequence&gt;
+      &lt;xs:attribute name="id" type="xs:string" use="required"/&gt;
+    &lt;/xs:complexType&gt;
+  &lt;/xs:element&gt;
+&lt;/xs:schema&gt;';
+</programlisting>
+  </para>
+
+  <para>
+   Create an XML schema only if it doesn't already exist:
+<programlisting>
+CREATE XMLSCHEMA IF NOT EXISTS person_schema AS '...';
+</programlisting>
+  </para>
+
+  <para>
+   Validate an XML document against the schema:
+<programlisting>
+SELECT XMLVALIDATE(
+  DOCUMENT '&lt;person&gt;&lt;name&gt;John&lt;/name&gt;&lt;age&gt;30&lt;/age&gt;&lt;/person&gt;'
+  ACCORDING TO XMLSCHEMA person_schema
+);
+</programlisting>
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   <command>CREATE XMLSCHEMA</command> is a
+   <productname>PostgreSQL</productname> extension. There is no
+   <command>CREATE XMLSCHEMA</command> statement in the SQL standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-alterxmlschema"/></member>
+   <member><xref linkend="sql-dropxmlschema"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/ref/drop_xmlschema.sgml b/doc/src/sgml/ref/drop_xmlschema.sgml
new file mode 100644
index 0000000000..4204f9bc41
--- /dev/null
+++ b/doc/src/sgml/ref/drop_xmlschema.sgml
@@ -0,0 +1,120 @@
+<!--
+doc/src/sgml/ref/drop_xmlschema.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-dropxmlschema">
+ <indexterm zone="sql-dropxmlschema">
+  <primary>DROP XMLSCHEMA</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>DROP XMLSCHEMA</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>DROP XMLSCHEMA</refname>
+  <refpurpose>remove an XML schema</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+DROP XMLSCHEMA [ IF EXISTS ] <replaceable class="parameter">name</replaceable> [, ...] [ CASCADE | RESTRICT ]
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>DROP XMLSCHEMA</command> removes XML schemas from the database.
+   An XML schema can only be dropped by its owner or a superuser.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><literal>IF EXISTS</literal></term>
+    <listitem>
+     <para>
+      Do not throw an error if the XML schema does not exist. A notice is issued
+      in this case.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">name</replaceable></term>
+    <listitem>
+     <para>
+      The name (optionally schema-qualified) of an XML schema.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>CASCADE</literal></term>
+    <listitem>
+     <para>
+      Automatically drop objects that depend on the XML schema,
+      and in turn all objects that depend on those objects
+      (see <xref linkend="ddl-depend"/>).
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>RESTRICT</literal></term>
+    <listitem>
+     <para>
+      Refuse to drop the XML schema if any objects depend on it.  This
+      is the default.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To remove the XML schema <literal>person_schema</literal>:
+<programlisting>
+DROP XMLSCHEMA person_schema;
+</programlisting>
+  </para>
+
+  <para>
+   To remove an XML schema and all dependent objects:
+<programlisting>
+DROP XMLSCHEMA person_schema CASCADE;
+</programlisting>
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   <command>DROP XMLSCHEMA</command> is a
+   <productname>PostgreSQL</productname> extension. There is no
+   <command>DROP XMLSCHEMA</command> statement in the SQL standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-createxmlschema"/></member>
+   <member><xref linkend="sql-alterxmlschema"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index 2cf02c37b1..982bbea3a2 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -76,6 +76,7 @@
    &alterUser;
    &alterUserMapping;
    &alterView;
+   &alterXmlSchema;
    &analyze;
    &begin;
    &call;
@@ -128,6 +129,7 @@
    &createUser;
    &createUserMapping;
    &createView;
+   &createXmlSchema;
    &deallocate;
    &declare;
    &delete;
@@ -176,6 +178,7 @@
    &dropUser;
    &dropUserMapping;
    &dropView;
+   &dropXmlSchema;
    &end;
    &execute;
    &explain;
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index 26fa0c9b18..ed9414ba63 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 a431fc0926..c5198ea6ec 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 f89267f034..0b8d8e3152 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;
 
diff --git a/src/backend/catalog/meson.build b/src/backend/catalog/meson.build
index 11d21c5ad6..9ba15e71b7 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 c3b79a2ba4..89b124ceee 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 02af64b82c..6306b9c8f6 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 0000000000..116bbfa71c
--- /dev/null
+++ b/src/backend/catalog/pg_xmlschema.c
@@ -0,0 +1,194 @@
+#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,
+				xmltype *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;
+		char	   *schemastr;
+
+		schemastr = text_to_cstring(schemadata);
+		xmlerrcxt = pg_xml_init(PG_XML_STRICTNESS_WELLFORMED);
+
+		PG_TRY();
+		{
+			parser_ctxt = xmlSchemaNewMemParserCtxt(schemastr, strlen(schemastr));
+			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");
+
+			xmlSchemaFree(schema_ptr);
+			xmlSchemaFreeParserCtxt(parser_ctxt);
+		}
+		PG_CATCH();
+		{
+			pg_xml_done(xmlerrcxt, true);
+			PG_RE_THROW();
+		}
+		PG_END_TRY();
+
+		pg_xml_done(xmlerrcxt, false);
+		pfree(schemastr);
+	}
+#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] = PointerGetDatum(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 64cb627840..07f04eafda 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 08957104c7..0894387901 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"
@@ -134,6 +135,10 @@ report_namespace_conflict(Oid classId, const char *name, Oid nspOid)
 		case TSConfigRelationId:
 			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;
@@ -417,6 +422,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:
@@ -561,6 +567,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:
@@ -871,6 +878,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 92526012d2..2e893968ab 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 028f9e2de9..4a087935f1 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 ca3f53c621..3363797ece 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 4160f5b685..83c6cdd9bb 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 0000000000..43b1a56a10
--- /dev/null
+++ b/src/backend/commands/xmlschemacmds.c
@@ -0,0 +1,95 @@
+#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"
+#include "utils/xml.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;
+	xmltype    *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")));
+
+	/* The schema definition is passed as a String node from the grammar */
+	schemaData = (xmltype *) cstring_to_text(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;
+	}
+
+	ObjectAddressSet(address, XmlSchemaRelationId, newoid);
+
+	return address;
+}
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 713ee5c10a..9886250c25 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -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
 
 	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);
@@ -18227,6 +18287,7 @@ unreserved_keyword:
 			| WRAPPER
 			| WRITE
 			| XML_P
+			| XMLSCHEMA
 			| YEAR_P
 			| YES_P
 			| ZONE
@@ -18896,6 +18957,7 @@ bare_label_keyword:
 			| XMLPARSE
 			| XMLPI
 			| XMLROOT
+			| XMLSCHEMA
 			| XMLSERIALIZE
 			| XMLTABLE
 			| YES_P
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 34dd6e18df..60e0a0f7e7 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 3a6905f954..412a045043 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/bin/pg_dump/common.c b/src/bin/pg_dump/common.c
index 349b47c8e2..651cf67436 100644
--- a/src/bin/pg_dump/common.c
+++ b/src/bin/pg_dump/common.c
@@ -182,6 +182,9 @@ getSchemaData(Archive *fout, int *numTablesPtr)
 	pg_log_info("reading user-defined conversions");
 	getConversions(fout);
 
+	pg_log_info("reading XML schemas");
+	getXmlSchemas(fout);
+
 	pg_log_info("reading type casts");
 	getCasts(fout);
 
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 35d3a07915..c418870174 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3815,6 +3815,7 @@ _getObjectDescription(PQExpBuffer buf, const TocEntry *te)
 		strcmp(type, "TEXT SEARCH CONFIGURATION") == 0 ||
 		strcmp(type, "TYPE") == 0 ||
 		strcmp(type, "VIEW") == 0 ||
+		strcmp(type, "XMLSCHEMA") == 0 ||
 	/* non-schema-specified objects */
 		strcmp(type, "DATABASE") == 0 ||
 		strcmp(type, "PROCEDURAL LANGUAGE") == 0 ||
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 2bebefd0ba..7312a07c42 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -311,6 +311,7 @@ static void dumpAccessMethod(Archive *fout, const AccessMethodInfo *aminfo);
 static void dumpOpclass(Archive *fout, const OpclassInfo *opcinfo);
 static void dumpOpfamily(Archive *fout, const OpfamilyInfo *opfinfo);
 static void dumpCollation(Archive *fout, const CollInfo *collinfo);
+static void dumpXmlSchema(Archive *fout, const XmlSchemaInfo * xmlschemainfo);
 static void dumpConversion(Archive *fout, const ConvInfo *convinfo);
 static void dumpRule(Archive *fout, const RuleInfo *rinfo);
 static void dumpAgg(Archive *fout, const AggInfo *agginfo);
@@ -6576,6 +6577,62 @@ getConversions(Archive *fout)
 	destroyPQExpBuffer(query);
 }
 
+/*
+ * getXmlSchemas:
+ *	  get information about all XML schemas in the system catalogs
+ */
+void
+getXmlSchemas(Archive *fout)
+{
+	PGresult   *res;
+	int			ntups;
+	int			i;
+	PQExpBuffer query;
+	XmlSchemaInfo *xmlschemainfo;
+	int			i_tableoid;
+	int			i_oid;
+	int			i_schemaname;
+	int			i_schemanamespace;
+	int			i_schemaowner;
+
+	query = createPQExpBuffer();
+
+	appendPQExpBufferStr(query, "SELECT tableoid, oid, schemaname, "
+						 "schemanamespace, "
+						 "schemaowner "
+						 "FROM pg_xmlschema");
+
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+
+	ntups = PQntuples(res);
+
+	xmlschemainfo = (XmlSchemaInfo *) pg_malloc(ntups * sizeof(XmlSchemaInfo));
+
+	i_tableoid = PQfnumber(res, "tableoid");
+	i_oid = PQfnumber(res, "oid");
+	i_schemaname = PQfnumber(res, "schemaname");
+	i_schemanamespace = PQfnumber(res, "schemanamespace");
+	i_schemaowner = PQfnumber(res, "schemaowner");
+
+	for (i = 0; i < ntups; i++)
+	{
+		xmlschemainfo[i].dobj.objType = DO_XMLSCHEMA;
+		xmlschemainfo[i].dobj.catId.tableoid = atooid(PQgetvalue(res, i, i_tableoid));
+		xmlschemainfo[i].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
+		AssignDumpId(&xmlschemainfo[i].dobj);
+		xmlschemainfo[i].dobj.name = pg_strdup(PQgetvalue(res, i, i_schemaname));
+		xmlschemainfo[i].dobj.namespace =
+			findNamespace(atooid(PQgetvalue(res, i, i_schemanamespace)));
+		xmlschemainfo[i].rolname = getRoleName(PQgetvalue(res, i, i_schemaowner));
+
+		selectDumpableObject(&(xmlschemainfo[i].dobj), fout);
+	}
+
+	PQclear(res);
+
+	destroyPQExpBuffer(query);
+}
+
 /*
  * getAccessMethods:
  *	  get information about all user-defined access methods
@@ -11726,6 +11783,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_CONVERSION:
 			dumpConversion(fout, (const ConvInfo *) dobj);
 			break;
+		case DO_XMLSCHEMA:
+			dumpXmlSchema(fout, (const XmlSchemaInfo *) dobj);
+			break;
 		case DO_TABLE:
 			dumpTable(fout, (const TableInfo *) dobj);
 			break;
@@ -15252,6 +15312,73 @@ dumpCollation(Archive *fout, const CollInfo *collinfo)
 	free(qcollname);
 }
 
+/*
+ * dumpXmlSchema
+ *	  write out a single XML schema definition
+ */
+static void
+dumpXmlSchema(Archive *fout, const XmlSchemaInfo * xmlschemainfo)
+{
+	DumpOptions *dopt = fout->dopt;
+	PQExpBuffer query;
+	PQExpBuffer q;
+	PQExpBuffer delq;
+	char	   *qxmlschemaname;
+	PGresult   *res;
+	int			i_schemadata;
+	char	   *schemadata;
+
+	if (!dopt->dumpSchema)
+		return;
+
+	query = createPQExpBuffer();
+	q = createPQExpBuffer();
+	delq = createPQExpBuffer();
+
+	qxmlschemaname = pg_strdup(fmtId(xmlschemainfo->dobj.name));
+
+	appendPQExpBuffer(query,
+					  "SELECT schemadata "
+					  "FROM pg_catalog.pg_xmlschema "
+					  "WHERE oid = '%u'::pg_catalog.oid",
+					  xmlschemainfo->dobj.catId.oid);
+
+	res = ExecuteSqlQueryForSingleRow(fout, query->data);
+
+	i_schemadata = PQfnumber(res, "schemadata");
+	schemadata = PQgetvalue(res, 0, i_schemadata);
+
+	appendPQExpBuffer(delq, "DROP XMLSCHEMA %s;\n",
+					  fmtQualifiedDumpable(xmlschemainfo));
+
+	appendPQExpBuffer(q, "CREATE XMLSCHEMA %s AS ",
+					  fmtQualifiedDumpable(xmlschemainfo));
+	appendStringLiteralAH(q, schemadata, fout);
+	appendPQExpBufferStr(q, ";\n");
+
+	if (xmlschemainfo->dobj.dump & DUMP_COMPONENT_DEFINITION)
+		ArchiveEntry(fout, xmlschemainfo->dobj.catId, xmlschemainfo->dobj.dumpId,
+					 ARCHIVE_OPTS(.tag = xmlschemainfo->dobj.name,
+								  .namespace = xmlschemainfo->dobj.namespace->dobj.name,
+								  .owner = xmlschemainfo->rolname,
+								  .description = "XMLSCHEMA",
+								  .section = SECTION_PRE_DATA,
+								  .createStmt = q->data,
+								  .dropStmt = delq->data));
+
+	if (xmlschemainfo->dobj.dump & DUMP_COMPONENT_COMMENT)
+		dumpComment(fout, "XMLSCHEMA", qxmlschemaname,
+					xmlschemainfo->dobj.namespace->dobj.name, xmlschemainfo->rolname,
+					xmlschemainfo->dobj.catId, 0, xmlschemainfo->dobj.dumpId);
+
+	PQclear(res);
+
+	destroyPQExpBuffer(query);
+	destroyPQExpBuffer(q);
+	destroyPQExpBuffer(delq);
+	free(qxmlschemaname);
+}
+
 /*
  * dumpConversion
  *	  write out a single conversion definition
@@ -20422,6 +20549,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_OPFAMILY:
 			case DO_COLLATION:
 			case DO_CONVERSION:
+			case DO_XMLSCHEMA:
 			case DO_TABLE:
 			case DO_TABLE_ATTACH:
 			case DO_ATTRDEF:
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 4c4b14e5fc..bc1cf5a00d 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -50,6 +50,7 @@ typedef enum
 	DO_OPFAMILY,
 	DO_COLLATION,
 	DO_CONVERSION,
+	DO_XMLSCHEMA,
 	DO_TABLE,
 	DO_TABLE_ATTACH,
 	DO_ATTRDEF,
@@ -299,6 +300,12 @@ typedef struct _convInfo
 	const char *rolname;
 } ConvInfo;
 
+typedef struct _xmlSchemaInfo
+{
+	DumpableObject dobj;
+	const char *rolname;
+}			XmlSchemaInfo;
+
 typedef struct _tableInfo
 {
 	/*
@@ -797,6 +804,7 @@ extern void getOpclasses(Archive *fout);
 extern void getOpfamilies(Archive *fout);
 extern void getCollations(Archive *fout);
 extern void getConversions(Archive *fout);
+extern void getXmlSchemas(Archive *fout);
 extern TableInfo *getTables(Archive *fout, int *numTables);
 extern void getOwnedSeqs(Archive *fout, TableInfo tblinfo[], int numTables);
 extern InhInfo *getInherits(Archive *fout, int *numInherits);
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 24bed6681d..d028149afe 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -1575,6 +1575,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "CONVERSION %s  (ID %d OID %u)",
 					 obj->name, obj->dumpId, obj->catId.oid);
 			return;
+		case DO_XMLSCHEMA:
+			snprintf(buf, bufsize,
+					 "XMLSCHEMA %s  (ID %d OID %u)",
+					 obj->name, obj->dumpId, obj->catId.oid);
+			return;
 		case DO_TABLE:
 			snprintf(buf, bufsize,
 					 "TABLE %s  (ID %d OID %u)",
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 8b91bc0006..3e3d788829 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -797,6 +797,13 @@ static const SchemaQuery Query_for_list_of_matviews = {
 	.result = "c.relname",
 };
 
+static const SchemaQuery Query_for_list_of_xmlschemas = {
+	.catname = "pg_catalog.pg_xmlschema s",
+	.viscondition = "pg_catalog.has_schema_privilege(s.oid, 'USAGE')",
+	.namespace = "s.schemanamespace",
+	.result = "s.schemaname",
+};
+
 static const SchemaQuery Query_for_list_of_indexes = {
 	.catname = "pg_catalog.pg_class c",
 	.selcondition =
@@ -1364,6 +1371,7 @@ static const pgsql_thing_t words_after_create[] = {
 	{"USER", Query_for_list_of_roles, NULL, NULL, Keywords_for_user_thing},
 	{"USER MAPPING FOR", NULL, NULL, NULL},
 	{"VIEW", NULL, NULL, &Query_for_list_of_views},
+	{"XMLSCHEMA", NULL, NULL, &Query_for_list_of_xmlschemas},
 	{NULL}						/* end of list */
 };
 
@@ -2701,6 +2709,10 @@ match_previous_words(int pattern_id,
 	else if (Matches("ALTER", "MATERIALIZED", "VIEW", MatchAny, "SET", "ACCESS", "METHOD"))
 		COMPLETE_WITH_QUERY(Query_for_list_of_table_access_methods);
 
+	/* ALTER XMLSCHEMA <name> */
+	else if (Matches("ALTER", "XMLSCHEMA", MatchAny))
+		COMPLETE_WITH("OWNER TO", "RENAME TO", "SET SCHEMA");
+
 	/* ALTER POLICY <name> */
 	else if (Matches("ALTER", "POLICY"))
 		COMPLETE_WITH_QUERY(Query_for_list_of_policies);
@@ -4178,6 +4190,16 @@ match_previous_words(int pattern_id,
 			 Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "USING", MatchAny, "AS"))
 		COMPLETE_WITH("SELECT");
 
+/* CREATE XMLSCHEMA */
+	else if (Matches("CREATE", "XMLSCHEMA", MatchAny))
+		COMPLETE_WITH("AS", "IF");
+	else if (Matches("CREATE", "XMLSCHEMA", "IF"))
+		COMPLETE_WITH("NOT");
+	else if (Matches("CREATE", "XMLSCHEMA", "IF", "NOT"))
+		COMPLETE_WITH("EXISTS");
+	else if (Matches("CREATE", "XMLSCHEMA", "IF", "NOT", "EXISTS", MatchAny))
+		COMPLETE_WITH("AS");
+
 /* CREATE EVENT TRIGGER */
 	else if (Matches("CREATE", "EVENT"))
 		COMPLETE_WITH("TRIGGER");
diff --git a/src/include/catalog/Makefile b/src/include/catalog/Makefile
index c90022f7c5..dcb1d896d9 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 b63cd58406..5846919f62 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 1a25973685..a0f9585c53 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 0000000000..3261420f2b
--- /dev/null
+++ b/src/include/catalog/pg_xmlschema.h
@@ -0,0 +1,48 @@
+#ifndef PG_XMLSCHEMA_H
+#define PG_XMLSCHEMA_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_xmlschema_d.h"
+#include "utils/xml.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
+	/* The XSD itself */
+	xml			schemadata BKI_FORCE_NOT_NULL;
+
+	/* 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,
+							xmltype *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 0000000000..302cb791f3
--- /dev/null
+++ b/src/include/commands/xmlschemacmds.h
@@ -0,0 +1,9 @@
+#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);
+
+#endif							/* XMLSCHEMACMDS_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 646d6ced76..510b79a346 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/parser/kwlist.h b/src/include/parser/kwlist.h
index f7753c5c8a..a018c2b9e0 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -520,6 +520,7 @@ 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("year", YEAR_P, UNRESERVED_KEYWORD, AS_LABEL)
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index 1290c9bab6..1cd35452ac 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 ec01fd581c..e24427c966 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/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out
index 25aaae8d05..246deabd17 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 103a22a3b1..4e24657d2d 100644
--- a/src/test/regress/expected/xml.out
+++ b/src/test/regress/expected/xml.out
@@ -1881,3 +1881,67 @@ 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>';
diff --git a/src/test/regress/expected/xml_1.out b/src/test/regress/expected/xml_1.out
index 73c411118a..aded57c78b 100644
--- a/src/test/regress/expected/xml_1.out
+++ b/src/test/regress/expected/xml_1.out
@@ -1496,3 +1496,64 @@ ERROR:  unsupported XML feature
 LINE 1: SELECT xmltext('x'|| '<P>73</P>'::xml || .42 || true || 'j':...
                              ^
 DETAIL:  This functionality requires the server to be built with libxml support.
+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>';
+ERROR:  xmlschema support requires libxml
+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>';
+ERROR:  xmlschema support requires libxml
+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:  xmlschema support requires libxml
+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>';
+ERROR:  xmlschema support requires libxml
+CREATE XMLSCHEMA bad_schema AS '<this-is-not-valid-xsd>';
+ERROR:  xmlschema support requires libxml
+CREATE XMLSCHEMA bad_xml_schema AS 'not even xml';
+ERROR:  xmlschema support requires libxml
+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>';
+ERROR:  xmlschema support requires libxml
diff --git a/src/test/regress/expected/xml_2.out b/src/test/regress/expected/xml_2.out
index a85d95358d..4e24657d2d 100644
--- a/src/test/regress/expected/xml_2.out
+++ b/src/test/regress/expected/xml_2.out
@@ -9,6 +9,8 @@ ERROR:  invalid XML content
 LINE 1: INSERT INTO xmltest VALUES (3, '<wrong');
                                        ^
 DETAIL:  line 1: Couldn't find end of Start Tag wrong line 1
+<wrong
+      ^
 SELECT * FROM xmltest;
  id |        data        
 ----+--------------------
@@ -92,6 +94,8 @@ ERROR:  invalid XML content
 LINE 1: SELECT xmlconcat('bad', '<syntax');
                                 ^
 DETAIL:  line 1: Couldn't find end of Start Tag syntax line 1
+<syntax
+       ^
 SELECT xmlconcat('<foo/>', NULL, '<?xml version="1.1" standalone="no"?><bar/>');
   xmlconcat   
 --------------
@@ -273,6 +277,8 @@ DETAIL:  line 1: Entity 'idontexist' not defined
 <twoerrors>&idontexist;</unbalanced>
                        ^
 line 1: Opening and ending tag mismatch: twoerrors line 1 and unbalanced
+<twoerrors>&idontexist;</unbalanced>
+                                    ^
 SELECT xmlparse(content '<nosuchprefix:tag/>');
       xmlparse       
 ---------------------
@@ -282,6 +288,8 @@ SELECT xmlparse(content '<nosuchprefix:tag/>');
 SELECT xmlparse(document '   ');
 ERROR:  invalid XML document
 DETAIL:  line 1: Start tag expected, '<' not found
+   
+   ^
 SELECT xmlparse(document 'abc');
 ERROR:  invalid XML document
 DETAIL:  line 1: Start tag expected, '<' not found
@@ -299,12 +307,16 @@ DETAIL:  line 1: xmlParseEntityRef: no name
 <invalidentity>&</abc>
                 ^
 line 1: Opening and ending tag mismatch: invalidentity line 1 and abc
+<invalidentity>&</abc>
+                      ^
 SELECT xmlparse(document '<undefinedentity>&idontexist;</abc>');
 ERROR:  invalid XML document
 DETAIL:  line 1: Entity 'idontexist' not defined
 <undefinedentity>&idontexist;</abc>
                              ^
 line 1: Opening and ending tag mismatch: undefinedentity line 1 and abc
+<undefinedentity>&idontexist;</abc>
+                                   ^
 SELECT xmlparse(document '<invalidns xmlns=''&lt;''/>');
          xmlparse          
 ---------------------------
@@ -323,6 +335,8 @@ DETAIL:  line 1: Entity 'idontexist' not defined
 <twoerrors>&idontexist;</unbalanced>
                        ^
 line 1: Opening and ending tag mismatch: twoerrors line 1 and unbalanced
+<twoerrors>&idontexist;</unbalanced>
+                                    ^
 SELECT xmlparse(document '<nosuchprefix:tag/>');
       xmlparse       
 ---------------------
@@ -1867,3 +1881,67 @@ 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>';
diff --git a/src/test/regress/pg_regress.c b/src/test/regress/pg_regress.c
index b5c0cb647a..7e9032b5ec 100644
--- a/src/test/regress/pg_regress.c
+++ b/src/test/regress/pg_regress.c
@@ -1232,7 +1232,7 @@ spawn_process(const char *cmdline)
 		char	   *cmdline2;
 
 		cmdline2 = psprintf("exec %s", cmdline);
-		execl(shellprog, shellprog, "-c", cmdline2, (char *) NULL);
+		execlp(shellprog, shellprog, "-c", cmdline2, (char *) NULL);
 		/* Not using the normal bail() here as we want _exit */
 		bail_noatexit("could not exec \"%s\": %m", shellprog);
 	}
diff --git a/src/test/regress/sql/xml.sql b/src/test/regress/sql/xml.sql
index 0ea4f50883..667973420f 100644
--- a/src/test/regress/sql/xml.sql
+++ b/src/test/regress/sql/xml.sql
@@ -679,3 +679,66 @@ 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>';
-- 
2.43.0



  [text/x-patch] v5-0002-Add-XMLVALIDATE.patch (63.5K, 3-v5-0002-Add-XMLVALIDATE.patch)
  download | inline diff:
From e030fc16e9081bc569e6e2b8cbd3b522e4f64160 Mon Sep 17 00:00:00 2001
From: Jim Jones <[email protected]>
Date: Thu, 5 Feb 2026 09:00:00 +0100
Subject: [PATCH v5 2/2] Add XMLVALIDATE

---
 doc/src/sgml/datatype.sgml            |   7 +-
 doc/src/sgml/func/func-xml.sgml       | 105 ++++++++++++
 src/backend/catalog/dependency.c      |  28 ++++
 src/backend/executor/execExprInterp.c |  27 +++
 src/backend/parser/gram.y             |  19 ++-
 src/backend/parser/parse_expr.c       |  43 +++++
 src/backend/parser/parse_target.c     |   3 +
 src/backend/utils/adt/ruleutils.c     |  17 +-
 src/backend/utils/adt/xml.c           | 144 +++++++++++++++-
 src/include/nodes/primnodes.h         |   1 +
 src/include/parser/kwlist.h           |   2 +
 src/include/utils/xml.h               |   1 +
 src/test/regress/expected/xml.out     | 233 ++++++++++++++++++++++++++
 src/test/regress/expected/xml_1.out   | 208 +++++++++++++++++++++++
 src/test/regress/expected/xml_2.out   | 233 ++++++++++++++++++++++++++
 src/test/regress/sql/xml.sql          | 209 +++++++++++++++++++++++
 16 files changed, 1272 insertions(+), 8 deletions(-)

diff --git a/doc/src/sgml/datatype.sgml b/doc/src/sgml/datatype.sgml
index f2ade57fe7..ddaf7c4d3d 100644
--- a/doc/src/sgml/datatype.sgml
+++ b/doc/src/sgml/datatype.sgml
@@ -4510,8 +4510,11 @@ xml '<foo>bar</foo>'
     against a document type declaration
     (DTD),<indexterm><primary>DTD</primary></indexterm>
     even when the input value specifies a DTD.
-    There is also currently no built-in support for validating against
-    other XML schema languages such as XML Schema.
+    However, <productname>PostgreSQL</productname> provides the
+    <function>XMLVALIDATE</function> function for validating XML documents
+    against XML Schema (XSD) documents. See
+    <xref linkend="functions-xml-processing-xmlvalidate"/> and
+    <xref linkend="sql-createxmlschema"/> for more information.
    </para>
 
    <para>
diff --git a/doc/src/sgml/func/func-xml.sgml b/doc/src/sgml/func/func-xml.sgml
index 511bc90852..c4a6e89d0e 100644
--- a/doc/src/sgml/func/func-xml.sgml
+++ b/doc/src/sgml/func/func-xml.sgml
@@ -1007,6 +1007,111 @@ SELECT xmltable.*
    3 |   4
    4 |   5
 (3 rows)
+]]></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> <replaceable>xml_value</replaceable> <literal>ACCORDING TO XMLSCHEMA</literal> <replaceable>schema_name</replaceable> ) <returnvalue>xml</returnvalue>
+</synopsis>
+
+    <para>
+     The <function>xmlvalidate</function> function validates an XML document
+     against an XML Schema. The schema must have been previously created using
+     <command>CREATE XMLSCHEMA</command>. If the XML is valid according to the
+     schema, the function returns the XML value unchanged. If validation fails,
+     an error is raised. If <replaceable>xml_value</replaceable> is
+     <literal>NULL</literal>, the function returns <literal>NULL</literal>.
+    </para>
+
+    <para>
+     The <replaceable>schema_name</replaceable> argument specifies the name
+     of an XML schema stored in the database. It can be schema-qualified
+     (e.g., <literal>myschema.person_schema</literal>) or unqualified, in
+     which case it will be searched for in the current schema search path.
+     The user must have <literal>USAGE</literal> privilege on the schema.
+    </para>
+
+    <para>
+     Examples:
+<screen><![CDATA[
+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>';
+
+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)
+]]></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 person_schema);
+ERROR:  XML validation failed
+]]></screen>
+    </para>
+
+    <para>
+     Schema-qualified names are supported:
+<screen><![CDATA[
+CREATE SCHEMA test_schema;
+CREATE XMLSCHEMA test_schema.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>';
+
+SELECT xmlvalidate(DOCUMENT '<product id="123"><name>Widget</name><price>9.99</price></product>'
+  ACCORDING TO XMLSCHEMA test_schema.product_schema);
+
+                          xmlvalidate
+---------------------------------------------------------------
+ <product id="123"><name>Widget</name><price>9.99</price></product>
+(1 row)
+]]></screen>
+    </para>
+
+    <para>
+     Since <function>xmlvalidate</function> returns <type>xml</type>, it can
+     be composed with other XML functions:
+<screen><![CDATA[
+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)
 ]]></screen>
     </para>
    </sect3>
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index 0b8d8e3152..50ce3871e7 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -2419,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/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
index 61ff5ddc74..7ab442d6bf 100644
--- a/src/backend/executor/execExprInterp.c
+++ b/src/backend/executor/execExprInterp.c
@@ -4630,6 +4630,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 9886250c25..9efc1e97d1 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
 
@@ -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 XMLSCHEMA XMLSERIALIZE XMLTABLE
+	XMLPARSE XMLPI XMLROOT XMLSCHEMA XMLSERIALIZE XMLTABLE XMLVALIDATE
 
 	YEAR_P YES_P
 
@@ -16348,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() */
@@ -17958,6 +17969,7 @@ unreserved_keyword:
 			| ABSENT
 			| ABSOLUTE_P
 			| ACCESS
+			| ACCORDING
 			| ACTION
 			| ADD_P
 			| ADMIN
@@ -18367,6 +18379,7 @@ col_name_keyword:
 			| XMLROOT
 			| XMLSERIALIZE
 			| XMLTABLE
+			| XMLVALIDATE
 		;
 
 /* Type/function identifier --- keywords that can be type or function names.
@@ -18506,6 +18519,7 @@ bare_label_keyword:
 			| ABSENT
 			| ABSOLUTE_P
 			| ACCESS
+			| ACCORDING
 			| ACTION
 			| ADD_P
 			| ADMIN
@@ -18960,6 +18974,7 @@ bare_label_keyword:
 			| XMLSCHEMA
 			| XMLSERIALIZE
 			| XMLTABLE
+			| XMLVALIDATE
 			| YES_P
 			| ZONE
 		;
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index dcfe1acc4c..098f13f665 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"
@@ -2370,6 +2371,7 @@ transformXmlExpr(ParseState *pstate, XmlExpr *x)
 	XmlExpr    *newx;
 	ListCell   *lc;
 	int			i;
+	Oid			xmlvalidate_schema_oid = InvalidOid;
 
 	newx = makeNode(XmlExpr);
 	newx->op = x->op;
@@ -2382,6 +2384,22 @@ 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.
@@ -2481,6 +2499,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");
@@ -2490,6 +2513,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 b5a2f915b6..b380bb39eb 100644
--- a/src/backend/parser/parse_target.c
+++ b/src/backend/parser/parse_target.c
@@ -1976,6 +1976,9 @@ FigureColnameInternal(Node *node, char **name)
 				case IS_XMLSERIALIZE:
 					*name = "xmlserialize";
 					return 2;
+				case IS_XMLVALIDATE:
+					*name = "xmlvalidate";
+					return 2;
 				case IS_DOCUMENT:
 					/* nothing */
 					break;
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index b5a7ad9066..cd8a9169a6 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)));
@@ -10173,6 +10176,9 @@ get_rule_expr(Node *node, deparse_context *context,
 							/* no extra decoration needed */
 							get_rule_expr((Node *) xexpr->args, context, true);
 							break;
+						case IS_XMLVALIDATE:
+							get_rule_expr((Node *) linitial(xexpr->args), context, true);
+							break;
 						case IS_XMLPARSE:
 							Assert(list_length(xexpr->args) == 2);
 
@@ -10242,6 +10248,13 @@ get_rule_expr(Node *node, deparse_context *context,
 						appendStringInfoString(buf, " NO INDENT");
 				}
 
+				if (xexpr->op == IS_XMLVALIDATE)
+				{
+					/* Output the schema name stored during parse analysis */
+					appendStringInfo(buf, " ACCORDING TO XMLSCHEMA %s",
+									 quote_identifier(xexpr->name));
+				}
+
 				if (xexpr->op == IS_DOCUMENT)
 					appendStringInfoString(buf, " IS DOCUMENT");
 				else
diff --git a/src/backend/utils/adt/xml.c b/src/backend/utils/adt/xml.c
index f69dc68286..d57c3c07f9 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,147 @@ 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;
+	xmltype    *schema_xml;
+	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_xml = DatumGetXmlP(schema_datum);
+	schemastr = text_to_cstring(schema_xml);
+	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 +1321,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/nodes/primnodes.h b/src/include/nodes/primnodes.h
index 5211cadc25..c40cbc8981 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 a018c2b9e0..4e8b5e1d7e 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)
@@ -523,6 +524,7 @@ PG_KEYWORD("xmlroot", XMLROOT, COL_NAME_KEYWORD, BARE_LABEL)
 PG_KEYWORD("xmlschema", XMLSCHEMA, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("xmlserialize", XMLSERIALIZE, COL_NAME_KEYWORD, BARE_LABEL)
 PG_KEYWORD("xmltable", XMLTABLE, COL_NAME_KEYWORD, BARE_LABEL)
+PG_KEYWORD("xmlvalidate", XMLVALIDATE, COL_NAME_KEYWORD, BARE_LABEL)
 PG_KEYWORD("year", YEAR_P, UNRESERVED_KEYWORD, AS_LABEL)
 PG_KEYWORD("yes", YES_P, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("zone", ZONE, UNRESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/include/utils/xml.h b/src/include/utils/xml.h
index 03acb25544..dc6a4d3784 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/xml.out b/src/test/regress/expected/xml.out
index 4e24657d2d..121d86d85d 100644
--- a/src/test/regress/expected/xml.out
+++ b/src/test/regress/expected/xml.out
@@ -1945,3 +1945,236 @@ CREATE XMLSCHEMA book_schema AS '<?xml version="1.0"?>
     </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_1.out b/src/test/regress/expected/xml_1.out
index aded57c78b..7b088380c4 100644
--- a/src/test/regress/expected/xml_1.out
+++ b/src/test/regress/expected/xml_1.out
@@ -1557,3 +1557,211 @@ CREATE XMLSCHEMA book_schema AS '<?xml version="1.0"?>
   </xs:element>
 </xs:schema>';
 ERROR:  xmlschema support requires libxml
+SELECT XMLVALIDATE(DOCUMENT '<person><name>John</name><age>30</age></person>'
+  ACCORDING TO XMLSCHEMA person_schema);
+ERROR:  XML schema "person_schema" does not exist
+SELECT XMLVALIDATE(DOCUMENT '<product id="P123"><name>Widget</name><price>9.99</price></product>'
+  ACCORDING TO XMLSCHEMA test_xmlschema_ns.product_schema);
+ERROR:  XML schema "test_xmlschema_ns.product_schema" does not exist
+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 schema "book_schema" does not exist
+SELECT XMLSERIALIZE(DOCUMENT
+  XMLVALIDATE(DOCUMENT '<person><name>Alice</name><age>25</age></person>'
+    ACCORDING TO XMLSCHEMA person_schema) AS text);
+ERROR:  XML schema "person_schema" does not exist
+SELECT XMLVALIDATE(DOCUMENT NULL ACCORDING TO XMLSCHEMA person_schema);
+ERROR:  XML schema "person_schema" does not exist
+SELECT XMLVALIDATE(DOCUMENT NULL ACCORDING TO XMLSCHEMA person_schema) IS NULL AS is_null;
+ERROR:  XML schema "person_schema" does not exist
+SELECT XMLVALIDATE(DOCUMENT '<person><name>John</name></person>'
+  ACCORDING TO XMLSCHEMA person_schema);
+ERROR:  XML schema "person_schema" does not exist
+SELECT XMLVALIDATE(DOCUMENT '<person><name>John</name><age>not-a-number</age></person>'
+  ACCORDING TO XMLSCHEMA person_schema);
+ERROR:  XML schema "person_schema" does not exist
+SELECT XMLVALIDATE(DOCUMENT '<product><name>Widget</name><price>9.99</price></product>'
+  ACCORDING TO XMLSCHEMA test_xmlschema_ns.product_schema);
+ERROR:  XML schema "test_xmlschema_ns.product_schema" does not exist
+SELECT XMLVALIDATE(DOCUMENT '<person><name>John</name><age>30</age><extra>data</extra></person>'
+  ACCORDING TO XMLSCHEMA person_schema);
+ERROR:  XML schema "person_schema" does not exist
+SELECT XMLVALIDATE(DOCUMENT '<notperson><name>John</name><age>30</age></notperson>'
+  ACCORDING TO XMLSCHEMA person_schema);
+ERROR:  XML schema "person_schema" does not exist
+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;
+ERROR:  XML schema "person_schema" does not exist
+SELECT pg_get_viewdef('validated_people'::regclass, true);
+ERROR:  relation "validated_people" does not exist
+LINE 1: SELECT pg_get_viewdef('validated_people'::regclass, true);
+                              ^
+DROP VIEW validated_people;
+ERROR:  view "validated_people" does not exist
+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;
+ERROR:  XML schema "test_xmlschema_ns.product_schema" does not exist
+SELECT pg_get_viewdef('validated_products'::regclass, true);
+ERROR:  relation "validated_products" does not exist
+LINE 1: SELECT pg_get_viewdef('validated_products'::regclass, true);
+                              ^
+DROP VIEW validated_products;
+ERROR:  view "validated_products" does not exist
+ALTER XMLSCHEMA book_schema RENAME TO library_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 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);
+ERROR:  XML schema "library_book_schema" does not exist
+ALTER XMLSCHEMA library_book_schema SET SCHEMA test_xmlschema_ns;
+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 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);
+ERROR:  XML schema "test_xmlschema_ns.library_book_schema" does not exist
+CREATE ROLE regress_xmlschema_test_role;
+ALTER XMLSCHEMA test_xmlschema_ns.library_book_schema OWNER TO regress_xmlschema_test_role;
+ERROR:  XML schema "test_xmlschema_ns.library_book_schema" does not exist
+SELECT schemaname, schemanamespace::regnamespace, schemaowner::regrole
+FROM pg_xmlschema
+WHERE schemaname = 'library_book_schema';
+ schemaname | schemanamespace | schemaowner 
+------------+-----------------+-------------
+(0 rows)
+
+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;
+ERROR:  XML schema "test_xmlschema_ns.library_book_schema" does not exist
+DROP XMLSCHEMA test_xmlschema_ns.library_book_schema;
+ERROR:  XML schema "test_xmlschema_ns.library_book_schema" does not exist
+DROP XMLSCHEMA test_xmlschema_ns.library_book_schema CASCADE;
+ERROR:  XML schema "test_xmlschema_ns.library_book_schema" does not exist
+SELECT * FROM book_view;
+ERROR:  relation "book_view" does not exist
+LINE 1: SELECT * FROM book_view;
+                      ^
+DROP XMLSCHEMA person_schema;
+ERROR:  XML schema "person_schema" does not exist
+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;
+ERROR:  XML schema "test_xmlschema_ns.product_schema" does not exist
+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>';
+ERROR:  xmlschema support requires libxml
+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>';
+ERROR:  xmlschema support requires libxml
+RESET ROLE;
+DROP XMLSCHEMA test_xmlschema_ns.role_schema;
+ERROR:  XML schema "test_xmlschema_ns.role_schema" does not exist
+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
+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>';
+ERROR:  xmlschema support requires libxml
+SET ROLE regress_xmlschema_user1;
+SELECT XMLVALIDATE(DOCUMENT '<test>data</test>'
+                   ACCORDING TO XMLSCHEMA permission_test_schema);
+ERROR:  XML schema "permission_test_schema" does not exist
+RESET ROLE;
+GRANT USAGE ON XMLSCHEMA permission_test_schema TO regress_xmlschema_user1;
+ERROR:  XML schema "permission_test_schema" does not exist
+SET ROLE regress_xmlschema_user1;
+SELECT XMLVALIDATE(DOCUMENT '<test>data</test>'
+                   ACCORDING TO XMLSCHEMA permission_test_schema);
+ERROR:  XML schema "permission_test_schema" does not exist
+RESET ROLE;
+SET ROLE regress_xmlschema_user2;
+SELECT XMLVALIDATE(DOCUMENT '<test>data</test>'
+                   ACCORDING TO XMLSCHEMA permission_test_schema);
+ERROR:  XML schema "permission_test_schema" does not exist
+RESET ROLE;
+GRANT USAGE ON XMLSCHEMA permission_test_schema TO regress_xmlschema_user2;
+ERROR:  XML schema "permission_test_schema" does not exist
+SET ROLE regress_xmlschema_user2;
+SELECT XMLVALIDATE(DOCUMENT '<test>data</test>'
+                   ACCORDING TO XMLSCHEMA permission_test_schema);
+ERROR:  XML schema "permission_test_schema" does not exist
+RESET ROLE;
+REVOKE USAGE ON XMLSCHEMA permission_test_schema FROM regress_xmlschema_user1;
+ERROR:  XML schema "permission_test_schema" does not exist
+SET ROLE regress_xmlschema_user1;
+SELECT XMLVALIDATE(DOCUMENT '<test>data</test>'
+                   ACCORDING TO XMLSCHEMA permission_test_schema);
+ERROR:  XML schema "permission_test_schema" does not exist
+RESET ROLE;
+DROP XMLSCHEMA permission_test_schema;
+ERROR:  XML schema "permission_test_schema" does not exist
+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>';
+ERROR:  xmlschema support requires libxml
+INSERT INTO validated_xml_data (data)
+VALUES (XMLVALIDATE(DOCUMENT '<data><value>42</value></data>'
+  ACCORDING TO XMLSCHEMA data_schema));
+ERROR:  XML schema "data_schema" does not exist
+INSERT INTO validated_xml_data (data)
+VALUES (XMLVALIDATE(DOCUMENT '<data><value>100</value></data>'
+  ACCORDING TO XMLSCHEMA data_schema));
+ERROR:  XML schema "data_schema" does not exist
+SELECT id, data FROM validated_xml_data ORDER BY id;
+ id | data 
+----+------
+(0 rows)
+
+INSERT INTO validated_xml_data (data)
+VALUES (XMLVALIDATE(DOCUMENT '<data><value>not-an-int</value></data>'
+  ACCORDING TO XMLSCHEMA data_schema));
+ERROR:  XML schema "data_schema" does not exist
+DROP TABLE validated_xml_data;
+DROP XMLSCHEMA data_schema;
+ERROR:  XML schema "data_schema" does not exist
+DROP XMLSCHEMA should_fail_schema;
+ERROR:  XML schema "should_fail_schema" does not exist
+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 4e24657d2d..121d86d85d 100644
--- a/src/test/regress/expected/xml_2.out
+++ b/src/test/regress/expected/xml_2.out
@@ -1945,3 +1945,236 @@ CREATE XMLSCHEMA book_schema AS '<?xml version="1.0"?>
     </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 667973420f..c1650ead94 100644
--- a/src/test/regress/sql/xml.sql
+++ b/src/test/regress/sql/xml.sql
@@ -742,3 +742,212 @@ CREATE XMLSCHEMA book_schema AS '<?xml version="1.0"?>
     </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;
\ No newline at end of file
-- 
2.43.0



^ permalink  raw  reply  [nested|flat] 9+ messages in thread

* Re: WIP - xmlvalidate implementation from TODO list
  2026-01-23 12:19 Re: WIP - xmlvalidate implementation from TODO list Jim Jones <[email protected]>
  2026-01-23 15:43 ` Re: WIP - xmlvalidate implementation from TODO list Jim Jones <[email protected]>
  2026-02-05 08:42   ` Re: WIP - xmlvalidate implementation from TODO list Jim Jones <[email protected]>
@ 2026-02-05 21:18     ` Marcos Magueta <[email protected]>
  2026-02-14 22:30       ` Re: WIP - xmlvalidate implementation from TODO list Marcos Magueta <[email protected]>
  0 siblings, 1 reply; 9+ messages in thread

From: Marcos Magueta @ 2026-02-05 21:18 UTC (permalink / raw)
  To: Jim Jones <[email protected]>; +Cc: Andrey Borodin <[email protected]>; Kirill Reshke <[email protected]>; PostgreSQL Hackers <[email protected]>

Hey Jim! Thanks for checking, I definitely overlooked that when splitting,
it was a bit too much to do in a single go. I will check it this weekend!

Regards!


^ permalink  raw  reply  [nested|flat] 9+ messages in thread

* Re: WIP - xmlvalidate implementation from TODO list
  2026-01-23 12:19 Re: WIP - xmlvalidate implementation from TODO list Jim Jones <[email protected]>
  2026-01-23 15:43 ` Re: WIP - xmlvalidate implementation from TODO list Jim Jones <[email protected]>
  2026-02-05 08:42   ` Re: WIP - xmlvalidate implementation from TODO list Jim Jones <[email protected]>
  2026-02-05 21:18     ` Re: WIP - xmlvalidate implementation from TODO list Marcos Magueta <[email protected]>
@ 2026-02-14 22:30       ` Marcos Magueta <[email protected]>
  2026-02-14 23:31         ` Re: WIP - xmlvalidate implementation from TODO list Jim Jones <[email protected]>
  0 siblings, 1 reply; 9+ messages in thread

From: Marcos Magueta @ 2026-02-14 22:30 UTC (permalink / raw)
  To: Jim Jones <[email protected]>; +Cc: Andrey Borodin <[email protected]>; Kirill Reshke <[email protected]>; PostgreSQL Hackers <[email protected]>

Hey Jim,

I finally got back to it.

> On second thought, is there any scenario where we'll need a_expr for "y"
> at all in "CREATE XMLSCHEMA x AS y"? Isn't it always going to be a
> string? I see now that my example in the previous post was somewhat
> misleading (sorry for the noise).
I think you are right about removing the alias here, but I thought that on
the previous exchange you were referring to the expression on x, not y.
Nonetheless I see no point in keeping aliases here, however. I added it
because I initially thought of aliasing chained expressions, but that makes
little sense in an effectful command.

> I'd suggest to focus on 0001 for now. Feel free to revert it to the
> state of v4 if you disagree with the current approach.
I am perfectly fine with the split, I think splitting it further will cause
more problems to review because of their interdependency as you pointed out.

It seems that for v6, all that is left is to revise the grant for read and
write, and add the docs. Correct?

If so, I will get started with it.

Regards!


^ permalink  raw  reply  [nested|flat] 9+ messages in thread

* Re: WIP - xmlvalidate implementation from TODO list
  2026-01-23 12:19 Re: WIP - xmlvalidate implementation from TODO list Jim Jones <[email protected]>
  2026-01-23 15:43 ` Re: WIP - xmlvalidate implementation from TODO list Jim Jones <[email protected]>
  2026-02-05 08:42   ` Re: WIP - xmlvalidate implementation from TODO list Jim Jones <[email protected]>
  2026-02-05 21:18     ` Re: WIP - xmlvalidate implementation from TODO list Marcos Magueta <[email protected]>
  2026-02-14 22:30       ` Re: WIP - xmlvalidate implementation from TODO list Marcos Magueta <[email protected]>
@ 2026-02-14 23:31         ` Jim Jones <[email protected]>
  2026-02-18 19:20           ` Re: WIP - xmlvalidate implementation from TODO list Marcos Magueta <[email protected]>
  0 siblings, 1 reply; 9+ messages in thread

From: Jim Jones @ 2026-02-14 23:31 UTC (permalink / raw)
  To: Marcos Magueta <[email protected]>; +Cc: Andrey Borodin <[email protected]>; Kirill Reshke <[email protected]>; PostgreSQL Hackers <[email protected]>



On 14/02/2026 23:30, Marcos Magueta wrote:
> It seems that for v6, all that is left is to revise the grant for read
> and write, and add the docs. Correct?

Yes, that would be good. Since CREATE XMLSCHEMA isn't part of the
SQL/XML standard, we'll have to rely on the docs to check if things are
working properly.

Best, Jim






^ permalink  raw  reply  [nested|flat] 9+ messages in thread

* Re: WIP - xmlvalidate implementation from TODO list
  2026-01-23 12:19 Re: WIP - xmlvalidate implementation from TODO list Jim Jones <[email protected]>
  2026-01-23 15:43 ` Re: WIP - xmlvalidate implementation from TODO list Jim Jones <[email protected]>
  2026-02-05 08:42   ` Re: WIP - xmlvalidate implementation from TODO list Jim Jones <[email protected]>
  2026-02-05 21:18     ` Re: WIP - xmlvalidate implementation from TODO list Marcos Magueta <[email protected]>
  2026-02-14 22:30       ` Re: WIP - xmlvalidate implementation from TODO list Marcos Magueta <[email protected]>
  2026-02-14 23:31         ` Re: WIP - xmlvalidate implementation from TODO list Jim Jones <[email protected]>
@ 2026-02-18 19:20           ` Marcos Magueta <[email protected]>
  2026-02-19 07:55             ` Re: WIP - xmlvalidate implementation from TODO list Jim Jones <[email protected]>
  0 siblings, 1 reply; 9+ messages in thread

From: Marcos Magueta @ 2026-02-18 19:20 UTC (permalink / raw)
  To: Jim Jones <[email protected]>; +Cc: Andrey Borodin <[email protected]>; Kirill Reshke <[email protected]>; PostgreSQL Hackers <[email protected]>

Hello there again, Jim!

Here's the v6 with the docs updated and some minor things. I have questions
about the predefined roles however.

The tab completion on psql was using the wrong OID type, so I took the
chance to change it to only complete unqualified XML schemas in
current_schemas with true.

I noticed I had added a change I made to run locally on pg_regress that
shouldn't be on the patch, so I reversed it (execl to execlp because of
NixOS search paths).

I was trying to figure out how to do the roles, and I have some thoughts on
it now. While I am not particularly against predefined roles, I think we
should be careful before committing to them in this form. As predefined
roles, they would effectively become part of a long-term public interface:
once they exist, we should then carry them forward and preserve their
semantics across releases, so adding a feature-specific role increases
permanent surface area (docs, tests, upgrade behavior, compatibility
expectations) and is harder to revisit later if the privilege model evolves
(which I think will make people mad, it's XML burden after all).

Also, a write-oriented role is not as straightforward as I thought with the
current ownership model (and that's me guessing here). Mutating DDL
behavior is still owner-driven (or superuser-driven) most of the places,
not purely ACL-driven, so a pg_write_xmlschemas role can look clearer at
first glance than it is in practice. To make that role fully consistent, we
may need broader policy decisions around what should be grantable versus
what should remain ownership-based, not just new role entries. Again, I am
not opposed to the idea, but these made me a bit anxious to simply add
them. What are your thoughts on these?

PS: Reattached patch 2 as v5 just to make it easier to get everything, but
nothing changed in there.

Regards,
Marcos Magueta.


Attachments:

  [application/octet-stream] v6-0001-Add-CREATE-XMLSCHEMA.patch (80.7K, 3-v6-0001-Add-CREATE-XMLSCHEMA.patch)
  download | inline diff:
From 31804932859aa75e050343d59628ea446e5733ea Mon Sep 17 00:00:00 2001
From: Marcos Magueta <[email protected]>
Date: Wed, 18 Feb 2026 16:13:06 -0300
Subject: [PATCH] [PATCH v6 1/2] Add CREATE XMLSCHEMA

---
 doc/src/sgml/datatype.sgml             |   1 +
 doc/src/sgml/ddl.sgml                  |  13 +-
 doc/src/sgml/ref/allfiles.sgml         |   3 +
 doc/src/sgml/ref/alter_xmlschema.sgml  | 145 ++++++++++++++++++
 doc/src/sgml/ref/create_xmlschema.sgml | 182 +++++++++++++++++++++++
 doc/src/sgml/ref/drop_xmlschema.sgml   | 120 +++++++++++++++
 doc/src/sgml/ref/grant.sgml            |   7 +-
 doc/src/sgml/ref/revoke.sgml           |   7 +
 doc/src/sgml/reference.sgml            |   3 +
 src/backend/catalog/Makefile           |   1 +
 src/backend/catalog/aclchk.c           |  17 +++
 src/backend/catalog/dependency.c       |   3 +
 src/backend/catalog/meson.build        |   1 +
 src/backend/catalog/namespace.c        |  56 +++++++
 src/backend/catalog/objectaddress.c    |  84 +++++++++++
 src/backend/catalog/pg_xmlschema.c     | 194 +++++++++++++++++++++++++
 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   |  95 ++++++++++++
 src/backend/parser/gram.y              |  64 +++++++-
 src/backend/tcop/utility.c             |  17 +++
 src/backend/utils/adt/acl.c            |   4 +
 src/bin/pg_dump/common.c               |   3 +
 src/bin/pg_dump/pg_backup_archiver.c   |   1 +
 src/bin/pg_dump/pg_dump.c              | 128 ++++++++++++++++
 src/bin/pg_dump/pg_dump.h              |   8 +
 src/bin/pg_dump/pg_dump_sort.c         |   5 +
 src/bin/psql/tab-complete.in.c         |  22 +++
 src/include/catalog/Makefile           |   1 +
 src/include/catalog/meson.build        |   1 +
 src/include/catalog/namespace.h        |   1 +
 src/include/catalog/pg_xmlschema.h     |  48 ++++++
 src/include/commands/xmlschemacmds.h   |   9 ++
 src/include/nodes/parsenodes.h         |   1 +
 src/include/parser/kwlist.h            |   1 +
 src/include/tcop/cmdtaglist.h          |   3 +
 src/include/utils/acl.h                |   1 +
 src/test/regress/expected/oidjoins.out |   2 +
 src/test/regress/expected/xml.out      |  64 ++++++++
 src/test/regress/expected/xml_1.out    |  61 ++++++++
 src/test/regress/expected/xml_2.out    |  78 ++++++++++
 src/test/regress/sql/xml.sql           |  63 ++++++++
 46 files changed, 1536 insertions(+), 4 deletions(-)
 create mode 100644 doc/src/sgml/ref/alter_xmlschema.sgml
 create mode 100644 doc/src/sgml/ref/create_xmlschema.sgml
 create mode 100644 doc/src/sgml/ref/drop_xmlschema.sgml
 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/datatype.sgml b/doc/src/sgml/datatype.sgml
index 3017c674040..f2ade57fe7d 100644
--- a/doc/src/sgml/datatype.sgml
+++ b/doc/src/sgml/datatype.sgml
@@ -4649,6 +4649,7 @@ SET xmloption TO { DOCUMENT | CONTENT };
     distribution.
    </para>
    </sect2>
+
   </sect1>
 
   &json;
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index 9070aaa5a7c..5a2bc9ecee3 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -2486,6 +2486,9 @@ REVOKE ALL ON accounts FROM PUBLIC;
        dependencies on a type, which could prevent the owner from changing
        the type later.)
       </para>
+      <para>
+       For XML schemas, allows use of the XML schema for XML validation.
+      </para>
       <para>
        For foreign-data wrappers, allows creation of new servers using the
        foreign-data wrapper.
@@ -2548,6 +2551,7 @@ REVOKE ALL ON accounts FROM PUBLIC;
    foreign data wrappers,
    foreign servers,
    large objects,
+   XML schemas,
    schemas,
    tablespaces,
    or configuration parameters.
@@ -2669,7 +2673,8 @@ REVOKE ALL ON accounts FROM PUBLIC;
        <literal>LANGUAGE</literal>,
        <literal>SCHEMA</literal>,
        <literal>SEQUENCE</literal>,
-       <literal>TYPE</literal>
+       <literal>TYPE</literal>,
+       <literal>XMLSCHEMA</literal>
       </entry>
      </row>
      <row>
@@ -2799,6 +2804,12 @@ REVOKE ALL ON accounts FROM PUBLIC;
       <entry><literal>U</literal></entry>
       <entry><literal>\dT+</literal></entry>
      </row>
+     <row>
+      <entry><literal>XMLSCHEMA</literal></entry>
+      <entry><literal>U</literal></entry>
+      <entry>none</entry>
+      <entry>none</entry>
+     </row>
     </tbody>
    </tgroup>
   </table>
diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index e167406c744..87a8e6066e4 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -48,6 +48,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY alterUser          SYSTEM "alter_user.sgml">
 <!ENTITY alterUserMapping   SYSTEM "alter_user_mapping.sgml">
 <!ENTITY alterView          SYSTEM "alter_view.sgml">
+<!ENTITY alterXmlSchema     SYSTEM "alter_xmlschema.sgml">
 <!ENTITY analyze            SYSTEM "analyze.sgml">
 <!ENTITY begin              SYSTEM "begin.sgml">
 <!ENTITY call               SYSTEM "call.sgml">
@@ -100,6 +101,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY createUser         SYSTEM "create_user.sgml">
 <!ENTITY createUserMapping  SYSTEM "create_user_mapping.sgml">
 <!ENTITY createView         SYSTEM "create_view.sgml">
+<!ENTITY createXmlSchema    SYSTEM "create_xmlschema.sgml">
 <!ENTITY deallocate         SYSTEM "deallocate.sgml">
 <!ENTITY declare            SYSTEM "declare.sgml">
 <!ENTITY delete             SYSTEM "delete.sgml">
@@ -148,6 +150,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY dropUser           SYSTEM "drop_user.sgml">
 <!ENTITY dropUserMapping    SYSTEM "drop_user_mapping.sgml">
 <!ENTITY dropView           SYSTEM "drop_view.sgml">
+<!ENTITY dropXmlSchema      SYSTEM "drop_xmlschema.sgml">
 <!ENTITY end                SYSTEM "end.sgml">
 <!ENTITY execute            SYSTEM "execute.sgml">
 <!ENTITY explain            SYSTEM "explain.sgml">
diff --git a/doc/src/sgml/ref/alter_xmlschema.sgml b/doc/src/sgml/ref/alter_xmlschema.sgml
new file mode 100644
index 00000000000..6a286ad00fe
--- /dev/null
+++ b/doc/src/sgml/ref/alter_xmlschema.sgml
@@ -0,0 +1,145 @@
+<!--
+doc/src/sgml/ref/alter_xmlschema.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-alterxmlschema">
+ <indexterm zone="sql-alterxmlschema">
+  <primary>ALTER XMLSCHEMA</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>ALTER XMLSCHEMA</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>ALTER XMLSCHEMA</refname>
+  <refpurpose>change the definition of an XML schema</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+ALTER XMLSCHEMA [ IF EXISTS ] <replaceable class="parameter">name</replaceable> OWNER TO { <replaceable class="parameter">new_owner</replaceable> | CURRENT_ROLE | CURRENT_USER | SESSION_USER }
+ALTER XMLSCHEMA [ IF EXISTS ] <replaceable class="parameter">name</replaceable> RENAME TO <replaceable class="parameter">new_name</replaceable>
+ALTER XMLSCHEMA [ IF EXISTS ] <replaceable class="parameter">name</replaceable> SET SCHEMA <replaceable class="parameter">new_schema</replaceable>
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>ALTER XMLSCHEMA</command> changes the definition of an XML schema.
+  </para>
+
+  <para>
+   You must own the XML schema to use <command>ALTER XMLSCHEMA</command>.
+   To change an XML schema's schema, you must also have <literal>CREATE</literal>
+   privilege on the new schema.
+   To alter the owner, you must be able to <literal>SET ROLE</literal> to the
+   new owning role, and that role must have <literal>CREATE</literal>
+   privilege on the XML schema's schema.
+   (These restrictions enforce that altering the owner
+   doesn't do anything you couldn't do by dropping and recreating the XML schema.
+   However, a superuser can alter ownership of any XML schema anyway.)
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><replaceable class="parameter">name</replaceable></term>
+    <listitem>
+     <para>
+      The name (optionally schema-qualified) of an XML schema to be altered.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>IF EXISTS</literal></term>
+    <listitem>
+     <para>
+      Do not throw an error if the XML schema does not exist. A notice is issued
+      in this case.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_owner</replaceable></term>
+    <listitem>
+     <para>
+      The user name of the new owner of the XML schema.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_name</replaceable></term>
+    <listitem>
+     <para>
+      The new name of the XML schema.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_schema</replaceable></term>
+    <listitem>
+     <para>
+      The new schema for the XML schema.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To rename an XML schema:
+<programlisting>
+ALTER XMLSCHEMA person_schema RENAME TO people_schema;
+</programlisting>
+  </para>
+
+  <para>
+   To change the owner of an XML schema:
+<programlisting>
+ALTER XMLSCHEMA people_schema OWNER TO joe;
+</programlisting>
+  </para>
+
+  <para>
+   To move an XML schema to another schema:
+<programlisting>
+ALTER XMLSCHEMA people_schema SET SCHEMA myschema;
+</programlisting>
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   <command>ALTER XMLSCHEMA</command> is a
+   <productname>PostgreSQL</productname> extension.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-createxmlschema"/></member>
+   <member><xref linkend="sql-dropxmlschema"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/ref/create_xmlschema.sgml b/doc/src/sgml/ref/create_xmlschema.sgml
new file mode 100644
index 00000000000..573b9b72111
--- /dev/null
+++ b/doc/src/sgml/ref/create_xmlschema.sgml
@@ -0,0 +1,182 @@
+<!--
+doc/src/sgml/ref/create_xmlschema.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-createxmlschema">
+ <indexterm zone="sql-createxmlschema">
+  <primary>CREATE XMLSCHEMA</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>CREATE XMLSCHEMA</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>CREATE XMLSCHEMA</refname>
+  <refpurpose>define a new XML schema</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+CREATE XMLSCHEMA [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> AS <replaceable class="parameter">schema_definition</replaceable>
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>CREATE XMLSCHEMA</command> defines a new XML schema in the
+   current database. The schema definition is an XML Schema Definition (XSD)
+   document that can be used to validate XML documents using the
+   <function>XMLVALIDATE</function> function.
+  </para>
+
+  <para>
+   If a schema name is given then the XML schema is created in the
+   specified schema. Otherwise it is created in the current schema.
+   The XML schema name must be unique within the schema.
+  </para>
+
+  <para>
+   After an XML schema is created, you use the
+   <function>XMLVALIDATE</function> function to validate XML documents
+   against it.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><literal>IF NOT EXISTS</literal></term>
+    <listitem>
+     <para>
+      Do not throw an error if an XML schema with the same name already exists.
+      A notice is issued in this case. Note that there is no guarantee that
+      the existing XML schema is similar to the one that would have been created.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">name</replaceable></term>
+    <listitem>
+     <para>
+      The name (optionally schema-qualified) of the XML schema to be created.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">schema_definition</replaceable></term>
+    <listitem>
+     <para>
+      An XML Schema Definition (XSD) document provided as a string literal.
+      The schema definition must be a valid XSD document conforming to the
+      W3C XML Schema standard.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Notes</title>
+
+  <para>
+   To create an XML schema, you must have <literal>CREATE</literal> privilege
+   on the current schema.
+  </para>
+
+  <para>
+   Use <link linkend="sql-dropxmlschema"><command>DROP XMLSCHEMA</command></link> to remove an XML schema.
+  </para>
+
+  <para>
+   See also <link linkend="sql-alterxmlschema"><command>ALTER XMLSCHEMA</command></link>
+   to rename an XML schema or change its owner.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   Create an XML schema for validating person documents:
+<programlisting>
+CREATE XMLSCHEMA person_schema AS '&lt;?xml version="1.0"?&gt;
+&lt;xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"&gt;
+  &lt;xs:element name="person"&gt;
+    &lt;xs:complexType&gt;
+      &lt;xs:sequence&gt;
+        &lt;xs:element name="name" type="xs:string"/&gt;
+        &lt;xs:element name="age" type="xs:integer"/&gt;
+      &lt;/xs:sequence&gt;
+    &lt;/xs:complexType&gt;
+  &lt;/xs:element&gt;
+&lt;/xs:schema&gt;';
+</programlisting>
+  </para>
+
+  <para>
+   Create an XML schema with a schema-qualified name:
+<programlisting>
+CREATE SCHEMA myschema;
+CREATE XMLSCHEMA myschema.product_schema AS '&lt;?xml version="1.0"?&gt;
+&lt;xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"&gt;
+  &lt;xs:element name="product"&gt;
+    &lt;xs:complexType&gt;
+      &lt;xs:sequence&gt;
+        &lt;xs:element name="name" type="xs:string"/&gt;
+        &lt;xs:element name="price" type="xs:decimal"/&gt;
+      &lt;/xs:sequence&gt;
+      &lt;xs:attribute name="id" type="xs:string" use="required"/&gt;
+    &lt;/xs:complexType&gt;
+  &lt;/xs:element&gt;
+&lt;/xs:schema&gt;';
+</programlisting>
+  </para>
+
+  <para>
+   Create an XML schema only if it doesn't already exist:
+<programlisting>
+CREATE XMLSCHEMA IF NOT EXISTS person_schema AS '...';
+</programlisting>
+  </para>
+
+  <para>
+   Validate an XML document against the schema:
+<programlisting>
+SELECT XMLVALIDATE(
+  DOCUMENT '&lt;person&gt;&lt;name&gt;John&lt;/name&gt;&lt;age&gt;30&lt;/age&gt;&lt;/person&gt;'
+  ACCORDING TO XMLSCHEMA person_schema
+);
+</programlisting>
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   <command>CREATE XMLSCHEMA</command> is a
+   <productname>PostgreSQL</productname> extension. There is no
+   <command>CREATE XMLSCHEMA</command> statement in the SQL standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-alterxmlschema"/></member>
+   <member><xref linkend="sql-dropxmlschema"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/ref/drop_xmlschema.sgml b/doc/src/sgml/ref/drop_xmlschema.sgml
new file mode 100644
index 00000000000..4204f9bc413
--- /dev/null
+++ b/doc/src/sgml/ref/drop_xmlschema.sgml
@@ -0,0 +1,120 @@
+<!--
+doc/src/sgml/ref/drop_xmlschema.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-dropxmlschema">
+ <indexterm zone="sql-dropxmlschema">
+  <primary>DROP XMLSCHEMA</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>DROP XMLSCHEMA</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>DROP XMLSCHEMA</refname>
+  <refpurpose>remove an XML schema</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+DROP XMLSCHEMA [ IF EXISTS ] <replaceable class="parameter">name</replaceable> [, ...] [ CASCADE | RESTRICT ]
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>DROP XMLSCHEMA</command> removes XML schemas from the database.
+   An XML schema can only be dropped by its owner or a superuser.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><literal>IF EXISTS</literal></term>
+    <listitem>
+     <para>
+      Do not throw an error if the XML schema does not exist. A notice is issued
+      in this case.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">name</replaceable></term>
+    <listitem>
+     <para>
+      The name (optionally schema-qualified) of an XML schema.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>CASCADE</literal></term>
+    <listitem>
+     <para>
+      Automatically drop objects that depend on the XML schema,
+      and in turn all objects that depend on those objects
+      (see <xref linkend="ddl-depend"/>).
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>RESTRICT</literal></term>
+    <listitem>
+     <para>
+      Refuse to drop the XML schema if any objects depend on it.  This
+      is the default.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To remove the XML schema <literal>person_schema</literal>:
+<programlisting>
+DROP XMLSCHEMA person_schema;
+</programlisting>
+  </para>
+
+  <para>
+   To remove an XML schema and all dependent objects:
+<programlisting>
+DROP XMLSCHEMA person_schema CASCADE;
+</programlisting>
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   <command>DROP XMLSCHEMA</command> is a
+   <productname>PostgreSQL</productname> extension. There is no
+   <command>DROP XMLSCHEMA</command> statement in the SQL standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-createxmlschema"/></member>
+   <member><xref linkend="sql-alterxmlschema"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/ref/grant.sgml b/doc/src/sgml/ref/grant.sgml
index 043f5d5a40a..28345bc2215 100644
--- a/doc/src/sgml/ref/grant.sgml
+++ b/doc/src/sgml/ref/grant.sgml
@@ -97,6 +97,11 @@ GRANT { USAGE | ALL [ PRIVILEGES ] }
     TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
     [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ]
 
+GRANT { USAGE | ALL [ PRIVILEGES ] }
+    ON XMLSCHEMA <replaceable>xmlschema_name</replaceable> [, ...]
+    TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
+    [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ]
+
 GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replaceable class="parameter">role_specification</replaceable> [, ...]
     [ WITH { ADMIN | INHERIT | SET } { OPTION | TRUE | FALSE } ]
     [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ]
@@ -119,7 +124,7 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace
    that grants privileges on a database object (table, column, view,
    foreign table, sequence, database, foreign-data wrapper, foreign server,
    function, procedure, procedural language, large object, configuration
-   parameter, schema, tablespace, or type), and one that grants
+   parameter, schema, tablespace, type, or XML schema), and one that grants
    membership in a role.  These variants are similar in many ways, but
    they are different enough to be described separately.
   </para>
diff --git a/doc/src/sgml/ref/revoke.sgml b/doc/src/sgml/ref/revoke.sgml
index 8df492281a1..3c8f81639d0 100644
--- a/doc/src/sgml/ref/revoke.sgml
+++ b/doc/src/sgml/ref/revoke.sgml
@@ -125,6 +125,13 @@ REVOKE [ GRANT OPTION FOR ]
     [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ]
     [ CASCADE | RESTRICT ]
 
+REVOKE [ GRANT OPTION FOR ]
+    { USAGE | ALL [ PRIVILEGES ] }
+    ON XMLSCHEMA <replaceable>xmlschema_name</replaceable> [, ...]
+    FROM <replaceable class="parameter">role_specification</replaceable> [, ...]
+    [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ]
+    [ CASCADE | RESTRICT ]
+
 REVOKE [ { ADMIN | INHERIT | SET } OPTION FOR ]
     <replaceable class="parameter">role_name</replaceable> [, ...] FROM <replaceable class="parameter">role_specification</replaceable> [, ...]
     [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ]
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index 2cf02c37b17..982bbea3a27 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -76,6 +76,7 @@
    &alterUser;
    &alterUserMapping;
    &alterView;
+   &alterXmlSchema;
    &analyze;
    &begin;
    &call;
@@ -128,6 +129,7 @@
    &createUser;
    &createUserMapping;
    &createView;
+   &createXmlSchema;
    &deallocate;
    &declare;
    &delete;
@@ -176,6 +178,7 @@
    &dropUser;
    &dropUserMapping;
    &dropView;
+   &dropXmlSchema;
    &end;
    &execute;
    &explain;
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 aef855abccc..fbc96176df5 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 7564965fa18..ddb60523d52 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;
 
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 4b0f4ba115d..00ee124cfd9 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"
@@ -4144,6 +4145,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 13d73f8909c..3f4c155e93b 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"
@@ -188,6 +189,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,
@@ -721,6 +736,9 @@ static const struct object_type_map
 	{
 		"collation", OBJECT_COLLATION
 	},
+	{
+		"xmlschema", OBJECT_XMLSCHEMA
+	},
 	{
 		"table constraint", OBJECT_TABCONSTRAINT
 	},
@@ -1030,6 +1048,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);
@@ -2283,6 +2306,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:
@@ -2461,6 +2485,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:
@@ -4068,6 +4093,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);
 	}
@@ -4670,6 +4723,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);
 	}
@@ -6020,6 +6077,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..116bbfa71c7
--- /dev/null
+++ b/src/backend/catalog/pg_xmlschema.c
@@ -0,0 +1,194 @@
+#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,
+				xmltype *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;
+		char	   *schemastr;
+
+		schemastr = text_to_cstring(schemadata);
+		xmlerrcxt = pg_xml_init(PG_XML_STRICTNESS_WELLFORMED);
+
+		PG_TRY();
+		{
+			parser_ctxt = xmlSchemaNewMemParserCtxt(schemastr, strlen(schemastr));
+			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");
+
+			xmlSchemaFree(schema_ptr);
+			xmlSchemaFreeParserCtxt(parser_ctxt);
+		}
+		PG_CATCH();
+		{
+			pg_xml_done(xmlerrcxt, true);
+			PG_RE_THROW();
+		}
+		PG_END_TRY();
+
+		pg_xml_done(xmlerrcxt, false);
+		pfree(schemastr);
+	}
+#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] = PointerGetDatum(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 c6f58d47be6..a451acde215 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"
@@ -134,6 +135,10 @@ report_namespace_conflict(Oid classId, const char *name, Oid nspOid)
 		case TSConfigRelationId:
 			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;
@@ -417,6 +422,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:
@@ -561,6 +567,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:
@@ -871,6 +878,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 5b80396723c..54c91ecf9a8 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..43b1a56a10c
--- /dev/null
+++ b/src/backend/commands/xmlschemacmds.c
@@ -0,0 +1,95 @@
+#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"
+#include "utils/xml.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;
+	xmltype    *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")));
+
+	/* The schema definition is passed as a String node from the grammar */
+	schemaData = (xmltype *) cstring_to_text(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;
+	}
+
+	ObjectAddressSet(address, XmlSchemaRelationId, newoid);
+
+	return address;
+}
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index c567252acc4..e56b0c0c84c 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -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
 
 	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);
@@ -18245,6 +18305,7 @@ unreserved_keyword:
 			| WRAPPER
 			| WRITE
 			| XML_P
+			| XMLSCHEMA
 			| YEAR_P
 			| YES_P
 			| ZONE
@@ -18914,6 +18975,7 @@ bare_label_keyword:
 			| XMLPARSE
 			| XMLPI
 			| XMLROOT
+			| XMLSCHEMA
 			| XMLSERIALIZE
 			| XMLTABLE
 			| YES_P
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 641673f0b0e..c6eb6418dbb 100644
--- a/src/backend/utils/adt/acl.c
+++ b/src/backend/utils/adt/acl.c
@@ -868,6 +868,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/bin/pg_dump/common.c b/src/bin/pg_dump/common.c
index 349b47c8e29..651cf674365 100644
--- a/src/bin/pg_dump/common.c
+++ b/src/bin/pg_dump/common.c
@@ -182,6 +182,9 @@ getSchemaData(Archive *fout, int *numTablesPtr)
 	pg_log_info("reading user-defined conversions");
 	getConversions(fout);
 
+	pg_log_info("reading XML schemas");
+	getXmlSchemas(fout);
+
 	pg_log_info("reading type casts");
 	getCasts(fout);
 
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 7afcc0859c8..1ea5b365ab4 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3812,6 +3812,7 @@ _getObjectDescription(PQExpBuffer buf, const TocEntry *te)
 		strcmp(type, "TEXT SEARCH CONFIGURATION") == 0 ||
 		strcmp(type, "TYPE") == 0 ||
 		strcmp(type, "VIEW") == 0 ||
+		strcmp(type, "XMLSCHEMA") == 0 ||
 	/* non-schema-specified objects */
 		strcmp(type, "DATABASE") == 0 ||
 		strcmp(type, "PROCEDURAL LANGUAGE") == 0 ||
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 49598304335..786f7d4f33e 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -305,6 +305,7 @@ static void dumpAccessMethod(Archive *fout, const AccessMethodInfo *aminfo);
 static void dumpOpclass(Archive *fout, const OpclassInfo *opcinfo);
 static void dumpOpfamily(Archive *fout, const OpfamilyInfo *opfinfo);
 static void dumpCollation(Archive *fout, const CollInfo *collinfo);
+static void dumpXmlSchema(Archive *fout, const XmlSchemaInfo * xmlschemainfo);
 static void dumpConversion(Archive *fout, const ConvInfo *convinfo);
 static void dumpRule(Archive *fout, const RuleInfo *rinfo);
 static void dumpAgg(Archive *fout, const AggInfo *agginfo);
@@ -6551,6 +6552,62 @@ getConversions(Archive *fout)
 	destroyPQExpBuffer(query);
 }
 
+/*
+ * getXmlSchemas:
+ *	  get information about all XML schemas in the system catalogs
+ */
+void
+getXmlSchemas(Archive *fout)
+{
+	PGresult   *res;
+	int			ntups;
+	int			i;
+	PQExpBuffer query;
+	XmlSchemaInfo *xmlschemainfo;
+	int			i_tableoid;
+	int			i_oid;
+	int			i_schemaname;
+	int			i_schemanamespace;
+	int			i_schemaowner;
+
+	query = createPQExpBuffer();
+
+	appendPQExpBufferStr(query, "SELECT tableoid, oid, schemaname, "
+						 "schemanamespace, "
+						 "schemaowner "
+						 "FROM pg_xmlschema");
+
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+
+	ntups = PQntuples(res);
+
+	xmlschemainfo = (XmlSchemaInfo *) pg_malloc(ntups * sizeof(XmlSchemaInfo));
+
+	i_tableoid = PQfnumber(res, "tableoid");
+	i_oid = PQfnumber(res, "oid");
+	i_schemaname = PQfnumber(res, "schemaname");
+	i_schemanamespace = PQfnumber(res, "schemanamespace");
+	i_schemaowner = PQfnumber(res, "schemaowner");
+
+	for (i = 0; i < ntups; i++)
+	{
+		xmlschemainfo[i].dobj.objType = DO_XMLSCHEMA;
+		xmlschemainfo[i].dobj.catId.tableoid = atooid(PQgetvalue(res, i, i_tableoid));
+		xmlschemainfo[i].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
+		AssignDumpId(&xmlschemainfo[i].dobj);
+		xmlschemainfo[i].dobj.name = pg_strdup(PQgetvalue(res, i, i_schemaname));
+		xmlschemainfo[i].dobj.namespace =
+			findNamespace(atooid(PQgetvalue(res, i, i_schemanamespace)));
+		xmlschemainfo[i].rolname = getRoleName(PQgetvalue(res, i, i_schemaowner));
+
+		selectDumpableObject(&(xmlschemainfo[i].dobj), fout);
+	}
+
+	PQclear(res);
+
+	destroyPQExpBuffer(query);
+}
+
 /*
  * getAccessMethods:
  *	  get information about all user-defined access methods
@@ -11712,6 +11769,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_CONVERSION:
 			dumpConversion(fout, (const ConvInfo *) dobj);
 			break;
+		case DO_XMLSCHEMA:
+			dumpXmlSchema(fout, (const XmlSchemaInfo *) dobj);
+			break;
 		case DO_TABLE:
 			dumpTable(fout, (const TableInfo *) dobj);
 			break;
@@ -15238,6 +15298,73 @@ dumpCollation(Archive *fout, const CollInfo *collinfo)
 	free(qcollname);
 }
 
+/*
+ * dumpXmlSchema
+ *	  write out a single XML schema definition
+ */
+static void
+dumpXmlSchema(Archive *fout, const XmlSchemaInfo * xmlschemainfo)
+{
+	DumpOptions *dopt = fout->dopt;
+	PQExpBuffer query;
+	PQExpBuffer q;
+	PQExpBuffer delq;
+	char	   *qxmlschemaname;
+	PGresult   *res;
+	int			i_schemadata;
+	char	   *schemadata;
+
+	if (!dopt->dumpSchema)
+		return;
+
+	query = createPQExpBuffer();
+	q = createPQExpBuffer();
+	delq = createPQExpBuffer();
+
+	qxmlschemaname = pg_strdup(fmtId(xmlschemainfo->dobj.name));
+
+	appendPQExpBuffer(query,
+					  "SELECT schemadata "
+					  "FROM pg_catalog.pg_xmlschema "
+					  "WHERE oid = '%u'::pg_catalog.oid",
+					  xmlschemainfo->dobj.catId.oid);
+
+	res = ExecuteSqlQueryForSingleRow(fout, query->data);
+
+	i_schemadata = PQfnumber(res, "schemadata");
+	schemadata = PQgetvalue(res, 0, i_schemadata);
+
+	appendPQExpBuffer(delq, "DROP XMLSCHEMA %s;\n",
+					  fmtQualifiedDumpable(xmlschemainfo));
+
+	appendPQExpBuffer(q, "CREATE XMLSCHEMA %s AS ",
+					  fmtQualifiedDumpable(xmlschemainfo));
+	appendStringLiteralAH(q, schemadata, fout);
+	appendPQExpBufferStr(q, ";\n");
+
+	if (xmlschemainfo->dobj.dump & DUMP_COMPONENT_DEFINITION)
+		ArchiveEntry(fout, xmlschemainfo->dobj.catId, xmlschemainfo->dobj.dumpId,
+					 ARCHIVE_OPTS(.tag = xmlschemainfo->dobj.name,
+								  .namespace = xmlschemainfo->dobj.namespace->dobj.name,
+								  .owner = xmlschemainfo->rolname,
+								  .description = "XMLSCHEMA",
+								  .section = SECTION_PRE_DATA,
+								  .createStmt = q->data,
+								  .dropStmt = delq->data));
+
+	if (xmlschemainfo->dobj.dump & DUMP_COMPONENT_COMMENT)
+		dumpComment(fout, "XMLSCHEMA", qxmlschemaname,
+					xmlschemainfo->dobj.namespace->dobj.name, xmlschemainfo->rolname,
+					xmlschemainfo->dobj.catId, 0, xmlschemainfo->dobj.dumpId);
+
+	PQclear(res);
+
+	destroyPQExpBuffer(query);
+	destroyPQExpBuffer(q);
+	destroyPQExpBuffer(delq);
+	free(qxmlschemaname);
+}
+
 /*
  * dumpConversion
  *	  write out a single conversion definition
@@ -20408,6 +20535,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_OPFAMILY:
 			case DO_COLLATION:
 			case DO_CONVERSION:
+			case DO_XMLSCHEMA:
 			case DO_TABLE:
 			case DO_TABLE_ATTACH:
 			case DO_ATTRDEF:
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 4c4b14e5fc7..bc1cf5a00d2 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -50,6 +50,7 @@ typedef enum
 	DO_OPFAMILY,
 	DO_COLLATION,
 	DO_CONVERSION,
+	DO_XMLSCHEMA,
 	DO_TABLE,
 	DO_TABLE_ATTACH,
 	DO_ATTRDEF,
@@ -299,6 +300,12 @@ typedef struct _convInfo
 	const char *rolname;
 } ConvInfo;
 
+typedef struct _xmlSchemaInfo
+{
+	DumpableObject dobj;
+	const char *rolname;
+}			XmlSchemaInfo;
+
 typedef struct _tableInfo
 {
 	/*
@@ -797,6 +804,7 @@ extern void getOpclasses(Archive *fout);
 extern void getOpfamilies(Archive *fout);
 extern void getCollations(Archive *fout);
 extern void getConversions(Archive *fout);
+extern void getXmlSchemas(Archive *fout);
 extern TableInfo *getTables(Archive *fout, int *numTables);
 extern void getOwnedSeqs(Archive *fout, TableInfo tblinfo[], int numTables);
 extern InhInfo *getInherits(Archive *fout, int *numInherits);
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 03e5c1c1116..0ec7dd33ac3 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -1575,6 +1575,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "CONVERSION %s  (ID %d OID %u)",
 					 obj->name, obj->dumpId, obj->catId.oid);
 			return;
+		case DO_XMLSCHEMA:
+			snprintf(buf, bufsize,
+					 "XMLSCHEMA %s  (ID %d OID %u)",
+					 obj->name, obj->dumpId, obj->catId.oid);
+			return;
 		case DO_TABLE:
 			snprintf(buf, bufsize,
 					 "TABLE %s  (ID %d OID %u)",
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 8b91bc00062..6c51cd6c9be 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -797,6 +797,13 @@ static const SchemaQuery Query_for_list_of_matviews = {
 	.result = "c.relname",
 };
 
+static const SchemaQuery Query_for_list_of_xmlschemas = {
+	.catname = "pg_catalog.pg_xmlschema s",
+	.viscondition = "EXISTS (SELECT 1 FROM pg_catalog.pg_namespace n WHERE n.oid = s.schemanamespace AND n.nspname = ANY(pg_catalog.current_schemas(true)))",
+	.namespace = "s.schemanamespace",
+	.result = "s.schemaname",
+};
+
 static const SchemaQuery Query_for_list_of_indexes = {
 	.catname = "pg_catalog.pg_class c",
 	.selcondition =
@@ -1364,6 +1371,7 @@ static const pgsql_thing_t words_after_create[] = {
 	{"USER", Query_for_list_of_roles, NULL, NULL, Keywords_for_user_thing},
 	{"USER MAPPING FOR", NULL, NULL, NULL},
 	{"VIEW", NULL, NULL, &Query_for_list_of_views},
+	{"XMLSCHEMA", NULL, NULL, &Query_for_list_of_xmlschemas},
 	{NULL}						/* end of list */
 };
 
@@ -2701,6 +2709,10 @@ match_previous_words(int pattern_id,
 	else if (Matches("ALTER", "MATERIALIZED", "VIEW", MatchAny, "SET", "ACCESS", "METHOD"))
 		COMPLETE_WITH_QUERY(Query_for_list_of_table_access_methods);
 
+	/* ALTER XMLSCHEMA <name> */
+	else if (Matches("ALTER", "XMLSCHEMA", MatchAny))
+		COMPLETE_WITH("OWNER TO", "RENAME TO", "SET SCHEMA");
+
 	/* ALTER POLICY <name> */
 	else if (Matches("ALTER", "POLICY"))
 		COMPLETE_WITH_QUERY(Query_for_list_of_policies);
@@ -4178,6 +4190,16 @@ match_previous_words(int pattern_id,
 			 Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "USING", MatchAny, "AS"))
 		COMPLETE_WITH("SELECT");
 
+/* CREATE XMLSCHEMA */
+	else if (Matches("CREATE", "XMLSCHEMA", MatchAny))
+		COMPLETE_WITH("AS", "IF");
+	else if (Matches("CREATE", "XMLSCHEMA", "IF"))
+		COMPLETE_WITH("NOT");
+	else if (Matches("CREATE", "XMLSCHEMA", "IF", "NOT"))
+		COMPLETE_WITH("EXISTS");
+	else if (Matches("CREATE", "XMLSCHEMA", "IF", "NOT", "EXISTS", MatchAny))
+		COMPLETE_WITH("AS");
+
 /* CREATE EVENT TRIGGER */
 	else if (Matches("CREATE", "EVENT"))
 		COMPLETE_WITH("TRIGGER");
diff --git a/src/include/catalog/Makefile b/src/include/catalog/Makefile
index 24b527230d4..7361dc6cd3f 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 433bcc908ad..1fdc6ac4e00 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..3261420f2bb
--- /dev/null
+++ b/src/include/catalog/pg_xmlschema.h
@@ -0,0 +1,48 @@
+#ifndef PG_XMLSCHEMA_H
+#define PG_XMLSCHEMA_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_xmlschema_d.h"
+#include "utils/xml.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
+	/* The XSD itself */
+	xml			schemadata BKI_FORCE_NOT_NULL;
+
+	/* 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,
+							xmltype *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..302cb791f32
--- /dev/null
+++ b/src/include/commands/xmlschemacmds.h
@@ -0,0 +1,9 @@
+#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);
+
+#endif							/* XMLSCHEMACMDS_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 0aec49bdd22..b9f0476596e 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2403,6 +2403,7 @@ typedef enum ObjectType
 	OBJECT_TYPE,
 	OBJECT_USER_MAPPING,
 	OBJECT_VIEW,
+	OBJECT_XMLSCHEMA,
 } ObjectType;
 
 /* ----------------------
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index f7753c5c8a8..a018c2b9e08 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -520,6 +520,7 @@ 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("year", YEAR_P, UNRESERVED_KEYWORD, AS_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/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out
index 25aaae8d05a..246deabd17b 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..4e24657d2d3 100644
--- a/src/test/regress/expected/xml.out
+++ b/src/test/regress/expected/xml.out
@@ -1881,3 +1881,67 @@ 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>';
diff --git a/src/test/regress/expected/xml_1.out b/src/test/regress/expected/xml_1.out
index 73c411118a3..aded57c78b2 100644
--- a/src/test/regress/expected/xml_1.out
+++ b/src/test/regress/expected/xml_1.out
@@ -1496,3 +1496,64 @@ ERROR:  unsupported XML feature
 LINE 1: SELECT xmltext('x'|| '<P>73</P>'::xml || .42 || true || 'j':...
                              ^
 DETAIL:  This functionality requires the server to be built with libxml support.
+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>';
+ERROR:  xmlschema support requires libxml
+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>';
+ERROR:  xmlschema support requires libxml
+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:  xmlschema support requires libxml
+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>';
+ERROR:  xmlschema support requires libxml
+CREATE XMLSCHEMA bad_schema AS '<this-is-not-valid-xsd>';
+ERROR:  xmlschema support requires libxml
+CREATE XMLSCHEMA bad_xml_schema AS 'not even xml';
+ERROR:  xmlschema support requires libxml
+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>';
+ERROR:  xmlschema support requires libxml
diff --git a/src/test/regress/expected/xml_2.out b/src/test/regress/expected/xml_2.out
index a85d95358d9..4e24657d2d3 100644
--- a/src/test/regress/expected/xml_2.out
+++ b/src/test/regress/expected/xml_2.out
@@ -9,6 +9,8 @@ ERROR:  invalid XML content
 LINE 1: INSERT INTO xmltest VALUES (3, '<wrong');
                                        ^
 DETAIL:  line 1: Couldn't find end of Start Tag wrong line 1
+<wrong
+      ^
 SELECT * FROM xmltest;
  id |        data        
 ----+--------------------
@@ -92,6 +94,8 @@ ERROR:  invalid XML content
 LINE 1: SELECT xmlconcat('bad', '<syntax');
                                 ^
 DETAIL:  line 1: Couldn't find end of Start Tag syntax line 1
+<syntax
+       ^
 SELECT xmlconcat('<foo/>', NULL, '<?xml version="1.1" standalone="no"?><bar/>');
   xmlconcat   
 --------------
@@ -273,6 +277,8 @@ DETAIL:  line 1: Entity 'idontexist' not defined
 <twoerrors>&idontexist;</unbalanced>
                        ^
 line 1: Opening and ending tag mismatch: twoerrors line 1 and unbalanced
+<twoerrors>&idontexist;</unbalanced>
+                                    ^
 SELECT xmlparse(content '<nosuchprefix:tag/>');
       xmlparse       
 ---------------------
@@ -282,6 +288,8 @@ SELECT xmlparse(content '<nosuchprefix:tag/>');
 SELECT xmlparse(document '   ');
 ERROR:  invalid XML document
 DETAIL:  line 1: Start tag expected, '<' not found
+   
+   ^
 SELECT xmlparse(document 'abc');
 ERROR:  invalid XML document
 DETAIL:  line 1: Start tag expected, '<' not found
@@ -299,12 +307,16 @@ DETAIL:  line 1: xmlParseEntityRef: no name
 <invalidentity>&</abc>
                 ^
 line 1: Opening and ending tag mismatch: invalidentity line 1 and abc
+<invalidentity>&</abc>
+                      ^
 SELECT xmlparse(document '<undefinedentity>&idontexist;</abc>');
 ERROR:  invalid XML document
 DETAIL:  line 1: Entity 'idontexist' not defined
 <undefinedentity>&idontexist;</abc>
                              ^
 line 1: Opening and ending tag mismatch: undefinedentity line 1 and abc
+<undefinedentity>&idontexist;</abc>
+                                   ^
 SELECT xmlparse(document '<invalidns xmlns=''&lt;''/>');
          xmlparse          
 ---------------------------
@@ -323,6 +335,8 @@ DETAIL:  line 1: Entity 'idontexist' not defined
 <twoerrors>&idontexist;</unbalanced>
                        ^
 line 1: Opening and ending tag mismatch: twoerrors line 1 and unbalanced
+<twoerrors>&idontexist;</unbalanced>
+                                    ^
 SELECT xmlparse(document '<nosuchprefix:tag/>');
       xmlparse       
 ---------------------
@@ -1867,3 +1881,67 @@ 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>';
diff --git a/src/test/regress/sql/xml.sql b/src/test/regress/sql/xml.sql
index 0ea4f508837..667973420fa 100644
--- a/src/test/regress/sql/xml.sql
+++ b/src/test/regress/sql/xml.sql
@@ -679,3 +679,66 @@ 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>';
-- 
2.52.0



  [application/octet-stream] v5-0002-Add-XMLVALIDATE.patch (63.5K, 4-v5-0002-Add-XMLVALIDATE.patch)
  download | inline diff:
From e030fc16e9081bc569e6e2b8cbd3b522e4f64160 Mon Sep 17 00:00:00 2001
From: Jim Jones <[email protected]>
Date: Thu, 5 Feb 2026 09:00:00 +0100
Subject: [PATCH v5 2/2] Add XMLVALIDATE

---
 doc/src/sgml/datatype.sgml            |   7 +-
 doc/src/sgml/func/func-xml.sgml       | 105 ++++++++++++
 src/backend/catalog/dependency.c      |  28 ++++
 src/backend/executor/execExprInterp.c |  27 +++
 src/backend/parser/gram.y             |  19 ++-
 src/backend/parser/parse_expr.c       |  43 +++++
 src/backend/parser/parse_target.c     |   3 +
 src/backend/utils/adt/ruleutils.c     |  17 +-
 src/backend/utils/adt/xml.c           | 144 +++++++++++++++-
 src/include/nodes/primnodes.h         |   1 +
 src/include/parser/kwlist.h           |   2 +
 src/include/utils/xml.h               |   1 +
 src/test/regress/expected/xml.out     | 233 ++++++++++++++++++++++++++
 src/test/regress/expected/xml_1.out   | 208 +++++++++++++++++++++++
 src/test/regress/expected/xml_2.out   | 233 ++++++++++++++++++++++++++
 src/test/regress/sql/xml.sql          | 209 +++++++++++++++++++++++
 16 files changed, 1272 insertions(+), 8 deletions(-)

diff --git a/doc/src/sgml/datatype.sgml b/doc/src/sgml/datatype.sgml
index f2ade57fe7..ddaf7c4d3d 100644
--- a/doc/src/sgml/datatype.sgml
+++ b/doc/src/sgml/datatype.sgml
@@ -4510,8 +4510,11 @@ xml '<foo>bar</foo>'
     against a document type declaration
     (DTD),<indexterm><primary>DTD</primary></indexterm>
     even when the input value specifies a DTD.
-    There is also currently no built-in support for validating against
-    other XML schema languages such as XML Schema.
+    However, <productname>PostgreSQL</productname> provides the
+    <function>XMLVALIDATE</function> function for validating XML documents
+    against XML Schema (XSD) documents. See
+    <xref linkend="functions-xml-processing-xmlvalidate"/> and
+    <xref linkend="sql-createxmlschema"/> for more information.
    </para>
 
    <para>
diff --git a/doc/src/sgml/func/func-xml.sgml b/doc/src/sgml/func/func-xml.sgml
index 511bc90852..c4a6e89d0e 100644
--- a/doc/src/sgml/func/func-xml.sgml
+++ b/doc/src/sgml/func/func-xml.sgml
@@ -1007,6 +1007,111 @@ SELECT xmltable.*
    3 |   4
    4 |   5
 (3 rows)
+]]></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> <replaceable>xml_value</replaceable> <literal>ACCORDING TO XMLSCHEMA</literal> <replaceable>schema_name</replaceable> ) <returnvalue>xml</returnvalue>
+</synopsis>
+
+    <para>
+     The <function>xmlvalidate</function> function validates an XML document
+     against an XML Schema. The schema must have been previously created using
+     <command>CREATE XMLSCHEMA</command>. If the XML is valid according to the
+     schema, the function returns the XML value unchanged. If validation fails,
+     an error is raised. If <replaceable>xml_value</replaceable> is
+     <literal>NULL</literal>, the function returns <literal>NULL</literal>.
+    </para>
+
+    <para>
+     The <replaceable>schema_name</replaceable> argument specifies the name
+     of an XML schema stored in the database. It can be schema-qualified
+     (e.g., <literal>myschema.person_schema</literal>) or unqualified, in
+     which case it will be searched for in the current schema search path.
+     The user must have <literal>USAGE</literal> privilege on the schema.
+    </para>
+
+    <para>
+     Examples:
+<screen><![CDATA[
+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>';
+
+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)
+]]></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 person_schema);
+ERROR:  XML validation failed
+]]></screen>
+    </para>
+
+    <para>
+     Schema-qualified names are supported:
+<screen><![CDATA[
+CREATE SCHEMA test_schema;
+CREATE XMLSCHEMA test_schema.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>';
+
+SELECT xmlvalidate(DOCUMENT '<product id="123"><name>Widget</name><price>9.99</price></product>'
+  ACCORDING TO XMLSCHEMA test_schema.product_schema);
+
+                          xmlvalidate
+---------------------------------------------------------------
+ <product id="123"><name>Widget</name><price>9.99</price></product>
+(1 row)
+]]></screen>
+    </para>
+
+    <para>
+     Since <function>xmlvalidate</function> returns <type>xml</type>, it can
+     be composed with other XML functions:
+<screen><![CDATA[
+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)
 ]]></screen>
     </para>
    </sect3>
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index 0b8d8e3152..50ce3871e7 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -2419,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/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
index 61ff5ddc74..7ab442d6bf 100644
--- a/src/backend/executor/execExprInterp.c
+++ b/src/backend/executor/execExprInterp.c
@@ -4630,6 +4630,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 9886250c25..9efc1e97d1 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
 
@@ -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 XMLSCHEMA XMLSERIALIZE XMLTABLE
+	XMLPARSE XMLPI XMLROOT XMLSCHEMA XMLSERIALIZE XMLTABLE XMLVALIDATE
 
 	YEAR_P YES_P
 
@@ -16348,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() */
@@ -17958,6 +17969,7 @@ unreserved_keyword:
 			| ABSENT
 			| ABSOLUTE_P
 			| ACCESS
+			| ACCORDING
 			| ACTION
 			| ADD_P
 			| ADMIN
@@ -18367,6 +18379,7 @@ col_name_keyword:
 			| XMLROOT
 			| XMLSERIALIZE
 			| XMLTABLE
+			| XMLVALIDATE
 		;
 
 /* Type/function identifier --- keywords that can be type or function names.
@@ -18506,6 +18519,7 @@ bare_label_keyword:
 			| ABSENT
 			| ABSOLUTE_P
 			| ACCESS
+			| ACCORDING
 			| ACTION
 			| ADD_P
 			| ADMIN
@@ -18960,6 +18974,7 @@ bare_label_keyword:
 			| XMLSCHEMA
 			| XMLSERIALIZE
 			| XMLTABLE
+			| XMLVALIDATE
 			| YES_P
 			| ZONE
 		;
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index dcfe1acc4c..098f13f665 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"
@@ -2370,6 +2371,7 @@ transformXmlExpr(ParseState *pstate, XmlExpr *x)
 	XmlExpr    *newx;
 	ListCell   *lc;
 	int			i;
+	Oid			xmlvalidate_schema_oid = InvalidOid;
 
 	newx = makeNode(XmlExpr);
 	newx->op = x->op;
@@ -2382,6 +2384,22 @@ 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.
@@ -2481,6 +2499,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");
@@ -2490,6 +2513,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 b5a2f915b6..b380bb39eb 100644
--- a/src/backend/parser/parse_target.c
+++ b/src/backend/parser/parse_target.c
@@ -1976,6 +1976,9 @@ FigureColnameInternal(Node *node, char **name)
 				case IS_XMLSERIALIZE:
 					*name = "xmlserialize";
 					return 2;
+				case IS_XMLVALIDATE:
+					*name = "xmlvalidate";
+					return 2;
 				case IS_DOCUMENT:
 					/* nothing */
 					break;
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index b5a7ad9066..cd8a9169a6 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)));
@@ -10173,6 +10176,9 @@ get_rule_expr(Node *node, deparse_context *context,
 							/* no extra decoration needed */
 							get_rule_expr((Node *) xexpr->args, context, true);
 							break;
+						case IS_XMLVALIDATE:
+							get_rule_expr((Node *) linitial(xexpr->args), context, true);
+							break;
 						case IS_XMLPARSE:
 							Assert(list_length(xexpr->args) == 2);
 
@@ -10242,6 +10248,13 @@ get_rule_expr(Node *node, deparse_context *context,
 						appendStringInfoString(buf, " NO INDENT");
 				}
 
+				if (xexpr->op == IS_XMLVALIDATE)
+				{
+					/* Output the schema name stored during parse analysis */
+					appendStringInfo(buf, " ACCORDING TO XMLSCHEMA %s",
+									 quote_identifier(xexpr->name));
+				}
+
 				if (xexpr->op == IS_DOCUMENT)
 					appendStringInfoString(buf, " IS DOCUMENT");
 				else
diff --git a/src/backend/utils/adt/xml.c b/src/backend/utils/adt/xml.c
index f69dc68286..d57c3c07f9 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,147 @@ 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;
+	xmltype    *schema_xml;
+	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_xml = DatumGetXmlP(schema_datum);
+	schemastr = text_to_cstring(schema_xml);
+	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 +1321,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/nodes/primnodes.h b/src/include/nodes/primnodes.h
index 5211cadc25..c40cbc8981 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 a018c2b9e0..4e8b5e1d7e 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)
@@ -523,6 +524,7 @@ PG_KEYWORD("xmlroot", XMLROOT, COL_NAME_KEYWORD, BARE_LABEL)
 PG_KEYWORD("xmlschema", XMLSCHEMA, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("xmlserialize", XMLSERIALIZE, COL_NAME_KEYWORD, BARE_LABEL)
 PG_KEYWORD("xmltable", XMLTABLE, COL_NAME_KEYWORD, BARE_LABEL)
+PG_KEYWORD("xmlvalidate", XMLVALIDATE, COL_NAME_KEYWORD, BARE_LABEL)
 PG_KEYWORD("year", YEAR_P, UNRESERVED_KEYWORD, AS_LABEL)
 PG_KEYWORD("yes", YES_P, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("zone", ZONE, UNRESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/include/utils/xml.h b/src/include/utils/xml.h
index 03acb25544..dc6a4d3784 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/xml.out b/src/test/regress/expected/xml.out
index 4e24657d2d..121d86d85d 100644
--- a/src/test/regress/expected/xml.out
+++ b/src/test/regress/expected/xml.out
@@ -1945,3 +1945,236 @@ CREATE XMLSCHEMA book_schema AS '<?xml version="1.0"?>
     </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_1.out b/src/test/regress/expected/xml_1.out
index aded57c78b..7b088380c4 100644
--- a/src/test/regress/expected/xml_1.out
+++ b/src/test/regress/expected/xml_1.out
@@ -1557,3 +1557,211 @@ CREATE XMLSCHEMA book_schema AS '<?xml version="1.0"?>
   </xs:element>
 </xs:schema>';
 ERROR:  xmlschema support requires libxml
+SELECT XMLVALIDATE(DOCUMENT '<person><name>John</name><age>30</age></person>'
+  ACCORDING TO XMLSCHEMA person_schema);
+ERROR:  XML schema "person_schema" does not exist
+SELECT XMLVALIDATE(DOCUMENT '<product id="P123"><name>Widget</name><price>9.99</price></product>'
+  ACCORDING TO XMLSCHEMA test_xmlschema_ns.product_schema);
+ERROR:  XML schema "test_xmlschema_ns.product_schema" does not exist
+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 schema "book_schema" does not exist
+SELECT XMLSERIALIZE(DOCUMENT
+  XMLVALIDATE(DOCUMENT '<person><name>Alice</name><age>25</age></person>'
+    ACCORDING TO XMLSCHEMA person_schema) AS text);
+ERROR:  XML schema "person_schema" does not exist
+SELECT XMLVALIDATE(DOCUMENT NULL ACCORDING TO XMLSCHEMA person_schema);
+ERROR:  XML schema "person_schema" does not exist
+SELECT XMLVALIDATE(DOCUMENT NULL ACCORDING TO XMLSCHEMA person_schema) IS NULL AS is_null;
+ERROR:  XML schema "person_schema" does not exist
+SELECT XMLVALIDATE(DOCUMENT '<person><name>John</name></person>'
+  ACCORDING TO XMLSCHEMA person_schema);
+ERROR:  XML schema "person_schema" does not exist
+SELECT XMLVALIDATE(DOCUMENT '<person><name>John</name><age>not-a-number</age></person>'
+  ACCORDING TO XMLSCHEMA person_schema);
+ERROR:  XML schema "person_schema" does not exist
+SELECT XMLVALIDATE(DOCUMENT '<product><name>Widget</name><price>9.99</price></product>'
+  ACCORDING TO XMLSCHEMA test_xmlschema_ns.product_schema);
+ERROR:  XML schema "test_xmlschema_ns.product_schema" does not exist
+SELECT XMLVALIDATE(DOCUMENT '<person><name>John</name><age>30</age><extra>data</extra></person>'
+  ACCORDING TO XMLSCHEMA person_schema);
+ERROR:  XML schema "person_schema" does not exist
+SELECT XMLVALIDATE(DOCUMENT '<notperson><name>John</name><age>30</age></notperson>'
+  ACCORDING TO XMLSCHEMA person_schema);
+ERROR:  XML schema "person_schema" does not exist
+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;
+ERROR:  XML schema "person_schema" does not exist
+SELECT pg_get_viewdef('validated_people'::regclass, true);
+ERROR:  relation "validated_people" does not exist
+LINE 1: SELECT pg_get_viewdef('validated_people'::regclass, true);
+                              ^
+DROP VIEW validated_people;
+ERROR:  view "validated_people" does not exist
+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;
+ERROR:  XML schema "test_xmlschema_ns.product_schema" does not exist
+SELECT pg_get_viewdef('validated_products'::regclass, true);
+ERROR:  relation "validated_products" does not exist
+LINE 1: SELECT pg_get_viewdef('validated_products'::regclass, true);
+                              ^
+DROP VIEW validated_products;
+ERROR:  view "validated_products" does not exist
+ALTER XMLSCHEMA book_schema RENAME TO library_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 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);
+ERROR:  XML schema "library_book_schema" does not exist
+ALTER XMLSCHEMA library_book_schema SET SCHEMA test_xmlschema_ns;
+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 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);
+ERROR:  XML schema "test_xmlschema_ns.library_book_schema" does not exist
+CREATE ROLE regress_xmlschema_test_role;
+ALTER XMLSCHEMA test_xmlschema_ns.library_book_schema OWNER TO regress_xmlschema_test_role;
+ERROR:  XML schema "test_xmlschema_ns.library_book_schema" does not exist
+SELECT schemaname, schemanamespace::regnamespace, schemaowner::regrole
+FROM pg_xmlschema
+WHERE schemaname = 'library_book_schema';
+ schemaname | schemanamespace | schemaowner 
+------------+-----------------+-------------
+(0 rows)
+
+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;
+ERROR:  XML schema "test_xmlschema_ns.library_book_schema" does not exist
+DROP XMLSCHEMA test_xmlschema_ns.library_book_schema;
+ERROR:  XML schema "test_xmlschema_ns.library_book_schema" does not exist
+DROP XMLSCHEMA test_xmlschema_ns.library_book_schema CASCADE;
+ERROR:  XML schema "test_xmlschema_ns.library_book_schema" does not exist
+SELECT * FROM book_view;
+ERROR:  relation "book_view" does not exist
+LINE 1: SELECT * FROM book_view;
+                      ^
+DROP XMLSCHEMA person_schema;
+ERROR:  XML schema "person_schema" does not exist
+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;
+ERROR:  XML schema "test_xmlschema_ns.product_schema" does not exist
+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>';
+ERROR:  xmlschema support requires libxml
+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>';
+ERROR:  xmlschema support requires libxml
+RESET ROLE;
+DROP XMLSCHEMA test_xmlschema_ns.role_schema;
+ERROR:  XML schema "test_xmlschema_ns.role_schema" does not exist
+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
+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>';
+ERROR:  xmlschema support requires libxml
+SET ROLE regress_xmlschema_user1;
+SELECT XMLVALIDATE(DOCUMENT '<test>data</test>'
+                   ACCORDING TO XMLSCHEMA permission_test_schema);
+ERROR:  XML schema "permission_test_schema" does not exist
+RESET ROLE;
+GRANT USAGE ON XMLSCHEMA permission_test_schema TO regress_xmlschema_user1;
+ERROR:  XML schema "permission_test_schema" does not exist
+SET ROLE regress_xmlschema_user1;
+SELECT XMLVALIDATE(DOCUMENT '<test>data</test>'
+                   ACCORDING TO XMLSCHEMA permission_test_schema);
+ERROR:  XML schema "permission_test_schema" does not exist
+RESET ROLE;
+SET ROLE regress_xmlschema_user2;
+SELECT XMLVALIDATE(DOCUMENT '<test>data</test>'
+                   ACCORDING TO XMLSCHEMA permission_test_schema);
+ERROR:  XML schema "permission_test_schema" does not exist
+RESET ROLE;
+GRANT USAGE ON XMLSCHEMA permission_test_schema TO regress_xmlschema_user2;
+ERROR:  XML schema "permission_test_schema" does not exist
+SET ROLE regress_xmlschema_user2;
+SELECT XMLVALIDATE(DOCUMENT '<test>data</test>'
+                   ACCORDING TO XMLSCHEMA permission_test_schema);
+ERROR:  XML schema "permission_test_schema" does not exist
+RESET ROLE;
+REVOKE USAGE ON XMLSCHEMA permission_test_schema FROM regress_xmlschema_user1;
+ERROR:  XML schema "permission_test_schema" does not exist
+SET ROLE regress_xmlschema_user1;
+SELECT XMLVALIDATE(DOCUMENT '<test>data</test>'
+                   ACCORDING TO XMLSCHEMA permission_test_schema);
+ERROR:  XML schema "permission_test_schema" does not exist
+RESET ROLE;
+DROP XMLSCHEMA permission_test_schema;
+ERROR:  XML schema "permission_test_schema" does not exist
+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>';
+ERROR:  xmlschema support requires libxml
+INSERT INTO validated_xml_data (data)
+VALUES (XMLVALIDATE(DOCUMENT '<data><value>42</value></data>'
+  ACCORDING TO XMLSCHEMA data_schema));
+ERROR:  XML schema "data_schema" does not exist
+INSERT INTO validated_xml_data (data)
+VALUES (XMLVALIDATE(DOCUMENT '<data><value>100</value></data>'
+  ACCORDING TO XMLSCHEMA data_schema));
+ERROR:  XML schema "data_schema" does not exist
+SELECT id, data FROM validated_xml_data ORDER BY id;
+ id | data 
+----+------
+(0 rows)
+
+INSERT INTO validated_xml_data (data)
+VALUES (XMLVALIDATE(DOCUMENT '<data><value>not-an-int</value></data>'
+  ACCORDING TO XMLSCHEMA data_schema));
+ERROR:  XML schema "data_schema" does not exist
+DROP TABLE validated_xml_data;
+DROP XMLSCHEMA data_schema;
+ERROR:  XML schema "data_schema" does not exist
+DROP XMLSCHEMA should_fail_schema;
+ERROR:  XML schema "should_fail_schema" does not exist
+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 4e24657d2d..121d86d85d 100644
--- a/src/test/regress/expected/xml_2.out
+++ b/src/test/regress/expected/xml_2.out
@@ -1945,3 +1945,236 @@ CREATE XMLSCHEMA book_schema AS '<?xml version="1.0"?>
     </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 667973420f..c1650ead94 100644
--- a/src/test/regress/sql/xml.sql
+++ b/src/test/regress/sql/xml.sql
@@ -742,3 +742,212 @@ CREATE XMLSCHEMA book_schema AS '<?xml version="1.0"?>
     </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;
\ No newline at end of file
-- 
2.43.0



^ permalink  raw  reply  [nested|flat] 9+ messages in thread

* Re: WIP - xmlvalidate implementation from TODO list
  2026-01-23 12:19 Re: WIP - xmlvalidate implementation from TODO list Jim Jones <[email protected]>
  2026-01-23 15:43 ` Re: WIP - xmlvalidate implementation from TODO list Jim Jones <[email protected]>
  2026-02-05 08:42   ` Re: WIP - xmlvalidate implementation from TODO list Jim Jones <[email protected]>
  2026-02-05 21:18     ` Re: WIP - xmlvalidate implementation from TODO list Marcos Magueta <[email protected]>
  2026-02-14 22:30       ` Re: WIP - xmlvalidate implementation from TODO list Marcos Magueta <[email protected]>
  2026-02-14 23:31         ` Re: WIP - xmlvalidate implementation from TODO list Jim Jones <[email protected]>
  2026-02-18 19:20           ` Re: WIP - xmlvalidate implementation from TODO list Marcos Magueta <[email protected]>
@ 2026-02-19 07:55             ` Jim Jones <[email protected]>
  2026-02-22 05:11               ` Re: WIP - xmlvalidate implementation from TODO list Marcos Magueta <[email protected]>
  0 siblings, 1 reply; 9+ messages in thread

From: Jim Jones @ 2026-02-19 07:55 UTC (permalink / raw)
  To: Marcos Magueta <[email protected]>; +Cc: Andrey Borodin <[email protected]>; Kirill Reshke <[email protected]>; PostgreSQL Hackers <[email protected]>

Hi Marcos,

Thanks for the update.

On 18/02/2026 20:20, Marcos Magueta wrote:
> Here's the v6 with the docs updated and some minor things. 

Unfortunately, the patch no longer applies.[1]

> I was trying to figure out how to do the roles, and I have some thoughts
> on it now. While I am not particularly against predefined roles, I think
> we should be careful before committing to them in this form. As
> predefined roles, they would effectively become part of a long-term
> public interface: once they exist, we should then carry them forward and
> preserve their semantics across releases, so adding a feature-specific
> role increases permanent surface area (docs, tests, upgrade behavior,
> compatibility expectations) and is harder to revisit later if the
> privilege model evolves (which I think will make people mad, it's XML
> burden after all).
> 
> Also, a write-oriented role is not as straightforward as I thought with
> the current ownership model (and that's me guessing here). Mutating DDL
> behavior is still owner-driven (or superuser-driven) most of the places,
> not purely ACL-driven, so a pg_write_xmlschemas role can look clearer at
> first glance than it is in practice. To make that role fully consistent,
> we may need broader policy decisions around what should be grantable
> versus what should remain ownership-based, not just new role entries.
> Again, I am not opposed to the idea, but these made me a bit anxious to
> simply add them. What are your thoughts on these?

The creation of XML schemas does not need to be specifically controlled
by predefined roles - it was just the first thing that came to mind. My
concern was that leaving it entirely open to any user could lead to
unwanted CPU or memory usage. Perhaps the other reviewers have a
different opinion on this.

Best, Jim

1 - https://cfbot.cputube.org/patch_6372.log






^ permalink  raw  reply  [nested|flat] 9+ messages in thread

* Re: WIP - xmlvalidate implementation from TODO list
  2026-01-23 12:19 Re: WIP - xmlvalidate implementation from TODO list Jim Jones <[email protected]>
  2026-01-23 15:43 ` Re: WIP - xmlvalidate implementation from TODO list Jim Jones <[email protected]>
  2026-02-05 08:42   ` Re: WIP - xmlvalidate implementation from TODO list Jim Jones <[email protected]>
  2026-02-05 21:18     ` Re: WIP - xmlvalidate implementation from TODO list Marcos Magueta <[email protected]>
  2026-02-14 22:30       ` Re: WIP - xmlvalidate implementation from TODO list Marcos Magueta <[email protected]>
  2026-02-14 23:31         ` Re: WIP - xmlvalidate implementation from TODO list Jim Jones <[email protected]>
  2026-02-18 19:20           ` Re: WIP - xmlvalidate implementation from TODO list Marcos Magueta <[email protected]>
  2026-02-19 07:55             ` Re: WIP - xmlvalidate implementation from TODO list Jim Jones <[email protected]>
@ 2026-02-22 05:11               ` Marcos Magueta <[email protected]>
  0 siblings, 0 replies; 9+ messages in thread

From: Marcos Magueta @ 2026-02-22 05:11 UTC (permalink / raw)
  To: Jim Jones <[email protected]>; +Cc: Andrey Borodin <[email protected]>; Kirill Reshke <[email protected]>; PostgreSQL Hackers <[email protected]>

> Unfortunately, the patch no longer applies.[1]
Trying to understand why it doesn't, we can get back to that.

> My concern was that leaving it entirely open to any user could lead to
> unwanted CPU or memory usage. Perhaps the other reviewers have a
> different opinion on this.

That's a fair concern. Is there even an alternative aside from predefined
roles?


^ permalink  raw  reply  [nested|flat] 9+ messages in thread


end of thread, other threads:[~2026-02-22 05:11 UTC | newest]

Thread overview: 9+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2026-01-23 12:19 Re: WIP - xmlvalidate implementation from TODO list Jim Jones <[email protected]>
2026-01-23 15:43 ` Jim Jones <[email protected]>
2026-02-05 08:42   ` Jim Jones <[email protected]>
2026-02-05 21:18     ` Marcos Magueta <[email protected]>
2026-02-14 22:30       ` Marcos Magueta <[email protected]>
2026-02-14 23:31         ` Jim Jones <[email protected]>
2026-02-18 19:20           ` Marcos Magueta <[email protected]>
2026-02-19 07:55             ` Jim Jones <[email protected]>
2026-02-22 05:11               ` Marcos Magueta <[email protected]>

This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox