Implementing SQL ASSERTION

Started by Joe Wildishover 10 years ago33 messages
#1Joe Wildish
joe-postgresql.com@elusive.cx

Hi all,

I’m wondering if there are other people out there working on implementing SQL ASSERTION functionality?

I’ve recently spent a bit of time looking to implement the execution models described in “Applied Mathematics for Database Professionals” by Toon Koppelaars and Lex de Haan. I’ve gotten as far as execution model 3 and am now looking at deriving polarity of involved tables to do EM4 (described in some detail in “Deriving Production Rules for Constraint Maintenance”, Ceri & Widom, VLDB Conference 1990, p555-577). EM5 & EM6 look rather more difficult but I’m intending to try and implement those, too.

If there are other people working on this stuff it would be great to collaborate.

Regards.
-Joe

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#2Robert Haas
robertmhaas@gmail.com
In reply to: Joe Wildish (#1)
Re: Implementing SQL ASSERTION

On Thu, Apr 30, 2015 at 6:36 PM, Joe Wildish
<joe-postgresql.com@elusive.cx> wrote:

I’m wondering if there are other people out there working on implementing SQL ASSERTION functionality?

I’ve recently spent a bit of time looking to implement the execution models described in “Applied Mathematics for Database Professionals” by Toon Koppelaars and Lex de Haan. I’ve gotten as far as execution model 3 and am now looking at deriving polarity of involved tables to do EM4 (described in some detail in “Deriving Production Rules for Constraint Maintenance”, Ceri & Widom, VLDB Conference 1990, p555-577). EM5 & EM6 look rather more difficult but I’m intending to try and implement those, too.

If there are other people working on this stuff it would be great to collaborate.

I don't know of anyone working on this. It sounds very difficult.

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#3Joe Wildish
joe-postgresql.com@elusive.cx
In reply to: Robert Haas (#2)
Re: Implementing SQL ASSERTION

On 1 May 2015, at 19:51, Robert Haas <robertmhaas@gmail.com> wrote:

On Thu, Apr 30, 2015 at 6:36 PM, Joe Wildish
<joe-postgresql.com@elusive.cx> wrote:

I’m wondering if there are other people out there working on implementing SQL ASSERTION functionality?

I’ve recently spent a bit of time looking to implement the execution models described in “Applied Mathematics for Database Professionals” by Toon Koppelaars and Lex de Haan. I’ve gotten as far as execution model 3 and am now looking at deriving polarity of involved tables to do EM4 (described in some detail in “Deriving Production Rules for Constraint Maintenance”, Ceri & Widom, VLDB Conference 1990, p555-577). EM5 & EM6 look rather more difficult but I’m intending to try and implement those, too.

If there are other people working on this stuff it would be great to collaborate.

I don't know of anyone working on this. It sounds very difficult.

The book I mention details a series of execution models, where each successive model aims to validate the assertion in a more efficient manner than the last. This is achieved by performing static analysis of the assertion's expression to determine under what circumstances the assertion need be (re)checked. Briefly:

EM1: after all DML statements;
EM2: only after DML statements involving tables mentioned in the assertion expression;
EM3: only after DML statements involving the columns mentioned in the assertion expression;
EM4: only after DML statements involving the columns, plus if the statement has a “polarity” that may affect the assertion expression.

“Polarity" here means that one is able to (statically) determine if only INSERTS and not DELETES can affect an expression or vice-versa.

EMs 5 and 6 are further enhancements that make use of querying the “transition effect” data of what actually changed in a statement, to determine if the assertion expression need be validated. I’ve not done as much reading around this topic yet so am concentrating on EMs 1-4.

I agree it is a difficult problem but there are a fair number of published academic papers relating to this topic. The AM4DP book draws a lot of this research together and presents the executions models.

I may start writing up on a blog of where I get to, and then post further to this list, if there is interest.

Regards.
-Joe

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#4David Fetter
david@fetter.org
In reply to: Joe Wildish (#3)
Re: Implementing SQL ASSERTION

On Sat, May 02, 2015 at 10:42:24PM +0100, Joe Wildish wrote:

On 1 May 2015, at 19:51, Robert Haas <robertmhaas@gmail.com> wrote:

On Thu, Apr 30, 2015 at 6:36 PM, Joe Wildish
<joe-postgresql.com@elusive.cx> wrote:

I’m wondering if there are other people out there working on implementing SQL ASSERTION functionality?

I’ve recently spent a bit of time looking to implement the execution models described in “Applied Mathematics for Database Professionals” by Toon Koppelaars and Lex de Haan. I’ve gotten as far as execution model 3 and am now looking at deriving polarity of involved tables to do EM4 (described in some detail in “Deriving Production Rules for Constraint Maintenance”, Ceri & Widom, VLDB Conference 1990, p555-577). EM5 & EM6 look rather more difficult but I’m intending to try and implement those, too.

If there are other people working on this stuff it would be great to collaborate.

I don't know of anyone working on this. It sounds very difficult.

The book I mention details a series of execution models, where each successive model aims to validate the assertion in a more efficient manner than the last. This is achieved by performing static analysis of the assertion's expression to determine under what circumstances the assertion need be (re)checked. Briefly:

EM1: after all DML statements;
EM2: only after DML statements involving tables mentioned in the assertion expression;
EM3: only after DML statements involving the columns mentioned in the assertion expression;
EM4: only after DML statements involving the columns, plus if the statement has a “polarity” that may affect the assertion expression.

“Polarity" here means that one is able to (statically) determine if only INSERTS and not DELETES can affect an expression or vice-versa.

EMs 5 and 6 are further enhancements that make use of querying the “transition effect” data of what actually changed in a statement, to determine if the assertion expression need be validated. I’ve not done as much reading around this topic yet so am concentrating on EMs 1-4.

I agree it is a difficult problem but there are a fair number of published academic papers relating to this topic. The AM4DP book draws a lot of this research together and presents the executions models.

I may start writing up on a blog of where I get to, and then post further to this list, if there is interest.

I suspect that you would get a lot further with a PoC patch including
the needed documentation. Remember to include how this would work at
all the transaction isolation levels and combinations of same that we
support. Recall also to include the lock strength needed. Just about
anything can be done with a database-wide lock :)

Cheers,
David.
--
David Fetter <david@fetter.org> http://fetter.org/
Phone: +1 415 235 3778 AIM: dfetter666 Yahoo!: dfetter
Skype: davidfetter XMPP: david.fetter@gmail.com

Remember to vote!
Consider donating to Postgres: http://www.postgresql.org/about/donate

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#5Joe Wildish
joe-postgresql.com@elusive.cx
In reply to: David Fetter (#4)
Re: Implementing SQL ASSERTION

On 3 May 2015, at 02:42, David Fetter <david@fetter.org> wrote:

On Sat, May 02, 2015 at 10:42:24PM +0100, Joe Wildish wrote:

I may start writing up on a blog of where I get to, and then post further to this list, if there is interest.

I suspect that you would get a lot further with a PoC patch including
the needed documentation. Remember to include how this would work at
all the transaction isolation levels and combinations of same that we
support. Recall also to include the lock strength needed. Just about
anything can be done with a database-wide lock :)

Thanks David. I’m obviously new here so I not that familiar with how one starts contributing.

Once I get to a decent level with the EM4 PoC I will post the details to this list. The general idea is that upon assertion creation, the expression is analysed to determine when it needs to be validated — corresponding internal "after statement” triggers are then created. There will definitely need to be some serialisation take place on the basis of when an assertion has been validated, but I’ve not got that far yet. I’ll be sure to include the details when I post though.

Regards.
-Joe

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#6Peter Eisentraut
peter_e@gmx.net
In reply to: Joe Wildish (#1)
Re: Implementing SQL ASSERTION

On 4/30/15 6:36 PM, Joe Wildish wrote:

I’m wondering if there are other people out there working on implementing SQL ASSERTION functionality?

I was the last one, probably:
</messages/by-id/1384486216.5008.17.camel@vanquo.pezone.net&gt;.
I intend to pick up that work sometime, but feel free to review the
thread for a start. The main question was how to manage transaction
isolation.

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#7Joe Wildish
joe-postgresql.org@elusive.cx
In reply to: Peter Eisentraut (#6)
1 attachment(s)
Re: Implementing SQL ASSERTION

Hackers,

Attached is a WIP patch for SQL assertion. I am posting it for anyone who might be interested in seeing it, for comments/feedback, and to see if others are keen to collaborate on taking it further. It is not near production-ready (see thoughts on that below).

The patch builds on the work posted by Peter back in 2013. I've taken his code and updated it to conform to some general changes made to the codebase since then. The bulk of the new work I have done is around when an assertion needs to be checked. Essentially it is an implementation of the algorithm described by Ceri & Widom in "Deriving Production Rules for Constraint Maintenance” — http://infolab.stanford.edu/pub/papers/constraint-maintenance.ps

The general idea is to traverse the expression tree and derive the set of potentially invalidating operations. These operations are used to determine when the constraint trigger fires and causes a re-check. The detail is in the paper but some examples are:

* insertion into the subject of an exists cannot be invalidating;
* deletion from the subject of a not exists cannot be invalidating;
* update of columns in the target list of an exists cannot be invalidating;
* certain combinations of aggregates with comparison operations cannot be invalidating.

As an example of the last point, the expression "CHECK (10 > (SELECT COUNT(*) FROM t))" cannot be invalidated by a delete or an update but can be invalidated by an insert.

I have implemented most of the optimisations mentioned in the paper. There are one or two that I am unsure about, specifically how to deal with set-operations that are the subject of an exists. According to the paper, these are optimisable when they're the subject of an exists, but I think it is only applicable for union and not intersect or except, so I have skipped that particular optimisation for the time being.

The algorithm works under the assumption that when a recheck occurs the previous check result was true (the research report by Ceri & Widom does acknolwedge this assumption). However, unfortunately the SQL specification requires that both true and unknown be valid results for an assertion's check expression. This doesn't play too well with the algorithm so for the time being I have disallowed null. I think the solution here may be that when a null result for a check occurs, the assertion is changed to trigger on all operations against the involved tables; once it returns to true, the triggers can be returned to fire only on the derived invalidating operations. More thought required though. (note: having just written this paragraph, I've realised I can't right now think of a concrete example illustrating the point, so it may be that I'm wrong on this).

The paper does mention a set of optimisations that I have not yet attempted to implement. These are essentially the technique of evaluating the expression against the deltas of a change rather than the full tables. Clearly there is a large overlap with incremental maintainence of views and actually the two authors of the paper have a similiarly named paper called "Deriving Production Rules for Incremental View Maintanence". Although I have yet to finish reviewing all the literature on the subject, I suspect that realistically for this to make it into production, we'd need some implementation of these techniques to make the performance palatable.

Cheers,
-Joe

Attachments:

0001-SQL-assertion-WIP.patchapplication/octet-stream; name=0001-SQL-assertion-WIP.patch; x-unix-mode=0600Download
From 5e3f1190a9599bd10e40b2b310f1a597ef07c0e5 Mon Sep 17 00:00:00 2001
From: Joe Wildish <joe@elusive.cx>
Date: Sun, 14 Jan 2018 21:52:54 +0000
Subject: [PATCH] SQL assertion WIP

---
 doc/src/sgml/ddl.sgml                      |   31 +
 doc/src/sgml/ref/allfiles.sgml             |    3 +
 doc/src/sgml/ref/alter_assertion.sgml      |  139 ++++
 doc/src/sgml/ref/create_assertion.sgml     |  133 ++++
 doc/src/sgml/ref/drop_assertion.sgml       |  115 +++
 doc/src/sgml/reference.sgml                |    3 +
 src/backend/catalog/aclchk.c               |   31 +
 src/backend/catalog/information_schema.sql |   60 +-
 src/backend/catalog/namespace.c            |  121 +++
 src/backend/catalog/objectaddress.c        |   11 +
 src/backend/catalog/pg_constraint.c        |   20 +-
 src/backend/commands/alter.c               |    6 +
 src/backend/commands/constraint.c          | 1092 +++++++++++++++++++++++++++-
 src/backend/commands/event_trigger.c       |    2 +
 src/backend/nodes/copyfuncs.c              |   14 +
 src/backend/nodes/equalfuncs.c             |   12 +
 src/backend/parser/gram.y                  |   80 +-
 src/backend/parser/parse_agg.c             |    2 +
 src/backend/parser/parse_expr.c            |    2 +
 src/backend/parser/parse_func.c            |    4 +
 src/backend/tcop/utility.c                 |   20 +
 src/backend/utils/cache/lsyscache.c        |   19 +
 src/bin/psql/command.c                     |    3 +
 src/bin/psql/describe.c                    |   92 +++
 src/bin/psql/describe.h                    |    3 +
 src/bin/psql/help.c                        |    1 +
 src/bin/psql/tab-complete.c                |   28 +-
 src/include/catalog/namespace.h            |    1 +
 src/include/catalog/pg_constraint.h        |    3 +-
 src/include/catalog/pg_constraint_fn.h     |    2 +-
 src/include/catalog/pg_proc.h              |    4 +
 src/include/commands/constraint.h          |   10 +
 src/include/nodes/nodes.h                  |    1 +
 src/include/nodes/parsenodes.h             |   11 +
 src/include/parser/parse_node.h            |    1 +
 src/include/utils/acl.h                    |    2 +
 src/include/utils/lsyscache.h              |    1 +
 src/test/regress/expected/assertions.out   |  779 ++++++++++++++++++++
 src/test/regress/parallel_schedule         |    2 +-
 src/test/regress/serial_schedule           |    1 +
 src/test/regress/sql/assertions.sql        |  775 ++++++++++++++++++++
 41 files changed, 3571 insertions(+), 69 deletions(-)
 create mode 100644 doc/src/sgml/ref/alter_assertion.sgml
 create mode 100644 doc/src/sgml/ref/create_assertion.sgml
 create mode 100644 doc/src/sgml/ref/drop_assertion.sgml
 create mode 100644 src/include/commands/constraint.h
 create mode 100644 src/test/regress/expected/assertions.out
 create mode 100644 src/test/regress/sql/assertions.sql

diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index b1167a40e6..d8c0263a6a 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -917,6 +917,37 @@ CREATE TABLE circles (
     of the type specified in the constraint declaration.
    </para>
   </sect2>
+
+  <sect2 id="ddl-constraints-assertions">
+   <title>Assertions</title>
+   
+   <indexterm zone="ddl-constraints-assertions">
+    <primary>assertion</primary>
+   </indexterm>
+
+   <para>
+    An assertion is a constraint that is not part of a table
+    definition.  An assertion can define constraints that evaluate the
+    data across multiple rows of a table beyond what unique and
+    exclusion constraints can do, and assertions can look at the data
+    in multiple tables.
+   </para>
+
+   <para>
+    An assertion is a separate schema object and is created with the
+    command <command>CREATE ASSERTION</command>.  The constraint
+    expression is written as a <literal>CHECK</literal> clause like in
+    check constraints.  For instance, to ensure that there is always
+    at least one entry in the product table:
+<programlisting>
+CREATE ASSERTION products_not_empty CHECK ((SELECT count(*) FROM products) &gt; 0);
+</programlisting>
+    Assertions will often involve aggregate functions computed over
+    entire tables.  Note, however, that this kind of assertion can be
+   quite inefficient and should only be used on tables that are small
+    and change rarely.
+   </para>
+  </sect2>
  </sect1>
 
  <sect1 id="ddl-system-columns">
diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index 22e6893211..57badaf157 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -7,6 +7,7 @@ Complete list of usable sgml source files in this directory.
 <!-- SQL commands -->
 <!ENTITY abort              SYSTEM "abort.sgml">
 <!ENTITY alterAggregate     SYSTEM "alter_aggregate.sgml">
+<!ENTITY alterAssertion     SYSTEM "alter_assertion.sgml">
 <!ENTITY alterCollation     SYSTEM "alter_collation.sgml">
 <!ENTITY alterConversion    SYSTEM "alter_conversion.sgml">
 <!ENTITY alterDatabase      SYSTEM "alter_database.sgml">
@@ -60,6 +61,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY copyTable          SYSTEM "copy.sgml">
 <!ENTITY createAccessMethod SYSTEM "create_access_method.sgml">
 <!ENTITY createAggregate    SYSTEM "create_aggregate.sgml">
+<!ENTITY createAssertion    SYSTEM "create_assertion.sgml">
 <!ENTITY createCast         SYSTEM "create_cast.sgml">
 <!ENTITY createCollation    SYSTEM "create_collation.sgml">
 <!ENTITY createConversion   SYSTEM "create_conversion.sgml">
@@ -107,6 +109,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY do                 SYSTEM "do.sgml">
 <!ENTITY dropAccessMethod   SYSTEM "drop_access_method.sgml">
 <!ENTITY dropAggregate      SYSTEM "drop_aggregate.sgml">
+<!ENTITY dropAssertion      SYSTEM "drop_assertion.sgml">
 <!ENTITY dropCast           SYSTEM "drop_cast.sgml">
 <!ENTITY dropCollation      SYSTEM "drop_collation.sgml">
 <!ENTITY dropConversion     SYSTEM "drop_conversion.sgml">
diff --git a/doc/src/sgml/ref/alter_assertion.sgml b/doc/src/sgml/ref/alter_assertion.sgml
new file mode 100644
index 0000000000..b06417f740
--- /dev/null
+++ b/doc/src/sgml/ref/alter_assertion.sgml
@@ -0,0 +1,139 @@
+<!--
+doc/src/sgml/ref/alter_assertion.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-alterassertion">
+
+ <indexterm zone="sql-alterassertion">
+  <primary>ALTER ASSERTION</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>ALTER ASSERTION</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>ALTER ASSERTION</refname>
+  <refpurpose>change the definition of an assertion</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+ALTER ASSERTION <replaceable>name</replaceable> RENAME TO <replaceable>new_name</replaceable>
+ALTER ASSERTION <replaceable>name</replaceable> OWNER TO <replaceable>new_owner</replaceable>
+ALTER ASSERTION <replaceable>name</replaceable> SET SCHEMA <replaceable>new_schema</replaceable>
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>ALTER ASSERTION</command> changes the definition of an
+   assertion.
+  </para>
+
+  <para>
+   You must own the assertion to use <command>ALTER ASSERTION</command>.  To
+   change the schema of an assertion, you must also have
+   <literal>CREATE</literal> privilege on the new schema.  To alter
+   the owner, you must also be a direct or indirect member of the new
+   owning role, and that role must have <literal>CREATE</literal>
+   privilege on the assertion's schema.  (These restrictions enforce
+   that altering the owner doesn't do anything you couldn't do by
+   dropping and recreating the assertion.  However, a superuser can
+   alter ownership of any assertion 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 existing assertion.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_name</replaceable></term>
+    <listitem>
+     <para>
+      The new name of the assertion.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_owner</replaceable></term>
+    <listitem>
+     <para>
+      The new owner of the assertion.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_schema</replaceable></term>
+    <listitem>
+     <para>
+      The new schema for the assertion.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To rename the assertuib <literal>check_size</literal>
+   to <literal>check_count</literal>:
+<programlisting>
+ALTER ASSERTION check_size RENAME TO check_count;
+</programlisting>
+  </para>
+
+  <para>
+   To change the owner of the assertion <literal>check_size</literal>
+   to <literal>joe</literal>:
+<programlisting>
+ALTER ASSERTION check_size OWNER TO joe;
+</programlisting>
+  </para>
+
+  <para>
+   To move the assertion <literal>check_size</literal> into
+   schema <literal>myschema</literal>:
+<programlisting>
+ALTER ASSERTION check_size SET SCHEMA myschema;
+</programlisting>
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   There is no <command>ALTER ASSERTION</command> statement in the SQL
+   standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-createassertion"/></member>
+   <member><xref linkend="sql-dropassertion"/></member>
+  </simplelist>
+ </refsect1>
+</refentry>
diff --git a/doc/src/sgml/ref/create_assertion.sgml b/doc/src/sgml/ref/create_assertion.sgml
new file mode 100644
index 0000000000..b36d1b4ecb
--- /dev/null
+++ b/doc/src/sgml/ref/create_assertion.sgml
@@ -0,0 +1,133 @@
+<!--
+doc/src/sgml/ref/create_assertion.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-createassertion">
+ <indexterm zone="sql-createassertion">
+  <primary>CREATE ASSERTION</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>CREATE ASSERTION</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>CREATE ASSERTION</refname>
+  <refpurpose>define a new assertion</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+CREATE ASSERTION <replaceable class="parameter">name</replaceable> CHECK ( <replaceable class="parameter">name</replaceable> ) [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>CREATE ASSERTION</command> creates an assertion.  An
+   assertion is a check constraint that is independent of a table row
+   and a table.  It can therefore be used to enforce more complex
+   constraints across multiple table rows and across multiple tables.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><replaceable class="parameter">name</replaceable></term>
+    <listitem>
+     <para>
+      The name (optionally schema-qualified) of the assertion to
+      create.  Assertions use the same namespace as constraints on
+      tables.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>CHECK ( <replaceable class="parameter">expression</replaceable> )</literal></term>
+    <listitem>
+     <para>
+      The <literal>CHECK</literal> clause specifies an expression producing a
+      Boolean result which the database must satisfy at all times for a
+      data change operation to succeed.  Expressions evaluating
+      to TRUE or UNKNOWN succeed.  Should the result of a data change
+      operation produce a FALSE result an error exception is
+      raised and the change is not made.
+     </para>
+
+     <para>
+      The check expression typically involves subselects in order to
+      read data from tables.  See the examples below.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>DEFERRABLE</literal></term>
+    <term><literal>NOT DEFERRABLE</literal></term>
+    <term><literal>INITIALLY IMMEDIATE</literal></term>
+    <term><literal>INITIALLY DEFERRED</literal></term>
+    <listitem>
+     <para>
+      These clauses control the deferrability of the constraint.  See
+      <xref linkend="sql-createtable"/> for an explanation.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Notes</title>
+
+  <para>
+   More specialized constraint forms such as table check constraints,
+   foreign-key constraints, or exclusion constraints should be used
+   instead when applicable, because they will be more efficient.
+  </para>
+
+  <para>
+   Assertion checks are not specially optimized.  For example,
+   checking the row count of a large table in an assertion will be
+   just as slow as implementing the check manually.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   Check that the table <literal>table1</literal> has at most 30 rows:
+<programlisting>
+CREATE ASSERTION table1_max30 CHECK ((SELECT count(*) FROM table1) &lt;= 30);
+</programlisting>
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   <command>CREATE ASSERTION</command> conforms to the SQL standard.
+   The PostgreSQL implementation has certain restrictions on what
+   check expressions are allowed in assertions.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-alterassertion"/></member>
+   <member><xref linkend="sql-dropassertion"/></member>
+  </simplelist>
+ </refsect1>
+</refentry>
diff --git a/doc/src/sgml/ref/drop_assertion.sgml b/doc/src/sgml/ref/drop_assertion.sgml
new file mode 100644
index 0000000000..39a60a0d29
--- /dev/null
+++ b/doc/src/sgml/ref/drop_assertion.sgml
@@ -0,0 +1,115 @@
+<!--
+doc/src/sgml/ref/drop_assertion.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-dropassertion">
+
+ <indexterm zone="sql-dropassertion">
+  <primary>DROP ASSERTION</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>DROP ASSERTION</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>DROP ASSERTION</refname>
+  <refpurpose>remove an assertion</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+DROP ASSERTION [ IF EXISTS ] <replaceable class="parameter">name</replaceable> [, ...] [ CASCADE | RESTRICT ]
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>DROP ASSERTION</command> removes an existing assertion. To
+   execute this command the current user must be the owner of the
+   assertion.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+
+   <varlistentry>
+    <term><literal>IF EXISTS</literal></term>
+    <listitem>
+     <para>
+      Do not throw an error if the assertion 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 existing assertion.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>CASCADE</literal></term>
+    <listitem>
+     <para>
+      Automatically drop objects that depend on the assertion.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>RESTRICT</literal></term>
+    <listitem>
+     <para>
+      Refuse to drop the assertion if any objects depend on it.  This
+      is the default.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To remove the assertion <literal>check_size</literal>:
+<programlisting>
+DROP ASSERTION check_size;
+</programlisting>
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   This command conforms to the SQL standard, except that the standard
+   only allows one assertion to be dropped per command, and apart from
+   the <literal>IF EXISTS</literal> option, which is
+   a <productname>PostgreSQL</productname> extension.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-alterassertion"/></member>
+   <member><xref linkend="sql-createassertion"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index d27fb414f7..9300f2d9fc 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -35,6 +35,7 @@
 
    &abort;
    &alterAggregate;
+   &alterAssertion;
    &alterCollation;
    &alterConversion;
    &alterDatabase;
@@ -88,6 +89,7 @@
    &copyTable;
    &createAccessMethod;
    &createAggregate;
+   &createAssertion;
    &createCast;
    &createCollation;
    &createConversion;
@@ -135,6 +137,7 @@
    &do;
    &dropAccessMethod;
    &dropAggregate;
+   &dropAssertion;
    &dropCast;
    &dropCollation;
    &dropConversion;
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index fac80612b8..e32a0ad347 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -3376,6 +3376,8 @@ static const char *const no_priv_msg[MAX_ACL_KIND] =
 	gettext_noop("permission denied for operator family %s"),
 	/* ACL_KIND_COLLATION */
 	gettext_noop("permission denied for collation %s"),
+	/* ACL_KIND_CONSTRAINT */
+    gettext_noop("permission denied for constraint %s"),
 	/* ACL_KIND_CONVERSION */
 	gettext_noop("permission denied for conversion %s"),
 	/* ACL_KIND_STATISTICS */
@@ -3428,6 +3430,8 @@ static const char *const not_owner_msg[MAX_ACL_KIND] =
 	gettext_noop("must be owner of operator family %s"),
 	/* ACL_KIND_COLLATION */
 	gettext_noop("must be owner of collation %s"),
+	/* ACK_KIND_CONSTRAINT */
+	gettext_noop("must be owner of constraint %s"),
 	/* ACL_KIND_CONVERSION */
 	gettext_noop("must be owner of conversion %s"),
 	/* ACL_KIND_STATISTICS */
@@ -5067,6 +5071,33 @@ pg_collation_ownercheck(Oid coll_oid, Oid roleid)
 	return has_privs_of_role(roleid, ownerId);
 }
 
+/*
+ * Ownership check for a constraint (specified by OID).
+ */
+bool
+pg_constraint_ownercheck(Oid constr_oid, Oid roleid)
+{
+	HeapTuple   tuple;
+	//Oid           ownerId;
+
+	/* Superusers bypass all permission checking. */
+	if (superuser_arg(roleid))
+		return true;
+
+	tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(constr_oid));
+	if (!HeapTupleIsValid(tuple))
+		ereport(ERROR,
+			    (errcode(ERRCODE_UNDEFINED_OBJECT),
+			     errmsg("constraint with OID %u does not exist", constr_oid)));
+
+	//FIXME: ownerId = ((Form_pg_constraint) GETSTRUCT(tuple))->conowner;
+
+	ReleaseSysCache(tuple);
+
+	//return has_privs_of_role(roleid, ownerId);
+	return true; // for now
+}
+
 /*
  * Ownership check for a conversion (specified by OID).
  */
diff --git a/src/backend/catalog/information_schema.sql b/src/backend/catalog/information_schema.sql
index 6fb1a1bc1c..9ef7cccdbb 100644
--- a/src/backend/catalog/information_schema.sql
+++ b/src/backend/catalog/information_schema.sql
@@ -285,7 +285,20 @@ GRANT SELECT ON administrable_role_authorizations TO PUBLIC;
  * ASSERTIONS view
  */
 
--- feature not supported
+CREATE VIEW assertions AS
+    SELECT CAST(current_database() AS sql_identifier) AS constraint_catalog,
+           CAST(n.nspname AS sql_identifier) AS constraint_schema,
+           CAST(con.conname AS sql_identifier) AS constraint_name,
+           CAST(CASE WHEN condeferrable THEN 'YES' ELSE 'NO' END
+             AS yes_or_no) AS is_deferrable,
+           CAST(CASE WHEN condeferred THEN 'YES' ELSE 'NO' END
+             AS yes_or_no) AS initially_deferred
+    FROM pg_namespace n, pg_constraint con
+    WHERE n.oid = con.connamespace
+          AND con.conrelid = 0 AND con.contypid = 0;
+          -- TODO: AND pg_has_role(con.conowner, 'USAGE');
+
+GRANT SELECT ON assertions TO PUBLIC;
 
 
 /*
@@ -790,7 +803,7 @@ CREATE VIEW constraint_column_usage AS
            CAST(cstrname AS sql_identifier) AS constraint_name
 
     FROM (
-        /* check constraints */
+        /* assertions and check constraints */
         SELECT DISTINCT nr.nspname, r.relname, r.relowner, a.attname, nc.nspname, c.conname
           FROM pg_namespace nr, pg_class r, pg_attribute a, pg_depend d, pg_namespace nc, pg_constraint c
           WHERE nr.oid = r.relnamespace
@@ -802,7 +815,7 @@ CREATE VIEW constraint_column_usage AS
             AND d.objid = c.oid
             AND c.connamespace = nc.oid
             AND c.contype = 'c'
-            AND r.relkind IN ('r', 'p')
+            AND r.relkind IN ('r', 'p', 'v')
             AND NOT a.attisdropped
 
         UNION ALL
@@ -842,20 +855,41 @@ GRANT SELECT ON constraint_column_usage TO PUBLIC;
 
 CREATE VIEW constraint_table_usage AS
     SELECT CAST(current_database() AS sql_identifier) AS table_catalog,
-           CAST(nr.nspname AS sql_identifier) AS table_schema,
-           CAST(r.relname AS sql_identifier) AS table_name,
+           CAST(tblschema AS sql_identifier) AS table_schema,
+           CAST(tblname AS sql_identifier) AS table_name,
            CAST(current_database() AS sql_identifier) AS constraint_catalog,
-           CAST(nc.nspname AS sql_identifier) AS constraint_schema,
-           CAST(c.conname AS sql_identifier) AS constraint_name
+           CAST(cstrschema AS sql_identifier) AS constraint_schema,
+           CAST(cstrname AS sql_identifier) AS constraint_name
+
+    FROM (
+        /* assertions and check constraints */
+        SELECT DISTINCT nr.nspname, r.relname, r.relowner, nc.nspname, c.conname
+          FROM pg_namespace nr, pg_class r,
+               pg_depend d, pg_namespace nc, pg_constraint c
+         WHERE nr.oid = r.relnamespace
+           AND d.refobjid = r.oid
+           AND c.connamespace = nc.oid
+           AND d.objid = c.oid
+           AND d.refclassid = 'pg_catalog.pg_class'::regclass
+           AND d.classid = 'pg_catalog.pg_constraint'::regclass
+           AND c.contype = 'c'
+           AND r.relkind IN ('r', 'p', 'v')
 
-    FROM pg_constraint c, pg_namespace nc,
-         pg_class r, pg_namespace nr
+        UNION ALL
 
-    WHERE c.connamespace = nc.oid AND r.relnamespace = nr.oid
-          AND ( (c.contype = 'f' AND c.confrelid = r.oid)
+        /* unique/primary key constraints */
+        SELECT nr.nspname, r.relname, r.relowner, nc.nspname, c.conname
+          FROM pg_constraint c, pg_namespace nc,
+               pg_class r, pg_namespace nr
+         WHERE c.connamespace = nc.oid
+           AND r.relnamespace = nr.oid
+           AND ( (c.contype = 'f' AND c.confrelid = r.oid)
              OR (c.contype IN ('p', 'u') AND c.conrelid = r.oid) )
-          AND r.relkind IN ('r', 'p')
-          AND pg_has_role(r.relowner, 'USAGE');
+           AND r.relkind IN ('r', 'p')
+
+    ) AS x (tblschema, tblname, tblowner, cstrschema, cstrname)
+
+    WHERE pg_has_role(x.tblowner, 'USAGE');
 
 GRANT SELECT ON constraint_table_usage TO PUBLIC;
 
diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c
index 93c4bbfcb0..17fe756979 100644
--- a/src/backend/catalog/namespace.c
+++ b/src/backend/catalog/namespace.c
@@ -27,6 +27,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_authid.h"
 #include "catalog/pg_collation.h"
+#include "catalog/pg_constraint.h"
 #include "catalog/pg_conversion.h"
 #include "catalog/pg_conversion_fn.h"
 #include "catalog/pg_namespace.h"
@@ -57,9 +58,11 @@
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/syscache.h"
+#include "utils/fmgroids.h"
 #include "utils/varlena.h"
 
 
+
 /*
  * The namespace search path is a possibly-empty list of namespace OIDs.
  * In addition to the explicit list, implicitly-searched namespaces
@@ -3483,6 +3486,124 @@ PopOverrideSearchPath(void)
 }
 
 
+static Oid
+get_assertion_oid_internal(Relation pg_constraint, char *assertion_name, Oid namespaceId, List *name)
+{
+	SysScanDesc scan;
+	ScanKeyData skey[4];
+	HeapTuple	tuple;
+	Oid			conOid = InvalidOid;
+
+	ScanKeyInit(&skey[0],
+				Anum_pg_constraint_conname,
+				BTEqualStrategyNumber, F_NAMEEQ,
+				PointerGetDatum(assertion_name));
+
+	ScanKeyInit(&skey[1],
+				Anum_pg_constraint_connamespace,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(namespaceId));
+
+	ScanKeyInit(&skey[2],
+				Anum_pg_constraint_conrelid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				InvalidOid);
+
+	ScanKeyInit(&skey[3],
+				Anum_pg_constraint_contypid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				InvalidOid);
+
+	scan = systable_beginscan(pg_constraint, InvalidOid, false,
+							  NULL, 4, skey);
+
+	/*
+	 * Fetch the constraint tuple from pg_constraint.  There may be
+	 * more than one match, because constraints are not required to
+	 * have unique names; if so, error out.
+	 */
+	while (HeapTupleIsValid(tuple = systable_getnext(scan)))
+	{
+		Form_pg_constraint con = (Form_pg_constraint) GETSTRUCT(tuple);
+
+		if (OidIsValid(con->conrelid) || OidIsValid(con->contypid))
+			ereport(ERROR,
+					(errmsg("constraint \"%s\" is not an assertion",
+							NameListToString(name))));
+
+		if (strcmp(NameStr(con->conname), assertion_name) == 0)
+		{
+			if (OidIsValid(conOid))
+				ereport(ERROR,
+						(errcode(ERRCODE_DUPLICATE_OBJECT),
+								errmsg("there are multiple assertions named \"%s\"",
+									   NameListToString(name))));
+			conOid = HeapTupleGetOid(tuple);
+		}
+	}
+
+	systable_endscan(scan);
+
+	return conOid;
+}
+
+/*
+ * get_assertion_oid
+ *		Find an assertion with the specified name.
+ *		Returns constraint's OID.
+ */
+Oid
+get_assertion_oid(List *name, bool missing_ok)
+{
+	char	   *schemaname;
+	char	   *assertion_name;
+	Oid			namespaceId;
+	Relation	pg_constraint;
+	Oid			conOid = InvalidOid;
+
+	DeconstructQualifiedName(name, &schemaname, &assertion_name);
+
+	pg_constraint = heap_open(ConstraintRelationId, AccessShareLock);
+
+	if (schemaname)
+	{
+		namespaceId = LookupExplicitNamespace(schemaname, missing_ok);
+		conOid = get_assertion_oid_internal(pg_constraint, assertion_name, namespaceId, name);
+	}
+	else
+	{
+		ListCell   *l;
+
+		recomputeNamespacePath();
+
+		foreach(l, activeSearchPath)
+		{
+			namespaceId = lfirst_oid(l);
+
+			if (namespaceId == myTempNamespace)
+				continue;		/* do not look in temp namespace */
+
+			conOid = get_assertion_oid_internal(pg_constraint, assertion_name, namespaceId, name);
+
+			if (OidIsValid(conOid))
+				break;
+		}
+	}
+
+	heap_close(pg_constraint, AccessShareLock);
+
+	/* If no such constraint exists, complain */
+	if (!OidIsValid(conOid) && !missing_ok)
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+						errmsg("assertion \"%s\" does not exist",
+							   NameListToString(name))));
+
+	return conOid;
+}
+
+
+
 /*
  * get_collation_oid - find a collation by possibly qualified name
  *
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index bc999ca3c4..daa9acf4e4 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -899,6 +899,11 @@ get_object_address(ObjectType objtype, Node *object,
 				address.objectId = LookupOperWithArgs(castNode(ObjectWithArgs, object), missing_ok);
 				address.objectSubId = 0;
 				break;
+			case OBJECT_ASSERTION:
+				address.classId = ConstraintRelationId;
+				address.objectId = get_assertion_oid(castNode(List, object), missing_ok);
+				address.objectSubId = 0;
+				break;
 			case OBJECT_COLLATION:
 				address.classId = CollationRelationId;
 				address.objectId = get_collation_oid(castNode(List, object), missing_ok);
@@ -2114,6 +2119,7 @@ pg_get_object_address(PG_FUNCTION_ARGS)
 		case OBJECT_FOREIGN_TABLE:
 		case OBJECT_COLUMN:
 		case OBJECT_ATTRIBUTE:
+		case OBJECT_ASSERTION:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_STATISTIC_EXT:
@@ -2274,6 +2280,11 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
 				aclcheck_error(ACLCHECK_NOT_OWNER, ACL_KIND_NAMESPACE,
 							   strVal((Value *) object));
 			break;
+		case OBJECT_ASSERTION:
+			if (!pg_constraint_ownercheck(address.objectId, roleid))
+				aclcheck_error(ACLCHECK_NOT_OWNER, ACL_KIND_CONSTRAINT,
+							   NameListToString(castNode(List, object)));
+			break;
 		case OBJECT_COLLATION:
 			if (!pg_collation_ownercheck(address.objectId, roleid))
 				aclcheck_error(ACLCHECK_NOT_OWNER, ACL_KIND_COLLATION,
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index 442ae7e23d..52a5a7efec 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -429,6 +429,13 @@ ConstraintNameIsUsed(ConstraintCategory conCat, Oid objId,
 			found = true;
 			break;
 		}
+		else if (conCat == CONSTRAINT_ASSERTION
+				 && con->conrelid == InvalidOid
+				 && con->contypid == InvalidOid)
+		{
+			found = true;
+			break;
+		}
 	}
 
 	systable_endscan(conscan);
@@ -600,8 +607,7 @@ RemoveConstraintById(Oid conId)
 		 * but we have no such concept at the moment.
 		 */
 	}
-	else
-		elog(ERROR, "constraint %u is not of a known type", conId);
+	/* Else it's an assertion; nothing special for that. */
 
 	/* Fry the constraint itself */
 	CatalogTupleDelete(conDesc, &tup->t_self);
@@ -657,6 +663,16 @@ RenameConstraintById(Oid conId, const char *newname)
 				(errcode(ERRCODE_DUPLICATE_OBJECT),
 				 errmsg("constraint \"%s\" for domain %s already exists",
 						newname, format_type_be(con->contypid))));
+	if (!OidIsValid(con->conrelid) &&
+		!OidIsValid(con->contypid) &&
+		ConstraintNameIsUsed(CONSTRAINT_ASSERTION,
+							 InvalidOid,
+							 con->connamespace,
+							 newname))
+		ereport(ERROR,
+				(errcode(ERRCODE_DUPLICATE_OBJECT),
+						errmsg("assertion \"%s\" already exists",
+							   newname)));
 
 	/* OK, do the rename --- tuple is a copy, so OK to scribble on it */
 	namestrcpy(&(con->conname), newname);
diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c
index 3995c5ef3d..66ba6cbd30 100644
--- a/src/backend/commands/alter.c
+++ b/src/backend/commands/alter.c
@@ -41,6 +41,7 @@
 #include "commands/alter.h"
 #include "commands/collationcmds.h"
 #include "commands/conversioncmds.h"
+#include "commands/constraint.h"
 #include "commands/dbcommands.h"
 #include "commands/defrem.h"
 #include "commands/event_trigger.h"
@@ -326,6 +327,9 @@ ExecRenameStmt(RenameStmt *stmt)
 {
 	switch (stmt->renameType)
 	{
+		case OBJECT_ASSERTION:
+			return RenameAssertion(stmt);
+
 		case OBJECT_TABCONSTRAINT:
 		case OBJECT_DOMCONSTRAINT:
 			return RenameConstraint(stmt);
@@ -491,6 +495,7 @@ ExecAlterObjectSchemaStmt(AlterObjectSchemaStmt *stmt,
 
 			/* generic code path */
 		case OBJECT_AGGREGATE:
+		case OBJECT_ASSERTION:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_FUNCTION:
@@ -838,6 +843,7 @@ ExecAlterOwnerStmt(AlterOwnerStmt *stmt)
 
 			/* Generic cases */
 		case OBJECT_AGGREGATE:
+		case OBJECT_ASSERTION:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_FUNCTION:
diff --git a/src/backend/commands/constraint.c b/src/backend/commands/constraint.c
index 90f19ad3dd..969ce6deff 100644
--- a/src/backend/commands/constraint.c
+++ b/src/backend/commands/constraint.c
@@ -11,6 +11,8 @@
  *
  *-------------------------------------------------------------------------
  */
+
+
 #include "postgres.h"
 
 #include "catalog/index.h"
@@ -20,6 +22,40 @@
 #include "utils/rel.h"
 #include "utils/tqual.h"
 
+#include "access/htup_details.h"
+#include "catalog/dependency.h"
+#include "catalog/namespace.h"
+#include "catalog/pg_aggregate.h"
+#include "catalog/pg_constraint.h"
+#include "catalog/pg_constraint_fn.h"
+#include "catalog/pg_operator.h"
+#include "catalog/pg_proc.h"
+#include "catalog/pg_language.h"
+#include "catalog/pg_trigger.h"
+#include "catalog/pg_type.h"
+#include "commands/constraint.h"
+#include "executor/functions.h"
+#include "executor/spi.h"
+#include "miscadmin.h"
+#include "nodes/makefuncs.h"
+#include "nodes/nodeFuncs.h"
+#include "optimizer/planner.h"
+#include "parser/analyze.h"
+#include "parser/parse_coerce.h"
+#include "parser/parse_expr.h"
+#include "parser/parse_node.h"
+#include "parser/parse_relation.h"
+#include "parser/parser.h"
+#include "parser/parsetree.h"
+#include "rewrite/rewriteHandler.h"
+#include "tcop/tcopprot.h"
+#include "utils/acl.h"
+#include "utils/lsyscache.h"
+#include "utils/snapmgr.h"
+#include "utils/syscache.h"
+#include "utils/ruleutils.h"
+#include "utils/fmgroids.h"
+
 
 /*
  * unique_key_recheck - trigger function to do a deferred uniqueness check.
@@ -39,15 +75,15 @@ unique_key_recheck(PG_FUNCTION_ARGS)
 {
 	TriggerData *trigdata = castNode(TriggerData, fcinfo->context);
 	const char *funcname = "unique_key_recheck";
-	HeapTuple	new_row;
+	HeapTuple new_row;
 	ItemPointerData tmptid;
-	Relation	indexRel;
-	IndexInfo  *indexInfo;
-	EState	   *estate;
+	Relation indexRel;
+	IndexInfo *indexInfo;
+	EState *estate;
 	ExprContext *econtext;
 	TupleTableSlot *slot;
-	Datum		values[INDEX_MAX_KEYS];
-	bool		isnull[INDEX_MAX_KEYS];
+	Datum values[INDEX_MAX_KEYS];
+	bool isnull[INDEX_MAX_KEYS];
 
 	/*
 	 * Make sure this is being called as an AFTER ROW trigger.  Note:
@@ -57,15 +93,15 @@ unique_key_recheck(PG_FUNCTION_ARGS)
 	if (!CALLED_AS_TRIGGER(fcinfo))
 		ereport(ERROR,
 				(errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED),
-				 errmsg("function \"%s\" was not called by trigger manager",
-						funcname)));
+					errmsg("function \"%s\" was not called by trigger manager",
+						   funcname)));
 
 	if (!TRIGGER_FIRED_AFTER(trigdata->tg_event) ||
 		!TRIGGER_FIRED_FOR_ROW(trigdata->tg_event))
 		ereport(ERROR,
 				(errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED),
-				 errmsg("function \"%s\" must be fired AFTER ROW",
-						funcname)));
+					errmsg("function \"%s\" must be fired AFTER ROW",
+						   funcname)));
 
 	/*
 	 * Get the new data that was inserted/updated.
@@ -78,9 +114,9 @@ unique_key_recheck(PG_FUNCTION_ARGS)
 	{
 		ereport(ERROR,
 				(errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED),
-				 errmsg("function \"%s\" must be fired for INSERT or UPDATE",
-						funcname)));
-		new_row = NULL;			/* keep compiler quiet */
+					errmsg("function \"%s\" must be fired for INSERT or UPDATE",
+						   funcname)));
+		new_row = NULL;            /* keep compiler quiet */
 	}
 
 	/*
@@ -194,3 +230,1033 @@ unique_key_recheck(PG_FUNCTION_ARGS)
 
 	return PointerGetDatum(NULL);
 }
+
+
+static void
+test_assertion_expr(char *name, char *expression)
+{
+	char *sql;
+	int returnCode;
+	bool isNull;
+	Datum value;
+
+	if (SPI_connect() != SPI_OK_CONNECT)
+		elog(ERROR, "SPI connect failed when executing ASSERTION statement");
+
+	sql = psprintf("SELECT %s", expression);
+	returnCode = SPI_exec(sql, 1);
+
+	if (returnCode > 0 && SPI_tuptable != NULL)
+	{
+		value = SPI_getbinval(SPI_tuptable->vals[0], SPI_tuptable->tupdesc, 1, &isNull);
+		if (isNull)
+			ereport(ERROR,
+					(errcode(ERRCODE_CHECK_VIOLATION),
+							errmsg("assertion \"%s\" truth is unknown", name)));
+		else if (!isNull && !DatumGetBool(value))
+			ereport(ERROR,
+					(errcode(ERRCODE_CHECK_VIOLATION),
+							errmsg("assertion \"%s\" violated", name)));
+	}
+	else
+		elog(ERROR, "unexpected SPI result when executing ASSERTION statement");
+
+	SPI_finish();
+}
+
+
+Datum
+assertion_check(PG_FUNCTION_ARGS)
+{
+	TriggerData *trigdata = (TriggerData *) fcinfo->context;
+	const char *funcname = "assertion_check";
+	Oid constraintOid;
+	HeapTuple tup;
+	Datum adatum;
+	bool isNull;
+
+	/*
+	 * Make sure this is being called as an AFTER STATEMENT trigger.	Note:
+	 * translatable error strings are shared with ri_triggers.c, so resist the
+	 * temptation to fold the function name into them.
+	 */
+	if (!CALLED_AS_TRIGGER(fcinfo))
+		ereport(ERROR,
+				(errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED),
+					errmsg("function \"%s\" was not called by trigger manager",
+						   funcname)));
+
+	if (!TRIGGER_FIRED_AFTER(trigdata->tg_event) ||
+		!TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event))
+		ereport(ERROR,
+				(errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED),
+					errmsg("function \"%s\" must be fired AFTER STATEMENT",
+						   funcname)));
+
+	constraintOid = trigdata->tg_trigger->tgconstraint;
+	tup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(constraintOid));
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for constraint %u", constraintOid);
+
+	// XXX bogus
+	adatum = SysCacheGetAttr(CONSTROID, tup,
+							 Anum_pg_constraint_consrc, &isNull);
+	if (isNull)
+		elog(ERROR, "constraint %u has null consrc", constraintOid);
+
+	test_assertion_expr(get_constraint_name(constraintOid), TextDatumGetCString(adatum));
+
+	ReleaseSysCache(tup);
+	return PointerGetDatum(NULL);
+}
+
+#define NO_FUNC				(0)
+#define OTHER_FUNC			(1 << 0)
+#define MIN_AGG_FUNC		(1 << 1)
+#define MAX_AGG_FUNC		(1 << 2)
+#define COUNT_AGG_FUNC		(1 << 3)
+#define EVERY_AGG_FUNC		(1 << 4)
+#define BOOL_AND_AGG_FUNC	(1 << 5)
+#define BOOL_OR_AGG_FUNC	(1 << 6)
+
+#define EQ_COMPARISONS 		(bms_make_singleton(ROWCOMPARE_EQ))
+#define NE_COMPARISONS 		(bms_make_singleton(ROWCOMPARE_NE))
+#define LTE_COMPARISONS 	(bms_add_member(bms_make_singleton(ROWCOMPARE_LE), ROWCOMPARE_LT))
+#define GTE_COMPARISONS 	(bms_add_member(bms_make_singleton(ROWCOMPARE_GE), ROWCOMPARE_GT))
+
+#define MatchesOnly(a,b)	(((a) & (b)) && !((a) & (~(b))))
+#define CanOptimise(s) 		(!bms_is_empty(s) && \
+							(bms_is_subset(s, EQ_COMPARISONS)  || \
+							 bms_is_subset(s, NE_COMPARISONS)  || \
+							 bms_is_subset(s, LTE_COMPARISONS) || \
+							 bms_is_subset(s, GTE_COMPARISONS) ))
+
+
+// TODO review TODOs in the tests
+// TODO assertions should allow unknown according to the SQL specification
+// TODO traversal into views needs rethinking - dependencies?
+// TODO pg_dump support
+
+/*
+ * DML operations that could affect the truth of an assertion. Only
+ * INSERT and DELETE are considered as part of the labeling algorithm.
+ * UPDATEs are inferred using different logic.
+ */
+typedef enum DmlOp
+{
+	INSERT,
+	DELETE,
+	INSERT_DELETE
+} DmlOp;
+
+
+/*
+ * A context used to capture operations that may invalidate an assertion.
+ */
+typedef struct AssertionInfo
+{
+	DmlOp	label;         /* current invaliding operation */
+	Expr   *expr;          /* current Expression node */
+	List   *rtable;        /* current range tables */
+	List   *operators;     /* current operators */
+	SetOperation setOp;         /* current set-operation */
+	bool	invert;		   /* ... */
+	bool	inView;        /* if the walker has entered a view */
+	bool	inTargetEntry; /* if the walker has entered a TargetEntry node */
+	bool	inComparison;  /* if the walker has entered a comparison operation */
+	bool	inExists;      /* if the walker has entered an EXISTS node */
+
+	List   *dependencies; /* ... */
+	List   *relations;    /* relation Oids for all tables involved in the assertion check */
+	List   *inserts;      /* relation Oids for which INSERT could invalidate the assertion */
+	List   *deletes;      /* relation Oids for which DELETE could invalidate the assertion */
+	List   *updates;      /* relation Oids for which UPDATE could invalidate the assertion */
+	List   *columns;      /* list of ObjectAddresses referencing involved columns */
+} AssertionInfo;
+
+
+static void initAssertionInfo(AssertionInfo *info);
+static void copyAssertionInfo(AssertionInfo *target, AssertionInfo *source);
+static DmlOp oppositeDmlOp(DmlOp operation);
+static RowCompareType oppositeCompareType(RowCompareType type);
+static DmlOp labelForComparisonWithAggFuncs(DmlOp label, RowCompareType compOp, int aggFuncs);
+static int16 triggerOnEvents(AssertionInfo *info, Oid relationId);
+static List *triggerOnColumns(AssertionInfo *info, Oid relationId);
+static bool listContainsObjectAddress(List *list, ObjectAddress *address);
+static Bitmapset * strategiesForOperators(List *operators);
+static int functionsForTargetList(List *targetList);
+static int funcMaskForFuncOid(Oid funcOid);
+static Query * queryForSQLFunction(FuncExpr *funcExpr);
+
+/* Expression visiting */
+static bool visitAllNodes(Node *node, AssertionInfo *info);
+static bool visitRangeTblRef(RangeTblRef *node, AssertionInfo *info);
+static bool visitQuery(Query *node, AssertionInfo *info);
+static bool visitSetOperationStmt(SetOperationStmt *node, AssertionInfo *info);
+static bool visitBoolExpr(BoolExpr *node, AssertionInfo *info);
+static bool visitSubLink(SubLink *node, AssertionInfo *info);
+static bool visitFuncExpr(FuncExpr *node, AssertionInfo *info);
+static bool visitExpr(Expr *node, AssertionInfo *info);
+static bool visitVar(Var *node, AssertionInfo *info);
+
+/* TargetList visiting */
+static bool visitAggrefNodes(Node *node, int *aggFuncs);
+static bool visitAggref(Aggref *node, int *aggFuncs);
+static bool visitWindowFunc(WindowFunc *node, int *aggFuncs);
+
+
+static void
+initAssertionInfo(AssertionInfo *info)
+{
+	info->label = DELETE;
+	info->expr = NULL;
+	info->rtable = NIL;
+	info->operators = NIL;
+	info->setOp = SETOP_NONE;
+	info->invert = false;
+	info->inView = false;
+	info->inTargetEntry = false;
+	info->inComparison = false;
+	info->inExists = false;
+
+	info->dependencies = NIL;
+	info->relations = NIL;
+	info->inserts = NIL;
+	info->deletes = NIL;
+	info->updates = NIL;
+	info->columns = NIL;
+}
+
+
+static void
+copyAssertionInfo(AssertionInfo *target, AssertionInfo *source)
+{
+	target->label = source->label;
+	target->expr = source->expr;
+	target->rtable = source->rtable;
+	target->operators = source->operators;
+	target->setOp = source->setOp;
+	target->invert = source->invert;
+	target->inView = source->inView;
+	target->inTargetEntry = source->inTargetEntry;
+	target->inComparison = source->inComparison;
+	target->inExists = source->inExists;
+}
+
+
+static DmlOp
+oppositeDmlOp(DmlOp operation)
+{
+	switch (operation)
+	{
+		case INSERT:
+			return DELETE;
+		case DELETE:
+			return INSERT;
+		case INSERT_DELETE:
+			return INSERT_DELETE;
+	}
+}
+
+
+static RowCompareType
+oppositeCompareType(RowCompareType type)
+{
+	switch (type)
+	{
+		case ROWCOMPARE_LE:
+			return ROWCOMPARE_GE;
+		case ROWCOMPARE_LT:
+			return ROWCOMPARE_GT;
+		case ROWCOMPARE_GT:
+			return ROWCOMPARE_LT;
+		case ROWCOMPARE_GE:
+			return ROWCOMPARE_LE;
+		default:
+			return type; /* we do not flip EQ and NE comparisons */
+	}
+}
+
+
+static DmlOp
+labelForComparisonWithAggFuncs(DmlOp label, RowCompareType compOp, int aggFuncs)
+{
+	int stronger, weaker;
+
+	switch (compOp)
+	{
+		case ROWCOMPARE_LT:
+		case ROWCOMPARE_LE:
+			stronger = MIN_AGG_FUNC;
+			weaker = MAX_AGG_FUNC | COUNT_AGG_FUNC;
+			break;
+
+		case ROWCOMPARE_EQ:
+			stronger = BOOL_AND_AGG_FUNC | EVERY_AGG_FUNC;
+			weaker = BOOL_OR_AGG_FUNC;
+			break;
+
+		case ROWCOMPARE_NE:
+			stronger = BOOL_AND_AGG_FUNC | EVERY_AGG_FUNC;
+			weaker = BOOL_OR_AGG_FUNC;
+			break;
+
+		case ROWCOMPARE_GE:
+		case ROWCOMPARE_GT:
+			stronger = MAX_AGG_FUNC | COUNT_AGG_FUNC;
+			weaker = MIN_AGG_FUNC;
+			break;
+
+		default:
+			stronger = weaker = NO_FUNC;
+	}
+
+	if (MatchesOnly(aggFuncs, weaker))
+		return label;
+	else if (MatchesOnly(aggFuncs, stronger))
+		return oppositeDmlOp(label);
+	else
+		return INSERT_DELETE;
+}
+
+
+static int16
+triggerOnEvents(AssertionInfo *info, Oid relationId)
+{
+	int16 result = 0;
+	if (list_member_oid(info->inserts, relationId))
+		result |= TRIGGER_TYPE_INSERT;
+	if (list_member_oid(info->deletes, relationId))
+		result |= TRIGGER_TYPE_DELETE | TRIGGER_TYPE_TRUNCATE;
+	if (list_member_oid(info->updates, relationId))
+		result |= TRIGGER_TYPE_UPDATE;
+	return result;
+}
+
+
+/*
+ * Returns a list of string nodes, suitable for use in the trigger
+ * definition, that contain the column names to be triggered against
+ * on UPDATE operations.
+ */
+static List *
+triggerOnColumns(AssertionInfo *info, Oid relationId)
+{
+	List     	  *columns = NIL;
+	ListCell 	  *cell;
+
+	foreach(cell, info->columns)
+	{
+		ObjectAddress *column = (ObjectAddress *) lfirst(cell);
+		AttrNumber attrNumber = (AttrNumber) column->objectSubId;
+
+		if (column->objectId == relationId)
+		{
+			Value *name = makeString(get_attname(relationId, attrNumber));
+			columns = lappend(columns, name);
+		}
+	}
+
+	return columns;
+}
+
+
+static bool
+listContainsObjectAddress(List *list, ObjectAddress *address)
+{
+	ListCell 	  *cell;
+	ObjectAddress *item;
+
+	foreach(cell, list)
+	{
+		item = (ObjectAddress *) lfirst(cell);
+		if (item->classId == address->classId &&
+			item->objectId == address->objectId &&
+			item->objectSubId == address->objectSubId)
+			return true;
+	}
+
+	return false;
+}
+
+
+static Bitmapset *
+strategiesForOperators(List *operators)
+{
+	ListCell  *operatorCell;
+	Bitmapset *strategies = NULL;
+
+	foreach(operatorCell, operators)
+	{
+		Oid operator = lfirst_oid(operatorCell);
+		List *interpretations = get_op_btree_interpretation(operator);
+		ListCell *interpretationCell;
+
+		foreach(interpretationCell, interpretations)
+			strategies = bms_add_member(strategies,
+										((OpBtreeInterpretation *) lfirst(interpretationCell))->strategy);
+	}
+
+	return strategies;
+}
+
+
+static int
+functionsForTargetList(List *targetList)
+{
+	int functionMask = NO_FUNC;
+	expression_tree_walker((Node *) targetList, visitAggrefNodes, &functionMask);
+	return functionMask;
+}
+
+
+static int
+funcMaskForFuncOid(Oid funcOid)
+{
+	char *name = get_func_name(funcOid);
+
+	if (name == NULL)
+		return OTHER_FUNC;
+	else if (strncmp(name, "min", strlen("min")) == 0)
+		return MIN_AGG_FUNC;
+	else if (strncmp(name, "max", strlen("max")) == 0)
+		return MAX_AGG_FUNC;
+	else if (strncmp(name, "count", strlen("count")) == 0)
+		return COUNT_AGG_FUNC;
+	else if (strncmp(name, "every", strlen("every")) == 0)
+		return EVERY_AGG_FUNC;
+	else if (strncmp(name, "bool_and", strlen("bool_and")) == 0)
+		return BOOL_AND_AGG_FUNC;
+	else if (strncmp(name, "bool_or", strlen("bool_or")) == 0)
+		return BOOL_OR_AGG_FUNC;
+	else
+		return OTHER_FUNC;
+}
+
+
+static Query *
+queryForSQLFunction(FuncExpr *funcExpr)
+{
+	Oid			funcId;
+	Relation 	procRel;
+	HeapTuple	tuple;
+	Datum		datum;
+	bool		isNull;
+	char	   *sql;
+	List	   *rawParseTree;
+	ParseState *pstate;
+	Query	   *query;
+	SQLFunctionParseInfoPtr pinfo;
+
+	funcId = funcExpr->funcid;
+	procRel = heap_open(ProcedureRelationId, ShareLock);
+
+	tuple = SearchSysCache1(PROCOID, ObjectIdGetDatum(funcId));
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for function %u", funcId);
+
+	datum = SysCacheGetAttr(PROCOID,
+							tuple,
+							Anum_pg_proc_prosrc,
+							&isNull);
+	if (isNull)
+		elog(ERROR, "null prosrc for function %u", funcId);
+
+	sql = TextDatumGetCString(datum);
+	rawParseTree = pg_parse_query(sql);
+
+	pinfo = prepare_sql_fn_parse_info(tuple,
+									  (Node *) funcExpr,
+									  funcExpr->inputcollid);
+
+	pstate = make_parsestate(NULL);
+	pstate->p_sourcetext = sql;
+	sql_fn_parser_setup(pstate, pinfo);
+
+	query = transformTopLevelStmt(pstate, linitial(rawParseTree));
+	free_parsestate(pstate);
+
+	ReleaseSysCache(tuple);
+	heap_close(procRel, NoLock);
+
+	return query;
+}
+
+
+static bool
+visitRangeTblRef(RangeTblRef *node, AssertionInfo *info)
+{
+	RangeTblEntry  *entry;
+	Oid				relationId;
+	RTEKind			rtekind;
+	char 		    relkind;
+	bool			result;
+
+	entry = rt_fetch(node->rtindex, info->rtable);
+	rtekind = entry->rtekind;
+	result = false;
+
+	if (rtekind == RTE_RELATION)
+	{
+		relationId = getrelid(node->rtindex, info->rtable);
+		relkind = get_rel_relkind(relationId);
+
+		if (relkind == RELKIND_RELATION)
+		{
+			if (info->label == INSERT || info->label == INSERT_DELETE)
+				info->inserts = list_append_unique_oid(info->inserts, relationId);
+
+			if (info->label == DELETE || info->label == INSERT_DELETE)
+				info->deletes = list_append_unique_oid(info->deletes, relationId);
+
+			if (!info->inView)
+				info->dependencies = list_append_unique_oid(info->dependencies, relationId);
+
+			info->relations = list_append_unique_oid(info->relations, relationId);
+
+		}
+		else if (relkind == RELKIND_VIEW)
+		{
+			Relation view = heap_open(relationId, AccessShareLock);
+			Query *query = get_view_query(view);
+
+			if (!info->inView)
+				info->dependencies = list_append_unique_oid(info->dependencies, relationId);
+
+			info->inView = true;
+			result = visitQuery(query, info);
+
+			heap_close(view, NoLock);
+		}
+
+	}
+	else if (rtekind == RTE_TABLEFUNC)
+	{
+		result = visitAllNodes((Node *) entry->tablefunc, info);
+	}
+	else if (rtekind == RTE_FUNCTION)
+	{
+		result = visitAllNodes((Node *) entry->functions, info);
+	}
+	else if (rtekind == RTE_SUBQUERY)
+	{
+		result = visitQuery(entry->subquery, info);
+	}
+
+	return result;
+}
+
+
+static bool
+visitQuery(Query *node, AssertionInfo *info)
+{
+	info->rtable = node->rtable;
+
+	if (info->inComparison)
+	{
+		int functions = functionsForTargetList(node->targetList);
+		Bitmapset *strategies = strategiesForOperators(info->operators);
+
+		if (CanOptimise(strategies))
+		{
+			RowCompareType rowCompareType = (RowCompareType) bms_first_member(strategies);
+			info->label = labelForComparisonWithAggFuncs(
+				info->label,
+				info->invert ? oppositeCompareType(rowCompareType) : rowCompareType,
+				functions
+			);
+		}
+		else
+		{
+			/*
+			 * Either no btree interpretation was found for the operator(s), there were
+			 * multiple interpretations that were incompatible with each other, or the
+			 * found interpretations were not able to be optimised. We must therefore
+			 * assume that both INSERT and DELETE operations may be invalidating.
+			 */
+			info->label = INSERT_DELETE;
+		}
+	}
+	else if (node->hasWindowFuncs)
+	{
+		info->label = INSERT_DELETE;
+	}
+
+	return query_tree_walker(node,
+							 visitAllNodes,
+							 info,
+							 QTW_IGNORE_RANGE_TABLE);
+}
+
+
+static bool
+visitSetOperationStmt(SetOperationStmt *node, AssertionInfo *info)
+{
+	info->setOp = node->op;
+
+	if (visitAllNodes(node->larg, info))
+		return true;
+
+	if (info->setOp == SETOP_EXCEPT)
+		info->label = oppositeDmlOp(info->label);
+
+	return visitAllNodes(node->rarg, info);
+}
+
+
+static bool
+visitBoolExpr(BoolExpr *node, AssertionInfo *info)
+{
+	if (node->boolop == NOT_EXPR)
+		info->label = oppositeDmlOp(info->label);
+
+	return expression_tree_walker((Node *) node, visitAllNodes, (void *) info);
+}
+
+
+// TODO refactor this function
+static bool
+visitSubLink(SubLink *node, AssertionInfo *info)
+{
+	List *operators;
+	bool  invert;
+	bool  inComparison;
+	bool  inExists;
+
+	switch (node->subLinkType)
+	{
+		case ARRAY_SUBLINK: // TODO write tests for this
+		case EXPR_SUBLINK:
+		{
+			Expr *expr = info->expr;
+			bool hasExpr = (expr != NULL);
+
+			if (hasExpr && IsA(expr, OpExpr))
+			{
+				OpExpr *opExpr = (OpExpr *) expr;
+				inComparison = true;
+				inExists = false;
+
+				/*
+				 * Optimisation code is written under the assumption that the sub-select is the
+				 * right operand. If it is the left operand the comparison needs to be inverted.
+				 */
+				invert = (list_nth_node(SubLink, opExpr->args, 0) == node);
+				operators = list_make1_oid(opExpr->opno);
+			}
+			else if (hasExpr && IsA(expr, FuncExpr) &&
+					((FuncExpr *)expr)->funcresulttype == BOOLOID)
+			{
+				/*
+				 * We are inside a function invocation that returns Boolean but is not an OpExpr.
+				 * Let's exploit the fact that "expr == TRUE -> expr", and pretend there is an
+				 * equality operator.
+				 */
+				inComparison = true;
+				inExists = false;
+				operators = list_make1_oid(F_BOOLEQ);
+				invert = false;
+			}
+			else if (exprType((const Node *) node) == BOOLOID)
+			{
+				/*
+				 * We are _not_ inside either an OpExpr or FuncExpr, but we are a query that can be
+				 * coerced to Boolean. We use the same logic above e.g. "expr == TRUE -> expr"
+				 */
+				inComparison = true;
+				inExists = false;
+				operators = list_make1_oid(F_BOOLEQ);
+				invert = false;
+			}
+			else
+			{
+				inComparison = false;
+				inExists = false;
+				invert = false;
+				operators = NIL;
+			}
+		}
+		break;
+
+		case ALL_SUBLINK:
+		case ANY_SUBLINK:
+		case ROWCOMPARE_SUBLINK:
+		{
+			if (IsA(node->testexpr, OpExpr))
+			{
+				OpExpr *expr = (OpExpr *) node->testexpr;
+				inComparison = true;
+				inExists = false;
+				invert = false;
+				operators = list_make1_oid(expr->opno);
+			}
+			else if (IsA(node->testexpr, RowCompareExpr))
+			{
+				RowCompareExpr *expr = (RowCompareExpr *) node->testexpr;
+				inComparison = true;
+				inExists = false;
+				invert = false;
+				operators = list_copy(expr->opnos);
+			}
+			else
+			{
+				inComparison = false;
+				inExists = false;
+				invert = false;
+				operators = NIL;
+			}
+		}
+		break;
+
+		case EXISTS_SUBLINK:
+		{
+			/* existential quantification, no operators */
+			inComparison = false;
+			inExists = true;
+			invert = false;
+			operators = NIL;
+		}
+		break;
+			// case MULTIEXPR_SUBLINK: // TODO write tests for MULTIEXPR_SUBLINK? -- or, can it only occur in UPDATEs?
+		default:
+		{
+			elog(ERROR, "unhandled sublink type %u", node->subLinkType);
+			return true;
+		}
+
+	}
+
+	info->operators = operators;
+	info->invert = invert;
+	info->inComparison = inComparison;
+	info->inExists = inExists;
+	info->setOp = SETOP_NONE;
+
+	return expression_tree_walker((Node *) node, visitAllNodes, (void *) info);
+}
+
+
+static bool
+visitTargetEntry(TargetEntry *node, AssertionInfo *info)
+{
+	info->inTargetEntry = true;
+	return expression_tree_walker((Node *) node, visitAllNodes, (void *) info);
+}
+
+
+static bool
+visitFromExpr(FromExpr *node, AssertionInfo *info)
+{
+	info->inExists = false;
+	info->inComparison = false;
+	return expression_tree_walker((Node *) node, visitAllNodes, (void *) info);
+}
+
+
+static bool
+visitFuncExpr(FuncExpr *node, AssertionInfo *info)
+{
+	Oid lang = get_func_lang(node->funcid);
+
+	info->expr = (Expr *) node;
+
+	if (!(lang == INTERNALlanguageId || lang == SQLlanguageId))
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_AMBIGUOUS_FUNCTION),
+				 errmsg("function \"%s\" uses unsupported language \"%s\"",
+				   get_func_name(node->funcid), get_language_name(lang, false))));
+		return true;
+	}
+
+	if (expression_tree_walker((Node *) node, visitAllNodes, (void *) info))
+		return true;
+
+	if (lang == SQLlanguageId)
+	{
+		Query *query = queryForSQLFunction(node);
+		Node *next;
+		if (node->funcretset)
+			next = (Node *) query;
+		else
+			next = (Node *) ((TargetEntry *) linitial(query->targetList))->expr;
+
+		return visitAllNodes(next, info);
+	}
+
+	return false;
+}
+
+
+static bool
+visitExpr(Expr *node, AssertionInfo *info)
+{
+	info->expr = node;
+	return expression_tree_walker((Node *) node, visitAllNodes, (void *) info);
+}
+
+
+static bool
+visitVar(Var *node, AssertionInfo *info)
+{
+	RangeTblEntry *entry = rt_fetch(node->varno, info->rtable);
+
+	// TODO we should do this only for Tables and not Views?
+	// TODO optimisations for set-operations
+	if (entry->rtekind == RTE_RELATION && !(info->inExists && info->setOp == SETOP_NONE))
+	{
+		Oid relationId = getrelid(node->varno, info->rtable);
+		ObjectAddress *column = (ObjectAddress *) palloc(sizeof(ObjectAddress));
+
+		column->classId = RelationRelationId;
+		column->objectId = relationId;
+		column->objectSubId = node->varattno;
+
+		if (!listContainsObjectAddress(info->columns, column))
+			info->columns = lcons(column, info->columns);
+
+		info->relations = list_append_unique_oid(info->relations, relationId);
+		info->updates = list_append_unique_oid(info->updates, relationId);
+	}
+
+	return expression_tree_walker((Node *) node, visitAllNodes, (void *) info);
+}
+
+
+static bool
+visitAllNodes(Node *node, AssertionInfo *info)
+{
+	AssertionInfo stored;
+	bool result;
+
+	copyAssertionInfo(&stored, info);
+
+	if (node == NULL)
+		result = false;
+	else if (IsA(node, RangeTblRef))
+		result = visitRangeTblRef((RangeTblRef *) node, info);
+	else if (IsA(node, Query))
+		result = visitQuery((Query *) node, info);
+	else if (IsA(node, BoolExpr))
+		result = visitBoolExpr((BoolExpr *) node, info);
+	else if (IsA(node, SubLink))
+		result = visitSubLink((SubLink *) node, info);
+	else if (IsA(node, SetOperationStmt))
+		result = visitSetOperationStmt((SetOperationStmt *) node, info);
+	else if (IsA(node, FuncExpr))
+		result = visitFuncExpr((FuncExpr *) node, info);
+	else if (IsA(node, OpExpr) ||
+			 IsA(node, DistinctExpr) ||
+			 IsA(node, NullIfExpr))
+		result = visitExpr((Expr *) node, info);
+	else if (IsA(node, TargetEntry))
+		result = visitTargetEntry((TargetEntry *) node, info);
+	else if (IsA(node, Var))
+		result = visitVar((Var *) node, info);
+	else if (IsA(node, FromExpr))
+		result = visitFromExpr((FromExpr *) node, info);
+	else
+		result = expression_tree_walker(node, visitAllNodes, (void *) info);
+
+	copyAssertionInfo(info, &stored);
+
+	return result;
+}
+
+
+static bool
+visitAggrefNodes(Node *node, int *aggFuncs)
+{
+	if (node == NULL)
+		return false;
+	else if (IsA(node, Aggref))
+		return visitAggref((Aggref *) node, aggFuncs);
+	else if(IsA(node, WindowFunc))
+		return visitWindowFunc((WindowFunc *) node, aggFuncs);
+	else
+		return expression_tree_walker(node, visitAggrefNodes, (void *) aggFuncs);
+}
+
+
+static bool
+visitAggref(Aggref *node, int *aggFuncs)
+{
+	*aggFuncs |= funcMaskForFuncOid(node->aggfnoid);
+	return expression_tree_walker((Node *) node, visitAggrefNodes, (void *) aggFuncs);
+}
+
+
+static bool
+visitWindowFunc(WindowFunc *node, int *aggFuncs)
+{
+	*aggFuncs |= funcMaskForFuncOid(node->winfnoid);
+
+	return expression_tree_walker((Node *) node, visitAggrefNodes, (void *) aggFuncs);
+}
+
+
+ObjectAddress
+CreateAssertion(CreateAssertionStmt *stmt)
+{
+	Oid namespaceId;
+	char *assertion_name;
+	AclResult aclresult;
+	Node *expr;
+	ParseState *pstate;
+	char *ccsrc;
+	char *ccbin;
+	Oid constrOid;
+	AssertionInfo info;
+	ListCell *lc;
+	ObjectAddress address;
+
+	namespaceId = QualifiedNameGetCreationNamespace(stmt->assertion_name,
+													&assertion_name);
+
+	aclresult = pg_namespace_aclcheck(namespaceId, GetUserId(), ACL_CREATE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, ACL_KIND_NAMESPACE,
+					   get_namespace_name(namespaceId));
+
+	if (ConstraintNameIsUsed(CONSTRAINT_ASSERTION, InvalidOid, namespaceId, assertion_name))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_NAME),
+				 errmsg("assertion \"%s\" already exists", assertion_name)));
+
+	pstate = make_parsestate(NULL);
+	expr = transformExpr(pstate, stmt->constraint->raw_expr, EXPR_KIND_ASSERTION_CHECK);
+	expr = coerce_to_boolean(pstate, expr, "CHECK");
+
+	ccbin = nodeToString(expr);
+	ccsrc = deparse_expression(expr, NIL, false, false);
+
+	constrOid = CreateConstraintEntry(assertion_name,
+									  namespaceId,
+									  CONSTRAINT_CHECK, /* constraint type */
+									  stmt->constraint->deferrable,
+									  stmt->constraint->initdeferred,
+									  !stmt->constraint->skip_validation,
+									  InvalidOid, /* not a relation constraint */
+									  NULL,          /* no keys */
+									  0,          /* no keys */
+									  InvalidOid, /* not a domain constraint */
+									  InvalidOid, /* no associated index */
+									  InvalidOid, /* foreign key fields ... */
+									  NULL,
+									  NULL,
+									  NULL,
+									  NULL,
+									  0,
+									  ' ',
+									  ' ',
+									  ' ',
+									  NULL,    /* not an exclusion constraint */
+									  expr, /* tree form of check constraint */
+									  ccbin, /* binary form of check constraint */
+									  ccsrc, /* source form of check constraint */
+									  true, /* is local */
+									  0,   /* inhcount */
+									  false, /* noinherit XXX */
+									  false); /* is_internal */
+
+	initAssertionInfo(&info);
+	visitAllNodes(expr, &info);
+
+	foreach (lc, info.relations)
+	{
+		Oid relationId = lfirst_oid(lc);
+		CreateTrigStmt *trigger;
+		Relation rel;
+
+		rel = heap_open(relationId, ShareLock); // XXX
+
+		trigger = makeNode(CreateTrigStmt);
+		trigger->trigname = "AssertionTrigger";
+		trigger->relation = makeRangeVar(get_namespace_name(namespaceId),
+										 pstrdup(RelationGetRelationName(rel)),
+										 -1);
+		trigger->funcname = SystemFuncName("assertion_check");
+		trigger->args = NIL;
+		trigger->row = false;
+		trigger->timing = TRIGGER_TYPE_AFTER;
+		trigger->events = triggerOnEvents(&info, relationId);
+		trigger->columns = triggerOnColumns(&info, relationId);
+		trigger->whenClause = NULL;
+		trigger->isconstraint = true;
+		trigger->deferrable = stmt->constraint->deferrable;
+		trigger->initdeferred = stmt->constraint->initdeferred;
+		trigger->constrrel = NULL;
+
+		CreateTrigger(trigger, NULL, InvalidOid, relationId, constrOid, InvalidOid, true);
+
+		heap_close(rel, NoLock);
+	}
+
+	/*
+	 * Record dependencies between the constraint and the relations found in the
+	 * top-level expression. Dependencies to specific columns will already have
+	 * been recorded by the trigger creation.
+	 */
+	ObjectAddress myself, referenced;
+	ListCell *cell;
+
+	myself.classId = ConstraintRelationId;
+	myself.objectId = constrOid;
+	myself.objectSubId = 0;
+
+	foreach (cell, info.dependencies)
+	{
+		referenced.classId = RelationRelationId;
+		referenced.objectId = lfirst_oid(cell);
+		referenced.objectSubId = 0;
+		recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+	}
+
+	test_assertion_expr(assertion_name, ccsrc);
+
+	ObjectAddressSet(address, ConstraintRelationId, constrOid);
+
+	return address;
+}
+
+
+ObjectAddress
+RenameAssertion(RenameStmt *stmt)
+{
+	Oid           assertionOid;
+	ObjectAddress address;
+	Relation      rel;
+	HeapTuple     tuple;
+	Form_pg_constraint con;
+	AclResult     aclresult;
+
+	List *oldName = castNode(List, stmt->object);
+	char *newName = stmt->newname;
+
+	rel = heap_open(ConstraintRelationId, RowExclusiveLock);
+	assertionOid = get_assertion_oid(oldName, false);
+
+	tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(assertionOid));
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for constraint %u",
+			 assertionOid);
+	con = (Form_pg_constraint) GETSTRUCT(tuple);
+
+	/* must be owner */
+	if (!pg_constraint_ownercheck(assertionOid, GetUserId()))
+		aclcheck_error(ACLCHECK_NOT_OWNER, ACL_KIND_CONVERSION,
+					   NameListToString(oldName));
+
+	/* must have CREATE privilege on namespace */
+	aclresult = pg_namespace_aclcheck(con->connamespace, GetUserId(), ACL_CREATE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, ACL_KIND_NAMESPACE,
+					   get_namespace_name(con->connamespace));
+
+	ReleaseSysCache(tuple);
+	RenameConstraintById(assertionOid, newName);
+	ObjectAddressSet(address, ConstraintRelationId, assertionOid);
+	heap_close(rel, NoLock);
+
+	return address;
+}
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index 8455138ed3..085645007d 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -88,6 +88,7 @@ typedef enum
 static event_trigger_support_data event_trigger_support[] = {
 	{"ACCESS METHOD", true},
 	{"AGGREGATE", true},
+	{"ASSERTION", true},
 	{"CAST", true},
 	{"CONSTRAINT", true},
 	{"COLLATION", true},
@@ -1081,6 +1082,7 @@ EventTriggerSupportsObjectType(ObjectType obtype)
 			return false;
 		case OBJECT_ACCESS_METHOD:
 		case OBJECT_AGGREGATE:
+		case OBJECT_ASSERTION:
 		case OBJECT_AMOP:
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index ddbbc79823..0543995939 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -4304,6 +4304,17 @@ _copyCreateSchemaStmt(const CreateSchemaStmt *from)
 	return newnode;
 }
 
+static CreateAssertionStmt *
+_copyCreateAssertionStmt(const CreateAssertionStmt *from)
+{
+	CreateAssertionStmt *newnode = makeNode(CreateAssertionStmt);
+
+	COPY_NODE_FIELD(assertion_name);
+	COPY_NODE_FIELD(constraint);
+
+	return newnode;
+}
+
 static CreateConversionStmt *
 _copyCreateConversionStmt(const CreateConversionStmt *from)
 {
@@ -5373,6 +5384,9 @@ copyObjectImpl(const void *from)
 		case T_CreateSchemaStmt:
 			retval = _copyCreateSchemaStmt(from);
 			break;
+		case T_CreateAssertionStmt:
+			retval = _copyCreateAssertionStmt(from);
+			break;
 		case T_CreateConversionStmt:
 			retval = _copyCreateConversionStmt(from);
 			break;
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 30ccc9c5ae..42323dac89 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -2117,6 +2117,15 @@ _equalCreateSchemaStmt(const CreateSchemaStmt *a, const CreateSchemaStmt *b)
 	return true;
 }
 
+static bool
+_equalCreateAssertionStmt(const CreateAssertionStmt *a, const CreateAssertionStmt *b)
+{
+	COMPARE_NODE_FIELD(assertion_name);
+	COMPARE_NODE_FIELD(constraint);
+
+	return true;
+}
+
 static bool
 _equalCreateConversionStmt(const CreateConversionStmt *a, const CreateConversionStmt *b)
 {
@@ -3511,6 +3520,9 @@ equal(const void *a, const void *b)
 		case T_CreateSchemaStmt:
 			retval = _equalCreateSchemaStmt(a, b);
 			break;
+		case T_CreateAssertionStmt:
+			retval = _equalCreateAssertionStmt(a, b);
+			break;
 		case T_CreateConversionStmt:
 			retval = _equalCreateConversionStmt(a, b);
 			break;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index e42b7caff6..89fdbb1793 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -259,11 +259,11 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 		CreateOpFamilyStmt AlterOpFamilyStmt CreatePLangStmt
 		CreateSchemaStmt CreateSeqStmt CreateStmt CreateStatsStmt CreateTableSpaceStmt
 		CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt
-		CreateAssertStmt CreateTransformStmt CreateTrigStmt CreateEventTrigStmt
+		CreateAssertionStmt CreateTransformStmt CreateTrigStmt CreateEventTrigStmt
 		CreateUserStmt CreateUserMappingStmt CreateRoleStmt CreatePolicyStmt
 		CreatedbStmt DeclareCursorStmt DefineStmt DeleteStmt DiscardStmt DoStmt
 		DropOpClassStmt DropOpFamilyStmt DropPLangStmt DropStmt
-		DropAssertStmt DropCastStmt DropRoleStmt
+		DropCastStmt DropRoleStmt
 		DropdbStmt DropTableSpaceStmt
 		DropTransformStmt
 		DropUserMappingStmt ExplainStmt FetchStmt
@@ -854,7 +854,7 @@ stmt :
 			| CopyStmt
 			| CreateAmStmt
 			| CreateAsStmt
-			| CreateAssertStmt
+			| CreateAssertionStmt
 			| CreateCastStmt
 			| CreateConversionStmt
 			| CreateDomainStmt
@@ -890,7 +890,6 @@ stmt :
 			| DeleteStmt
 			| DiscardStmt
 			| DoStmt
-			| DropAssertStmt
 			| DropCastStmt
 			| DropOpClassStmt
 			| DropOpFamilyStmt
@@ -5591,45 +5590,28 @@ enable_trigger:
  *
  *		QUERIES :
  *				CREATE ASSERTION ...
- *				DROP ASSERTION ...
  *
  *****************************************************************************/
 
-CreateAssertStmt:
-			CREATE ASSERTION name CHECK '(' a_expr ')'
+CreateAssertionStmt:
+			CREATE ASSERTION any_name CHECK '(' a_expr ')'
 			ConstraintAttributeSpec
 				{
-					CreateTrigStmt *n = makeNode(CreateTrigStmt);
-					n->trigname = $3;
-					n->args = list_make1($6);
-					n->isconstraint  = true;
-					processCASbits($8, @8, "ASSERTION",
-								   &n->deferrable, &n->initdeferred, NULL,
-								   NULL, yyscanner);
-
-					ereport(ERROR,
-							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-							 errmsg("CREATE ASSERTION is not yet implemented")));
-
-					$$ = (Node *)n;
-				}
-		;
-
-DropAssertStmt:
-			DROP ASSERTION name opt_drop_behavior
-				{
-					DropStmt *n = makeNode(DropStmt);
-					n->objects = NIL;
-					n->behavior = $4;
-					n->removeType = OBJECT_TRIGGER; /* XXX */
-					ereport(ERROR,
-							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-							 errmsg("DROP ASSERTION is not yet implemented")));
-					$$ = (Node *) n;
+					CreateAssertionStmt *n = makeNode(CreateAssertionStmt);
+					Constraint *c = makeNode(Constraint);
+					c->contype = CONSTR_CHECK;
+					c->location = @4;
+					c->raw_expr = $6;
+					c->cooked_expr = NULL;
+ 					processCASbits($8, @8, "ASSERTION",
+								   &c->deferrable, &c->initdeferred, NULL,
+ 								   NULL, yyscanner);
+					n->assertion_name = $3;
+					n->constraint = c;
+ 					$$ = (Node *)n;
 				}
 		;
 
-
 /*****************************************************************************
  *
  *		QUERY :
@@ -6283,6 +6265,7 @@ drop_type_any_name:
 			| MATERIALIZED VIEW						{ $$ = OBJECT_MATVIEW; }
 			| INDEX									{ $$ = OBJECT_INDEX; }
 			| FOREIGN TABLE							{ $$ = OBJECT_FOREIGN_TABLE; }
+			| ASSERTION								{ $$ = OBJECT_ASSERTION; }
 			| COLLATION								{ $$ = OBJECT_COLLATION; }
 			| CONVERSION_P							{ $$ = OBJECT_CONVERSION; }
 			| STATISTICS							{ $$ = OBJECT_STATISTIC_EXT; }
@@ -6552,6 +6535,7 @@ comment_type_any_name:
 			| TABLE								{ $$ = OBJECT_TABLE; }
 			| VIEW								{ $$ = OBJECT_VIEW; }
 			| MATERIALIZED VIEW					{ $$ = OBJECT_MATVIEW; }
+			| ASSERTION							{ $$ = OBJECT_ASSERTION; }
 			| COLLATION							{ $$ = OBJECT_COLLATION; }
 			| CONVERSION_P						{ $$ = OBJECT_CONVERSION; }
 			| FOREIGN TABLE						{ $$ = OBJECT_FOREIGN_TABLE; }
@@ -8402,6 +8386,15 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name
 					n->missing_ok = false;
 					$$ = (Node *)n;
 				}
+			| ALTER ASSERTION any_name RENAME TO name
+				{
+					RenameStmt *n = makeNode(RenameStmt);
+					n->renameType = OBJECT_ASSERTION;
+					n->object = (Node *) $3;
+					n->newname = $6;
+					n->missing_ok = false;
+					$$ = (Node *)n;
+				}
 			| ALTER COLLATION any_name RENAME TO name
 				{
 					RenameStmt *n = makeNode(RenameStmt);
@@ -8986,6 +8979,15 @@ AlterObjectSchemaStmt:
 					n->missing_ok = false;
 					$$ = (Node *)n;
 				}
+			| ALTER ASSERTION any_name SET SCHEMA name
+				{
+					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
+					n->objectType = OBJECT_ASSERTION;
+					n->object = (Node *) $3;
+					n->newschema = $6;
+					n->missing_ok = false;
+					$$ = (Node *)n;
+				}
 			| ALTER COLLATION any_name SET SCHEMA name
 				{
 					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
@@ -9271,6 +9273,14 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
 					n->newowner = $6;
 					$$ = (Node *)n;
 				}
+			| ALTER ASSERTION any_name OWNER TO RoleSpec
+				{
+					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
+					n->objectType = OBJECT_ASSERTION;
+					n->object = (Node *) $3;
+					n->newowner = $6;
+					$$ = (Node *)n;
+				}
 			| ALTER COLLATION any_name OWNER TO RoleSpec
 				{
 					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c
index 6a9f1b0217..08d233ba3a 100644
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -450,6 +450,7 @@ check_agglevels_and_constraints(ParseState *pstate, Node *expr)
 			break;
 		case EXPR_KIND_CHECK_CONSTRAINT:
 		case EXPR_KIND_DOMAIN_CHECK:
+		case EXPR_KIND_ASSERTION_CHECK:
 			if (isAgg)
 				err = _("aggregate functions are not allowed in check constraints");
 			else
@@ -867,6 +868,7 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
 			break;
 		case EXPR_KIND_CHECK_CONSTRAINT:
 		case EXPR_KIND_DOMAIN_CHECK:
+		case EXPR_KIND_ASSERTION_CHECK:
 			err = _("window functions are not allowed in check constraints");
 			break;
 		case EXPR_KIND_COLUMN_DEFAULT:
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index b2f5e46e3b..a8ada122ac 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -1817,6 +1817,7 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
 		case EXPR_KIND_RETURNING:
 		case EXPR_KIND_VALUES:
 		case EXPR_KIND_VALUES_SINGLE:
+		case EXPR_KIND_ASSERTION_CHECK:
 		case EXPR_KIND_CALL:
 			/* okay */
 			break;
@@ -3452,6 +3453,7 @@ ParseExprKindName(ParseExprKind exprKind)
 			return "VALUES";
 		case EXPR_KIND_CHECK_CONSTRAINT:
 		case EXPR_KIND_DOMAIN_CHECK:
+		case EXPR_KIND_ASSERTION_CHECK:
 			return "CHECK";
 		case EXPR_KIND_COLUMN_DEFAULT:
 		case EXPR_KIND_FUNCTION_DEFAULT:
diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c
index ffae0f3cf3..9acf54b383 100644
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -2263,6 +2263,10 @@ check_srf_call_placement(ParseState *pstate, Node *last_srf, int location)
 			/* okay, since we process this like a SELECT tlist */
 			pstate->p_hasTargetSRFs = true;
 			break;
+		case EXPR_KIND_ASSERTION_CHECK:
+			/* okay */
+			pstate->p_hasTargetSRFs = true;
+			break;
 		case EXPR_KIND_CHECK_CONSTRAINT:
 		case EXPR_KIND_DOMAIN_CHECK:
 			err = _("set-returning functions are not allowed in check constraints");
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index ec98a612ec..fee335502c 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -29,6 +29,7 @@
 #include "commands/cluster.h"
 #include "commands/comment.h"
 #include "commands/collationcmds.h"
+#include "commands/constraint.h"
 #include "commands/conversioncmds.h"
 #include "commands/copy.h"
 #include "commands/createas.h"
@@ -159,6 +160,7 @@ check_xact_readonly(Node *parsetree)
 		case T_RenameStmt:
 		case T_CommentStmt:
 		case T_DefineStmt:
+		case T_CreateAssertionStmt:
 		case T_CreateCastStmt:
 		case T_CreateEventTrigStmt:
 		case T_AlterEventTrigStmt:
@@ -1493,6 +1495,10 @@ ProcessUtilitySlow(ParseState *pstate,
 				address = DefineDomain((CreateDomainStmt *) parsetree);
 				break;
 
+			case T_CreateAssertionStmt:
+				address = CreateAssertion((CreateAssertionStmt *) parsetree);
+				break;
+
 			case T_CreateConversionStmt:
 				address = CreateConversionCommand((CreateConversionStmt *) parsetree);
 				break;
@@ -1903,6 +1909,9 @@ AlterObjectTypeCommandTag(ObjectType objtype)
 		case OBJECT_AGGREGATE:
 			tag = "ALTER AGGREGATE";
 			break;
+		case OBJECT_ASSERTION:
+			tag = "ALTER ASSERTION";
+			break;
 		case OBJECT_ATTRIBUTE:
 			tag = "ALTER TYPE";
 			break;
@@ -2241,6 +2250,9 @@ CreateCommandTag(Node *parsetree)
 				case OBJECT_DOMAIN:
 					tag = "DROP DOMAIN";
 					break;
+				case OBJECT_ASSERTION:
+					tag = "DROP ASSERTION";
+					break;
 				case OBJECT_COLLATION:
 					tag = "DROP COLLATION";
 					break;
@@ -2671,6 +2683,10 @@ CreateCommandTag(Node *parsetree)
 			tag = "REINDEX";
 			break;
 
+		case T_CreateAssertionStmt:
+			tag = "CREATE ASSERTION";
+			break;
+
 		case T_CreateConversionStmt:
 			tag = "CREATE CONVERSION";
 			break;
@@ -3271,6 +3287,10 @@ GetCommandLogLevel(Node *parsetree)
 			lev = LOGSTMT_ALL;	/* should this be DDL? */
 			break;
 
+		case T_CreateAssertionStmt:
+			lev = LOGSTMT_DDL;
+			break;
+
 		case T_CreateConversionStmt:
 			lev = LOGSTMT_DDL;
 			break;
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index e8aa179347..dbbe1a5513 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -1557,6 +1557,25 @@ get_func_retset(Oid funcid)
 	return result;
 }
 
+/*
+ * get_func_lang
+ *		Given procedure id, return the function's language id.
+ */
+Oid
+get_func_lang(Oid funcid)
+{
+	HeapTuple	tp;
+	Oid			result;
+
+	tp = SearchSysCache1(PROCOID, ObjectIdGetDatum(funcid));
+	if (!HeapTupleIsValid(tp))
+		elog(ERROR, "cache lookup failed for function %u", funcid);
+
+	result = ((Form_pg_proc) GETSTRUCT(tp))->prolang;
+	ReleaseSysCache(tp);
+	return result;
+}
+
 /*
  * func_strict
  *		Given procedure id, return the function's proisstrict flag.
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 015c391aa4..928e280090 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -785,6 +785,9 @@ exec_command_d(PsqlScanState scan_state, bool active_branch, const char *cmd)
 			case 'p':
 				success = permissionsList(pattern);
 				break;
+			case 'Q':
+				success = describeAssertions(pattern);
+				break;
 			case 'T':
 				success = describeTypes(pattern, show_verbose, show_system);
 				break;
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index d2787ab41b..41ebc2ce13 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -132,6 +132,66 @@ describeAggregates(const char *pattern, bool verbose, bool showSystem)
 	return true;
 }
 
+/* \dA
+ * Takes an optional regexp to select particular assertions
+ */
+bool
+describeAssertions(const char *pattern)
+{
+	PQExpBufferData buf;
+	PGresult   *res;
+	printQueryOpt myopt = pset.popt;
+
+	if (pset.sversion < 90400)
+	{
+		fprintf(stderr, _("The server (version %d.%d) does not support assertions.\n"),
+				pset.sversion / 10000, (pset.sversion / 100) % 100);
+		return true;
+	}
+
+	initPQExpBuffer(&buf);
+
+	printfPQExpBuffer(&buf,
+					  "SELECT n.nspname AS \"%s\",\n"
+							  "  c.conname AS \"%s\",\n"
+							  "  pg_catalog.pg_get_constraintdef(c.oid, true) AS \"%s\",\n"
+							  "  pg_catalog.obj_description(c.oid, 'pg_constraint') AS \"%s\"\n"
+							  "FROM pg_catalog.pg_constraint c\n"
+							  "     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.connamespace\n"
+							  "WHERE c.conrelid = 0 AND c.contypid = 0\n",
+					  gettext_noop("Schema"),
+					  gettext_noop("Name"),
+					  gettext_noop("Definition"),
+					  gettext_noop("Description"));
+
+	if (!pattern)
+		appendPQExpBuffer(&buf, "      AND n.nspname <> 'pg_catalog'\n"
+				"      AND n.nspname <> 'information_schema'\n");
+
+#if TODO
+	processSQLNamePattern(pset.db, &buf, pattern, true, false,
+						  "n.nspname", "c.conname", NULL,
+						  "pg_catalog.pg_constraint_is_visible(c.oid)");
+#endif
+
+	appendPQExpBuffer(&buf, "ORDER BY 1, 2, 3, 4;");
+
+	res = PSQLexec(buf.data);
+	termPQExpBuffer(&buf);
+	if (!res)
+		return false;
+
+	myopt.nullPrint = NULL;
+	myopt.title = _("List of assertions");
+	myopt.translate_header = true;
+
+	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
+
+	PQclear(res);
+	return true;
+}
+
+
 /*
  * \dA
  * Takes an optional regexp to select particular access methods
@@ -2475,6 +2535,38 @@ describeOneTableDetails(const char *schemaname,
 			PQclear(result);
 		}
 
+		/* print assertions referencing this table (none if no triggers) */
+		if (tableinfo.hastriggers)
+		{
+			printfPQExpBuffer(&buf,
+							  "SELECT conname,\n"
+									  "  pg_catalog.pg_get_constraintdef(c.oid, true) AS condef\n"
+									  "FROM pg_catalog.pg_constraint c\n"
+									  "WHERE c.oid IN (SELECT objid FROM pg_depend WHERE classid = 'pg_constraint'::pg_catalog.regclass AND refclassid = 'pg_class'::pg_catalog.regclass AND refobjid = '%s')\n"
+									  "  AND c.conrelid = 0 AND c.contypid = 0 AND c.contype = 'c'\n"
+									  "ORDER BY 1",
+							  oid);
+			result = PSQLexec(buf.data);
+			if (!result)
+				goto error_return;
+			else
+				tuples = PQntuples(result);
+
+			if (tuples > 0)
+			{
+				printTableAddFooter(&cont, _("Assertions:"));
+				for (i = 0; i < tuples; i++)
+				{
+					printfPQExpBuffer(&buf, "    \"%s\" %s",
+									  PQgetvalue(result, i, 0),
+									  PQgetvalue(result, i, 1));
+
+					printTableAddFooter(&cont, buf.data);
+				}
+			}
+			PQclear(result);
+		}
+
 		/* print rules */
 		if (tableinfo.hasrules && tableinfo.relkind != RELKIND_MATVIEW)
 		{
diff --git a/src/bin/psql/describe.h b/src/bin/psql/describe.h
index a4cc5efae0..7b405b65da 100644
--- a/src/bin/psql/describe.h
+++ b/src/bin/psql/describe.h
@@ -15,6 +15,9 @@ extern bool describeAggregates(const char *pattern, bool verbose, bool showSyste
 /* \dA */
 extern bool describeAccessMethods(const char *pattern, bool verbose);
 
+/* \dQ */
+extern bool describeAssertions(const char *pattern);
+
 /* \db */
 extern bool describeTablespaces(const char *pattern, bool verbose);
 
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index 702e742af4..9deabd195f 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -223,6 +223,7 @@ slashUsage(unsigned short int pager)
 	fprintf(output, _("  \\d[S+]                 list tables, views, and sequences\n"));
 	fprintf(output, _("  \\d[S+]  NAME           describe table, view, sequence, or index\n"));
 	fprintf(output, _("  \\da[S]  [PATTERN]      list aggregates\n"));
+	fprintf(output, _("  \\dQ     [PATTERN]      list assertions\n"));
 	fprintf(output, _("  \\dA[+]  [PATTERN]      list access methods\n"));
 	fprintf(output, _("  \\db[+]  [PATTERN]      list tablespaces\n"));
 	fprintf(output, _("  \\dc[S+] [PATTERN]      list conversions\n"));
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index b51098deca..7628ecb816 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -360,6 +360,22 @@ static const SchemaQuery Query_for_list_of_aggregates = {
 	NULL
 };
 
+static const SchemaQuery Query_for_list_of_assertions = {
+    /* catname */
+	"pg_catalog.pg_constraint c",
+	/* selcondition */
+	"(c.conrelid = 0 AND c.contypid = 0)",
+	/* viscondition */
+	"TRUE", //TODO: "pg_catalog.pg_constraint_is_visible(c.oid)",
+	/* namespace */
+	"c.connamespace",
+	/* result */
+	"pg_catalog.quote_ident(c.conname)",
+	/* qualresult */
+	NULL
+};
+
+
 static const SchemaQuery Query_for_list_of_datatypes = {
 	/* catname */
 	"pg_catalog.pg_type t",
@@ -1033,6 +1049,7 @@ typedef struct
 static const pgsql_thing_t words_after_create[] = {
 	{"ACCESS METHOD", NULL, NULL, THING_NO_ALTER},
 	{"AGGREGATE", NULL, &Query_for_list_of_aggregates},
+	{"ASSERTION", NULL, &Query_for_list_of_assertions},
 	{"CAST", NULL, NULL},		/* Casts have complex structures for names, so
 								 * skip it */
 	{"COLLATION", "SELECT pg_catalog.quote_ident(collname) FROM pg_catalog.pg_collation WHERE collencoding IN (-1, pg_catalog.pg_char_to_encoding(pg_catalog.getdatabaseencoding())) AND substring(pg_catalog.quote_ident(collname),1,%d)='%s'"},
@@ -1456,7 +1473,7 @@ psql_completion(const char *text, int start, int end)
 		"\\a",
 		"\\connect", "\\conninfo", "\\C", "\\cd", "\\copy",
 		"\\copyright", "\\crosstabview",
-		"\\d", "\\da", "\\dA", "\\db", "\\dc", "\\dC", "\\dd", "\\ddp", "\\dD",
+		"\\d", "\\da", "\\dA", "\\dQ", "\\db", "\\dc", "\\dC", "\\dd", "\\ddp", "\\dD",
 		"\\des", "\\det", "\\deu", "\\dew", "\\dE", "\\df",
 		"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
 		"\\dm", "\\dn", "\\do", "\\dO", "\\dp",
@@ -1607,6 +1624,11 @@ psql_completion(const char *text, int start, int end)
 	else if (Matches3("ALTER", "SCHEMA", MatchAny))
 		COMPLETE_WITH_LIST2("OWNER TO", "RENAME TO");
 
+	// TODO ASSERTION
+
+	else if (Matches3("ALTER", "ASSERTION", MatchAny))
+		COMPLETE_WITH_CONST("RENAME TO ");
+
 	/* ALTER COLLATION <name> */
 	else if (Matches3("ALTER", "COLLATION", MatchAny))
 		COMPLETE_WITH_LIST3("OWNER TO", "RENAME TO", "SET SCHEMA");
@@ -2720,7 +2742,7 @@ psql_completion(const char *text, int start, int end)
 /* DROP */
 	/* Complete DROP object with CASCADE / RESTRICT */
 	else if (Matches3("DROP",
-					  "COLLATION|CONVERSION|DOMAIN|EXTENSION|LANGUAGE|PUBLICATION|SCHEMA|SEQUENCE|SERVER|SUBSCRIPTION|STATISTICS|TABLE|TYPE|VIEW",
+					  "ASSERTION|COLLATION|CONVERSION|DOMAIN|EXTENSION|LANGUAGE|PUBLICATION|SCHEMA|SEQUENCE|SERVER|SUBSCRIPTION|STATISTICS|TABLE|TYPE|VIEW",
 					  MatchAny) ||
 			 Matches4("DROP", "ACCESS", "METHOD", MatchAny) ||
 			 (Matches4("DROP", "AGGREGATE|FUNCTION|PROCEDURE|ROUTINE", MatchAny, MatchAny) &&
@@ -3452,6 +3474,8 @@ psql_completion(const char *text, int start, int end)
 	}
 	else if (TailMatchesCS1("\\da*"))
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_aggregates, NULL);
+	else if (TailMatchesCS1("\\dQ*"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_assertions, NULL);
 	else if (TailMatchesCS1("\\dA*"))
 		COMPLETE_WITH_QUERY(Query_for_list_of_access_methods);
 	else if (TailMatchesCS1("\\db*"))
diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h
index 5f8cf4992e..841be6db59 100644
--- a/src/include/catalog/namespace.h
+++ b/src/include/catalog/namespace.h
@@ -140,6 +140,7 @@ extern bool OverrideSearchPathMatchesCurrent(OverrideSearchPath *path);
 extern void PushOverrideSearchPath(OverrideSearchPath *newpath);
 extern void PopOverrideSearchPath(void);
 
+extern Oid	get_assertion_oid(List *name, bool missing_ok);
 extern Oid	get_collation_oid(List *collname, bool missing_ok);
 extern Oid	get_conversion_oid(List *conname, bool missing_ok);
 extern Oid	FindDefaultConversionProc(int32 for_encoding, int32 to_encoding);
diff --git a/src/include/catalog/pg_constraint.h b/src/include/catalog/pg_constraint.h
index 8fca86d71e..060e436fcd 100644
--- a/src/include/catalog/pg_constraint.h
+++ b/src/include/catalog/pg_constraint.h
@@ -57,8 +57,7 @@ CATALOG(pg_constraint,2606)
 	 * contypid links to the pg_type row for a domain if this is a domain
 	 * constraint.  Otherwise it's 0.
 	 *
-	 * For SQL-style global ASSERTIONs, both conrelid and contypid would be
-	 * zero. This is not presently supported, however.
+	 * For SQL-style global ASSERTIONs, both conrelid and contypid are zero.
 	 */
 	Oid			contypid;		/* domain this constraint constrains */
 
diff --git a/src/include/catalog/pg_constraint_fn.h b/src/include/catalog/pg_constraint_fn.h
index 6bb1b09714..b5f06339fa 100644
--- a/src/include/catalog/pg_constraint_fn.h
+++ b/src/include/catalog/pg_constraint_fn.h
@@ -24,7 +24,7 @@ typedef enum ConstraintCategory
 {
 	CONSTRAINT_RELATION,
 	CONSTRAINT_DOMAIN,
-	CONSTRAINT_ASSERTION		/* for future expansion */
+	CONSTRAINT_ASSERTION
 } ConstraintCategory;
 
 extern Oid CreateConstraintEntry(const char *constraintName,
diff --git a/src/include/catalog/pg_proc.h b/src/include/catalog/pg_proc.h
index 298e0ae2f0..d3e761e4d7 100644
--- a/src/include/catalog/pg_proc.h
+++ b/src/include/catalog/pg_proc.h
@@ -2081,6 +2081,10 @@ DESCR("oid of replica identity index if any");
 DATA(insert OID = 1250 (  unique_key_recheck	PGNSP PGUID 12 1 0 0 0 f f f f t f v s 0 0 2279 "" _null_ _null_ _null_ _null_ _null_ unique_key_recheck _null_ _null_ _null_ ));
 DESCR("deferred UNIQUE constraint check");
 
+DATA(insert OID = 3556 (  assertion_check	    PGNSP PGUID 12 1 0 0 0 f f f f t f v s 0 0 2279 "" _null_ _null_ _null_ _null_ _null_ assertion_check _null_ _null_ _null_ ));
+DESCR("assertion check");
+
+
 /* Generic referential integrity constraint triggers */
 DATA(insert OID = 1644 (  RI_FKey_check_ins		PGNSP PGUID 12 1 0 0 0 f f f f t f v s 0 0 2279 "" _null_ _null_ _null_ _null_ _null_ RI_FKey_check_ins _null_ _null_ _null_ ));
 DESCR("referential integrity FOREIGN KEY ... REFERENCES");
diff --git a/src/include/commands/constraint.h b/src/include/commands/constraint.h
new file mode 100644
index 0000000000..98aa8f996e
--- /dev/null
+++ b/src/include/commands/constraint.h
@@ -0,0 +1,10 @@
+#ifndef CONSTRAINT_H
+#define CONSTRAINT_H
+
+#include "catalog/objectaddress.h"
+#include "nodes/parsenodes.h"
+
+extern ObjectAddress CreateAssertion(CreateAssertionStmt *stmt);
+extern ObjectAddress RenameAssertion(RenameStmt *stmt);
+
+#endif
\ No newline at end of file
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index 2eb3d6d371..b4258aa483 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -414,6 +414,7 @@ typedef enum NodeTag
 	T_DropSubscriptionStmt,
 	T_CreateStatsStmt,
 	T_AlterCollationStmt,
+	T_CreateAssertionStmt,
 	T_CallStmt,
 
 	/*
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index b72178efd1..7103013d0c 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -1618,6 +1618,7 @@ typedef enum ObjectType
 	OBJECT_AGGREGATE,
 	OBJECT_AMOP,
 	OBJECT_AMPROC,
+	OBJECT_ASSERTION,
 	OBJECT_ATTRIBUTE,			/* type's attribute, when distinct from column */
 	OBJECT_CAST,
 	OBJECT_COLUMN,
@@ -3271,6 +3272,16 @@ typedef struct ReindexStmt
 	int			options;		/* Reindex options flags */
 } ReindexStmt;
 
+/* ----------------------
+ *      CREATE ASSERTION Statement
+ */
+typedef struct CreateAssertionStmt
+{
+	NodeTag		type;
+	List    	*assertion_name;
+	Constraint	*constraint;
+} CreateAssertionStmt;
+
 /* ----------------------
  *		CREATE CONVERSION Statement
  * ----------------------
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
index 4e96fa7907..68770aa3e0 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -59,6 +59,7 @@ typedef enum ParseExprKind
 	EXPR_KIND_VALUES_SINGLE,	/* single-row VALUES (in INSERT only) */
 	EXPR_KIND_CHECK_CONSTRAINT, /* CHECK constraint for a table */
 	EXPR_KIND_DOMAIN_CHECK,		/* CHECK constraint for a domain */
+	EXPR_KIND_ASSERTION_CHECK,	/* CHECK constraint for an assertion */
 	EXPR_KIND_COLUMN_DEFAULT,	/* default value for a table column */
 	EXPR_KIND_FUNCTION_DEFAULT, /* default parameter value for function */
 	EXPR_KIND_INDEX_EXPRESSION, /* index expression */
diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h
index 67c7b2d4ac..82fe96075a 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -199,6 +199,7 @@ typedef enum AclObjectKind
 	ACL_KIND_OPCLASS,			/* pg_opclass */
 	ACL_KIND_OPFAMILY,			/* pg_opfamily */
 	ACL_KIND_COLLATION,			/* pg_collation */
+	ACL_KIND_CONSTRAINT,        /* pg_constraint */
 	ACL_KIND_CONVERSION,		/* pg_conversion */
 	ACL_KIND_STATISTICS,		/* pg_statistic_ext */
 	ACL_KIND_TABLESPACE,		/* pg_tablespace */
@@ -326,6 +327,7 @@ extern bool pg_opclass_ownercheck(Oid opc_oid, Oid roleid);
 extern bool pg_opfamily_ownercheck(Oid opf_oid, Oid roleid);
 extern bool pg_database_ownercheck(Oid db_oid, Oid roleid);
 extern bool pg_collation_ownercheck(Oid coll_oid, Oid roleid);
+extern bool pg_constraint_ownercheck(Oid constr_oid, Oid roleid);
 extern bool pg_conversion_ownercheck(Oid conv_oid, Oid roleid);
 extern bool pg_ts_dict_ownercheck(Oid dict_oid, Oid roleid);
 extern bool pg_ts_config_ownercheck(Oid cfg_oid, Oid roleid);
diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h
index 9731e6f7ae..9709759dfd 100644
--- a/src/include/utils/lsyscache.h
+++ b/src/include/utils/lsyscache.h
@@ -115,6 +115,7 @@ extern int	get_func_nargs(Oid funcid);
 extern Oid	get_func_signature(Oid funcid, Oid **argtypes, int *nargs);
 extern Oid	get_func_variadictype(Oid funcid);
 extern bool get_func_retset(Oid funcid);
+extern Oid  get_func_lang(Oid funcid);
 extern bool func_strict(Oid funcid);
 extern char func_volatile(Oid funcid);
 extern char func_parallel(Oid funcid);
diff --git a/src/test/regress/expected/assertions.out b/src/test/regress/expected/assertions.out
new file mode 100644
index 0000000000..7fc0d710a3
--- /dev/null
+++ b/src/test/regress/expected/assertions.out
@@ -0,0 +1,779 @@
+BEGIN TRANSACTION;
+--
+-- Create some helper views and functions to allow us to more easily
+-- assert which operations will cause an assertion to be checked.
+--
+CREATE OR REPLACE VIEW invalidating_operation
+  (assertion_name,
+   relation_name,
+   operation)
+AS
+SELECT c.conname,
+       cl.relname,
+       v.operation
+  FROM pg_trigger t
+  JOIN pg_constraint c ON (t.tgconstraint = c.oid)
+  JOIN pg_proc p ON (t.tgfoid = p.oid)
+  JOIN pg_class cl ON (t.tgrelid = cl.oid)
+  JOIN (VALUES ('INSERT', 1<<2),
+               ('DELETE', 1<<3|1<<5),
+               ('UPDATE', 1<<4))
+    AS v(operation, mask) ON ((v.mask & tgtype) > 0)
+ WHERE t.tgisinternal
+   AND p.proname = 'assertion_check';
+CREATE OR REPLACE VIEW invalidating_by_update_of_column
+  (assertion_name,
+   relation_name,
+   column_name)
+AS
+SELECT c.conname,
+       cl.relname,
+       a.attname
+  FROM pg_trigger t INNER JOIN pg_constraint c ON (t.tgconstraint = c.oid)
+                    INNER JOIN pg_proc p ON (t.tgfoid = p.oid)
+                    INNER JOIN pg_class cl ON (t.tgrelid = cl.oid)
+                    CROSS JOIN LATERAL UNNEST(t.tgattr) AS co(n)
+                    INNER JOIN pg_attribute a ON (a.attrelid = cl.oid AND attnum = co.n)
+ WHERE t.tgisinternal
+   AND p.proname = 'assertion_check'
+   AND (tgtype & (1 << 4)) > 0;
+CREATE OR REPLACE VIEW invalidating_summary
+  (assertion_name,
+   operations)
+AS
+SELECT assertion_name,
+       string_agg(operation, ' ' ORDER BY operation)
+  FROM (SELECT assertion_name,
+               operation ||'('|| string_agg(relation_name, ', ' ORDER BY relation_name) ||')'
+          FROM invalidating_operation
+         WHERE operation <> 'UPDATE'
+         GROUP BY assertion_name,
+                  operation
+         UNION
+        SELECT assertion_name,
+               operation ||'('|| string_agg(relation_name ||'.'|| column_name, ', ' ORDER BY relation_name, column_name) ||')'
+          FROM invalidating_by_update_of_column JOIN invalidating_operation USING (assertion_name, relation_name)
+         WHERE operation = 'UPDATE'
+         GROUP BY assertion_name,
+                  operation)
+    AS v(assertion_name, operation)
+ GROUP BY assertion_name;
+CREATE OR REPLACE FUNCTION have_identical_invalidating_operations
+  (a text, b text)
+RETURNS TABLE (predicate text, correct text)
+AS $$
+  SELECT 'assertion "'|| a ||'" has identical invalidating operations to assertion "'|| b ||'"',
+         CASE WHEN
+           NOT EXISTS (SELECT relation_name, operation
+                         FROM invalidating_operation
+                        WHERE assertion_name = a
+                       EXCEPT 
+                       SELECT relation_name, operation
+                         FROM invalidating_operation
+                        WHERE assertion_name = b) AND
+           NOT EXISTS (SELECT relation_name, column_name
+                         FROM invalidating_by_update_of_column
+                        WHERE assertion_name = a
+                       EXCEPT
+                       SELECT relation_name, column_name
+                         FROM invalidating_by_update_of_column
+                        WHERE assertion_name = b) THEN 'YES'
+                                                  ELSE 'NO' END
+$$ LANGUAGE SQL;
+COMMIT;
+BEGIN TRANSACTION;
+--
+-- Perform some basic sanity checking on creating assertions
+--
+CREATE TABLE test1 (a int, b text);
+CREATE ASSERTION foo CHECK (1 < 2);
+CREATE ASSERTION a2 CHECK ((SELECT count(*) FROM test1) < 5);
+DELETE FROM test1;
+INSERT INTO test1 VALUES (1, 'one');
+INSERT INTO test1 VALUES (2, 'two');
+INSERT INTO test1 VALUES (3, 'three');
+INSERT INTO test1 VALUES (4, 'four');
+SAVEPOINT pre_insert_too_many;
+INSERT INTO test1 VALUES (5, 'five');
+ERROR:  assertion "a2" violated
+ROLLBACK TO SAVEPOINT pre_insert_too_many;
+SELECT constraint_schema,
+       constraint_name
+  FROM information_schema.assertions
+ ORDER BY 1, 2;
+ constraint_schema | constraint_name 
+-------------------+-----------------
+ public            | a2
+ public            | foo
+(2 rows)
+
+\dQ
+                        List of assertions
+ Schema | Name |             Definition             | Description 
+--------+------+------------------------------------+-------------
+ public | a2   | CHECK ((( SELECT count(*) AS count+| 
+        |      |    FROM test1)) < 5)               | 
+ public | foo  | CHECK (1 < 2)                      | 
+(2 rows)
+
+ALTER ASSERTION a2 RENAME TO a3;
+SAVEPOINT pre_rename_foo;
+ALTER ASSERTION foo RENAME TO a3; -- fails
+ERROR:  assertion "a3" already exists
+ROLLBACK TO SAVEPOINT pre_rename_foo;
+DROP ASSERTION foo;
+SAVEPOINT pre_drop_test1;
+DROP TABLE test1; -- fails
+ERROR:  cannot drop table test1 because other objects depend on it
+DETAIL:  constraint a3 depends on table test1
+HINT:  Use DROP ... CASCADE to drop the dependent objects too.
+ROLLBACK TO SAVEPOINT pre_drop_test1;
+DROP TABLE test1 CASCADE;
+NOTICE:  drop cascades to constraint a3
+ROLLBACK;
+BEGIN TRANSACTION;
+--
+-- Expressions involving various operators, functions and casts
+-- 
+CREATE TABLE t (n INTEGER NOT NULL PRIMARY KEY, p BOOLEAN NOT NULL);
+INSERT INTO t (n, p) VALUES (20, TRUE);
+CREATE ASSERTION is_distinct_from CHECK (10 IS DISTINCT FROM (SELECT MIN(n) FROM t));
+CREATE ASSERTION is_not_distinct_from CHECK (20 IS NOT DISTINCT FROM (SELECT MIN(n) FROM t));
+CREATE ASSERTION null_if CHECK (NULLIF((SELECT BOOL_AND(p) FROM t), FALSE));
+CREATE ASSERTION coerce_to_boolean_from_min CHECK (CAST((SELECT MIN(n) FROM t) AS BOOLEAN));
+CREATE ASSERTION coerce_to_boolean_from_bool_and CHECK (CAST((SELECT BOOL_AND(p) FROM t) AS BOOLEAN));
+CREATE ASSERTION coerce_to_boolean_from_boolean CHECK (CAST((SELECT TRUE FROM t) AS BOOLEAN));
+ROLLBACK;
+BEGIN TRANSACTION;
+--
+-- Expressions involving function calls
+--
+CREATE TABLE t (n INTEGER NOT NULL PRIMARY KEY, m INTEGER NOT NULL, d DATE NOT NULL);
+CREATE FUNCTION f(n INTEGER) RETURNS INTEGER
+AS $$
+  SELECT n * 2
+$$ LANGUAGE SQL;
+CREATE FUNCTION r(n INTEGER) RETURNS TABLE (m INTEGER )
+AS $$
+  SELECT m
+    FROM t
+   WHERE n > r.n
+$$ LANGUAGE SQL;
+CREATE ASSERTION age_of_d_in_t CHECK (
+  NOT EXISTS (
+    SELECT FROM t
+     WHERE AGE(DATE '01-01-2018') > INTERVAL '10 days' -- AGE(DATE) is built-in SQL
+  )
+);
+CREATE ASSERTION use_f CHECK (f(0) = 0);
+CREATE ASSERTION use_f_in_predicate CHECK (NOT EXISTS (SELECT FROM t WHERE f(n) = 20));
+CREATE ASSERTION use_f_in_target CHECK (NOT EXISTS (SELECT f(n) FROM t));
+CREATE ASSERTION use_r_in_from CHECK (NOT EXISTS (SELECT FROM r(100)));
+CREATE ASSERTION use_r_in_target CHECK (NOT EXISTS (SELECT r(100)));
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (COALESCE(operations, 'NONE') = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('age_of_d_in_t',      'INSERT(t)'),
+       ('use_f',              'NONE'),
+       ('use_f_in_predicate', 'INSERT(t) UPDATE(t.n)'),
+       ('use_f_in_target',    'INSERT(t)'),
+       ('use_r_in_from',      'INSERT(t) UPDATE(t.m, t.n)'), -- TODO should _from and _target be the same?
+       ('use_r_in_target',    'INSERT(t) UPDATE(t.n)'))
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+   assertion_name   |           actual           |          expected          | correct 
+--------------------+----------------------------+----------------------------+---------
+ age_of_d_in_t      | INSERT(t)                  | INSERT(t)                  | YES
+ use_f              | NONE                       | NONE                       | YES
+ use_f_in_predicate | INSERT(t) UPDATE(t.n)      | INSERT(t) UPDATE(t.n)      | YES
+ use_f_in_target    | INSERT(t)                  | INSERT(t)                  | YES
+ use_r_in_from      | INSERT(t) UPDATE(t.m, t.n) | INSERT(t) UPDATE(t.m, t.n) | YES
+ use_r_in_target    | INSERT(t) UPDATE(t.n)      | INSERT(t) UPDATE(t.n)      | YES
+(6 rows)
+
+CREATE FUNCTION g(n INTEGER) RETURNS INTEGER
+AS $$
+BEGIN
+  RETURN n * 2;
+END;
+$$ LANGUAGE PLPGSQL;
+-- Use of functions other than internal or those implemented in SQL are illegal
+CREATE ASSERTION use_g_in_from CHECK (NOT EXISTS (SELECT FROM g(100))); -- fails
+ERROR:  function "g" uses unsupported language "plpgsql"
+ROLLBACK;
+BEGIN TRANSACTION;
+--
+-- Expressions involving EXISTS and NOT EXISTS
+--
+CREATE TABLE r (n INTEGER NOT NULL PRIMARY KEY);
+CREATE TABLE s (n INTEGER NOT NULL PRIMARY KEY);
+CREATE TABLE t (n INTEGER NOT NULL PRIMARY KEY, m INTEGER NOT NULL);
+CREATE TABLE t1 (n INTEGER NOT NULL PRIMARY KEY, m INTEGER NOT NULL);
+CREATE TABLE t2 (n INTEGER NOT NULL PRIMARY KEY, m INTEGER NOT NULL);
+INSERT INTO r (n) VALUES (1);
+INSERT INTO t (n, m) VALUES (0, 0);
+INSERT INTO t1 (n, m) VALUES (0, 0);
+INSERT INTO t2 (n, m) VALUES (0, 0);
+CREATE ASSERTION exists_no_predicate CHECK (
+  EXISTS (SELECT FROM r)
+);
+CREATE ASSERTION exists_with_predicate CHECK (
+  EXISTS (SELECT FROM r WHERE n > 0)
+);
+CREATE ASSERTION not_exists_no_predicate CHECK (
+  NOT EXISTS (SELECT FROM s)
+);
+CREATE ASSERTION not_exists_with_predicate CHECK (
+  NOT EXISTS (SELECT FROM r WHERE n < 1)
+);
+CREATE ASSERTION direct_subject_of_an_exists CHECK (
+  EXISTS (SELECT n FROM t WHERE m = 0)
+);
+-- TODO These can be optimised, at least if the set operation is UNION.
+CREATE ASSERTION except_subject_of_an_exists CHECK (
+  EXISTS (SELECT n FROM t1 WHERE m = 0 EXCEPT SELECT n FROM t2 WHERE m = 1)
+);
+CREATE ASSERTION intersect_subject_of_an_exists CHECK (
+  EXISTS (SELECT n FROM t1 WHERE m = 0 INTERSECT SELECT n FROM t2 WHERE m = 0)
+);
+CREATE ASSERTION union_subject_of_an_exists CHECK (
+  EXISTS (SELECT n FROM t1 WHERE m = 0 UNION SELECT n FROM t2 WHERE m = 0)
+);
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (operations = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('exists_no_predicate',            'DELETE(r)'),
+       ('exists_with_predicate',          'DELETE(r) UPDATE(r.n)'),
+       ('not_exists_no_predicate',        'INSERT(s)'),
+       ('not_exists_with_predicate',      'INSERT(r) UPDATE(r.n)'),
+       ('direct_subject_of_an_exists',    'DELETE(t) UPDATE(t.m)'),
+       ('except_subject_of_an_exists',    'DELETE(t1) INSERT(t2) UPDATE(t1.m, t1.n, t2.m, t2.n)'),
+       ('intersect_subject_of_an_exists', 'DELETE(t1, t2) UPDATE(t1.m, t1.n, t2.m, t2.n)'),
+       ('union_subject_of_an_exists',     'DELETE(t1, t2) UPDATE(t1.m, t1.n, t2.m, t2.n)'))
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+         assertion_name         |                        actual                        |                       expected                       | correct 
+--------------------------------+------------------------------------------------------+------------------------------------------------------+---------
+ direct_subject_of_an_exists    | DELETE(t) UPDATE(t.m)                                | DELETE(t) UPDATE(t.m)                                | YES
+ except_subject_of_an_exists    | DELETE(t1) INSERT(t2) UPDATE(t1.m, t1.n, t2.m, t2.n) | DELETE(t1) INSERT(t2) UPDATE(t1.m, t1.n, t2.m, t2.n) | YES
+ exists_no_predicate            | DELETE(r)                                            | DELETE(r)                                            | YES
+ exists_with_predicate          | DELETE(r) UPDATE(r.n)                                | DELETE(r) UPDATE(r.n)                                | YES
+ intersect_subject_of_an_exists | DELETE(t1, t2) UPDATE(t1.m, t1.n, t2.m, t2.n)        | DELETE(t1, t2) UPDATE(t1.m, t1.n, t2.m, t2.n)        | YES
+ not_exists_no_predicate        | INSERT(s)                                            | INSERT(s)                                            | YES
+ not_exists_with_predicate      | INSERT(r) UPDATE(r.n)                                | INSERT(r) UPDATE(r.n)                                | YES
+ union_subject_of_an_exists     | DELETE(t1, t2) UPDATE(t1.m, t1.n, t2.m, t2.n)        | DELETE(t1, t2) UPDATE(t1.m, t1.n, t2.m, t2.n)        | YES
+(8 rows)
+
+ROLLBACK;
+BEGIN TRANSACTION;
+--
+-- Expressions involving NOT
+--
+CREATE TABLE r (p BOOLEAN NOT NULL PRIMARY KEY);
+INSERT INTO r (p) VALUES (TRUE);
+CREATE ASSERTION a CHECK (EXISTS (SELECT FROM r WHERE p));
+CREATE ASSERTION b CHECK (NOT EXISTS (SELECT FROM r WHERE NOT p));
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (operations = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('a', 'DELETE(r) UPDATE(r.p)'),
+       ('b', 'INSERT(r) UPDATE(r.p)')) -- NOT(x) and x have opposite invalidating operations
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+ assertion_name |        actual         |       expected        | correct 
+----------------+-----------------------+-----------------------+---------
+ a              | DELETE(r) UPDATE(r.p) | DELETE(r) UPDATE(r.p) | YES
+ b              | INSERT(r) UPDATE(r.p) | INSERT(r) UPDATE(r.p) | YES
+(2 rows)
+
+ROLLBACK;
+BEGIN TRANSACTION;
+--
+-- Expressions involving set operations INTERSECT, UNION, and EXCEPT
+--
+CREATE TABLE r (n INTEGER NOT NULL PRIMARY KEY);
+CREATE TABLE s (n INTEGER NOT NULL PRIMARY KEY);
+CREATE TABLE t (n INTEGER NOT NULL PRIMARY KEY);
+INSERT INTO t (n) VALUES (0);
+CREATE ASSERTION except_operands CHECK (NOT EXISTS (SELECT FROM r EXCEPT SELECT FROM s));
+CREATE ASSERTION intersect_operands CHECK (NOT EXISTS (SELECT FROM r INTERSECT SELECT FROM s));
+CREATE ASSERTION union_operands CHECK (EXISTS (SELECT FROM s UNION SELECT FROM t));
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (operations = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('except_operands',    'DELETE(s) INSERT(r)'),
+       ('intersect_operands', 'INSERT(r, s)'),
+       ('union_operands',     'DELETE(s, t)'))
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+   assertion_name   |       actual        |      expected       | correct 
+--------------------+---------------------+---------------------+---------
+ except_operands    | DELETE(s) INSERT(r) | DELETE(s) INSERT(r) | YES
+ intersect_operands | INSERT(r, s)        | INSERT(r, s)        | YES
+ union_operands     | DELETE(s, t)        | DELETE(s, t)        | YES
+(3 rows)
+
+ROLLBACK;
+BEGIN TRANSACTION;
+--
+-- Expressions involving COUNT aggregations
+--
+CREATE TABLE s (n INTEGER NOT NULL PRIMARY KEY, m INTEGER NOT NULL);
+INSERT INTO s VALUES (1, 2);
+CREATE ASSERTION ge_all_count CHECK (1 >= ALL (SELECT COUNT(*) FROM s));
+CREATE ASSERTION ge_any_count CHECK (1 >= ANY (SELECT COUNT(*) FROM s));
+CREATE ASSERTION ge_count     CHECK (1 >=     (SELECT COUNT(*) FROM s));
+CREATE ASSERTION gt_all_count CHECK (2 >  ALL (SELECT COUNT(*) FROM s));
+CREATE ASSERTION gt_any_count CHECK (2 >  ANY (SELECT COUNT(*) FROM s));
+CREATE ASSERTION gt_count     CHECK (2 >      (SELECT COUNT(*) FROM s));
+CREATE ASSERTION le_all_count CHECK (1 <= ALL (SELECT COUNT(*) FROM s));
+CREATE ASSERTION le_any_count CHECK (0 <= ANY (SELECT COUNT(*) FROM s));
+CREATE ASSERTION le_count     CHECK (0 <=     (SELECT COUNT(*) FROM s));
+CREATE ASSERTION lt_all_count CHECK (0 <  ALL (SELECT COUNT(*) FROM s));
+CREATE ASSERTION lt_any_count CHECK (0 <  ANY (SELECT COUNT(*) FROM s));
+CREATE ASSERTION lt_count     CHECK (0 <      (SELECT COUNT(*) FROM s));
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (operations = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('ge_all_count', 'INSERT(s)'),
+       ('ge_any_count', 'INSERT(s)'),
+       ('ge_count',     'INSERT(s)'),
+       ('gt_all_count', 'INSERT(s)'),
+       ('gt_any_count', 'INSERT(s)'),
+       ('gt_count',     'INSERT(s)'),
+       ('le_all_count', 'DELETE(s)'),
+       ('le_any_count', 'DELETE(s)'),
+       ('le_count',     'DELETE(s)'),
+       ('lt_all_count', 'DELETE(s)'),
+       ('lt_any_count', 'DELETE(s)'),
+       ('lt_count',     'DELETE(s)'))
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+ assertion_name |  actual   | expected  | correct 
+----------------+-----------+-----------+---------
+ ge_all_count   | INSERT(s) | INSERT(s) | YES
+ ge_any_count   | INSERT(s) | INSERT(s) | YES
+ ge_count       | INSERT(s) | INSERT(s) | YES
+ gt_all_count   | INSERT(s) | INSERT(s) | YES
+ gt_any_count   | INSERT(s) | INSERT(s) | YES
+ gt_count       | INSERT(s) | INSERT(s) | YES
+ le_all_count   | DELETE(s) | DELETE(s) | YES
+ le_any_count   | DELETE(s) | DELETE(s) | YES
+ le_count       | DELETE(s) | DELETE(s) | YES
+ lt_all_count   | DELETE(s) | DELETE(s) | YES
+ lt_any_count   | DELETE(s) | DELETE(s) | YES
+ lt_count       | DELETE(s) | DELETE(s) | YES
+(12 rows)
+
+ROLLBACK;
+BEGIN TRANSACTION;
+--
+-- Expressions involving MIN aggregations
+--
+CREATE TABLE s (n INTEGER NOT NULL PRIMARY KEY, m INTEGER NOT NULL);
+INSERT INTO s VALUES (1, 2);
+CREATE ASSERTION ge_any_min CHECK (1 >= ANY (SELECT MIN(n) FROM s));
+CREATE ASSERTION ge_all_min CHECK (1 >= ALL (SELECT MIN(n) FROM s));
+CREATE ASSERTION ge_min     CHECK (1 >=     (SELECT MIN(n) FROM s));
+CREATE ASSERTION gt_any_min CHECK (2 >  ANY (SELECT MIN(n) FROM s));
+CREATE ASSERTION gt_all_min CHECK (2 >  ALL (SELECT MIN(n) FROM s));
+CREATE ASSERTION gt_min     CHECK (2 >      (SELECT MIN(n) FROM s));
+CREATE ASSERTION le_any_min CHECK (0 <= ANY (SELECT MIN(n) FROM s));
+CREATE ASSERTION le_all_min CHECK (0 <= ALL (SELECT MIN(n) FROM s));
+CREATE ASSERTION le_min     CHECK (0 <=     (SELECT MIN(n) FROM s));
+CREATE ASSERTION lt_any_min CHECK (0 <  ANY (SELECT MIN(n) FROM s));
+CREATE ASSERTION lt_all_min CHECK (0 <  ALL (SELECT MIN(n) FROM s));
+CREATE ASSERTION lt_min     CHECK (0 <      (SELECT MIN(n) FROM s));
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (operations = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('ge_all_min', 'DELETE(s) UPDATE(s.n)'),
+       ('ge_any_min', 'DELETE(s) UPDATE(s.n)'),
+       ('ge_min',     'DELETE(s) UPDATE(s.n)'),
+       ('gt_all_min', 'DELETE(s) UPDATE(s.n)'),
+       ('gt_any_min', 'DELETE(s) UPDATE(s.n)'),
+       ('gt_min',     'DELETE(s) UPDATE(s.n)'),
+       ('le_all_min', 'INSERT(s) UPDATE(s.n)'),
+       ('le_any_min', 'INSERT(s) UPDATE(s.n)'),
+       ('le_min',     'INSERT(s) UPDATE(s.n)'),
+       ('lt_all_min', 'INSERT(s) UPDATE(s.n)'),
+       ('lt_any_min', 'INSERT(s) UPDATE(s.n)'),
+       ('lt_min',     'INSERT(s) UPDATE(s.n)'))
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+ assertion_name |        actual         |       expected        | correct 
+----------------+-----------------------+-----------------------+---------
+ ge_all_min     | DELETE(s) UPDATE(s.n) | DELETE(s) UPDATE(s.n) | YES
+ ge_any_min     | DELETE(s) UPDATE(s.n) | DELETE(s) UPDATE(s.n) | YES
+ ge_min         | DELETE(s) UPDATE(s.n) | DELETE(s) UPDATE(s.n) | YES
+ gt_all_min     | DELETE(s) UPDATE(s.n) | DELETE(s) UPDATE(s.n) | YES
+ gt_any_min     | DELETE(s) UPDATE(s.n) | DELETE(s) UPDATE(s.n) | YES
+ gt_min         | DELETE(s) UPDATE(s.n) | DELETE(s) UPDATE(s.n) | YES
+ le_all_min     | INSERT(s) UPDATE(s.n) | INSERT(s) UPDATE(s.n) | YES
+ le_any_min     | INSERT(s) UPDATE(s.n) | INSERT(s) UPDATE(s.n) | YES
+ le_min         | INSERT(s) UPDATE(s.n) | INSERT(s) UPDATE(s.n) | YES
+ lt_all_min     | INSERT(s) UPDATE(s.n) | INSERT(s) UPDATE(s.n) | YES
+ lt_any_min     | INSERT(s) UPDATE(s.n) | INSERT(s) UPDATE(s.n) | YES
+ lt_min         | INSERT(s) UPDATE(s.n) | INSERT(s) UPDATE(s.n) | YES
+(12 rows)
+
+ROLLBACK;
+BEGIN TRANSACTION;
+--
+-- Expressions involving MAX aggregations
+--
+CREATE TABLE s (n INTEGER NOT NULL PRIMARY KEY, m INTEGER NOT NULL);
+INSERT INTO s VALUES (1, 2);
+CREATE ASSERTION ge_any_max CHECK (1 >= ANY (SELECT MAX(n) FROM s));
+CREATE ASSERTION ge_all_max CHECK (1 >= ALL (SELECT MAX(n) FROM s));
+CREATE ASSERTION ge_max     CHECK (1 >=     (SELECT MAX(n) FROM s));
+CREATE ASSERTION gt_all_max CHECK (2 >  ALL (SELECT MAX(n) FROM s));
+CREATE ASSERTION gt_any_max CHECK (2 >  ANY (SELECT MAX(n) FROM s));
+CREATE ASSERTION gt_max     CHECK (2 >      (SELECT MAX(n) FROM s));
+CREATE ASSERTION le_all_max CHECK (0 <= ALL (SELECT MAX(n) FROM s));
+CREATE ASSERTION le_any_max CHECK (0 <= ANY (SELECT MAX(n) FROM s));
+CREATE ASSERTION le_max     CHECK (0 <=     (SELECT MAX(n) FROM s));
+CREATE ASSERTION lt_all_max CHECK (0 <  ALL (SELECT MAX(n) FROM s));
+CREATE ASSERTION lt_any_max CHECK (0 <  ANY (SELECT MAX(n) FROM s));
+CREATE ASSERTION lt_max     CHECK (0 <      (SELECT MAX(n) FROM s));
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (operations = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('ge_all_max', 'INSERT(s) UPDATE(s.n)'),
+       ('ge_any_max', 'INSERT(s) UPDATE(s.n)'),
+       ('ge_max',     'INSERT(s) UPDATE(s.n)'),
+       ('gt_all_max', 'INSERT(s) UPDATE(s.n)'),
+       ('gt_any_max', 'INSERT(s) UPDATE(s.n)'),
+       ('gt_max',     'INSERT(s) UPDATE(s.n)'),
+       ('le_all_max', 'DELETE(s) UPDATE(s.n)'),
+       ('le_any_max', 'DELETE(s) UPDATE(s.n)'),
+       ('le_max',     'DELETE(s) UPDATE(s.n)'),
+       ('lt_all_max', 'DELETE(s) UPDATE(s.n)'),
+       ('lt_any_max', 'DELETE(s) UPDATE(s.n)'),
+       ('lt_max',     'DELETE(s) UPDATE(s.n)'))
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+ assertion_name |        actual         |       expected        | correct 
+----------------+-----------------------+-----------------------+---------
+ ge_all_max     | INSERT(s) UPDATE(s.n) | INSERT(s) UPDATE(s.n) | YES
+ ge_any_max     | INSERT(s) UPDATE(s.n) | INSERT(s) UPDATE(s.n) | YES
+ ge_max         | INSERT(s) UPDATE(s.n) | INSERT(s) UPDATE(s.n) | YES
+ gt_all_max     | INSERT(s) UPDATE(s.n) | INSERT(s) UPDATE(s.n) | YES
+ gt_any_max     | INSERT(s) UPDATE(s.n) | INSERT(s) UPDATE(s.n) | YES
+ gt_max         | INSERT(s) UPDATE(s.n) | INSERT(s) UPDATE(s.n) | YES
+ le_all_max     | DELETE(s) UPDATE(s.n) | DELETE(s) UPDATE(s.n) | YES
+ le_any_max     | DELETE(s) UPDATE(s.n) | DELETE(s) UPDATE(s.n) | YES
+ le_max         | DELETE(s) UPDATE(s.n) | DELETE(s) UPDATE(s.n) | YES
+ lt_all_max     | DELETE(s) UPDATE(s.n) | DELETE(s) UPDATE(s.n) | YES
+ lt_any_max     | DELETE(s) UPDATE(s.n) | DELETE(s) UPDATE(s.n) | YES
+ lt_max         | DELETE(s) UPDATE(s.n) | DELETE(s) UPDATE(s.n) | YES
+(12 rows)
+
+ROLLBACK;
+BEGIN TRANSACTION;
+--
+-- Expressions involving BOOL_AND aggregations
+--
+CREATE TABLE t (n INTEGER NOT NULL PRIMARY KEY, p BOOLEAN NOT NULL);
+INSERT INTO t (n, p) VALUES (0, TRUE);
+CREATE ASSERTION eq_bool_and     CHECK (TRUE  =      (SELECT BOOL_AND(p) FROM t));
+CREATE ASSERTION eq_any_bool_and CHECK (TRUE  =  ANY (SELECT BOOL_AND(p) FROM t));
+CREATE ASSERTION eq_all_bool_and CHECK (TRUE  =  ALL (SELECT BOOL_AND(p) FROM t));
+CREATE ASSERTION ne_bool_and     CHECK (FALSE <>     (SELECT BOOL_AND(p) FROM t));
+CREATE ASSERTION ne_any_bool_and CHECK (FALSE <> ANY (SELECT BOOL_AND(p) FROM t));
+CREATE ASSERTION ne_all_bool_and CHECK (FALSE <> ALL (SELECT BOOL_AND(p) FROM t));
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (operations = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('eq_all_bool_and', 'INSERT(t) UPDATE(t.p)'),
+       ('eq_any_bool_and', 'INSERT(t) UPDATE(t.p)'),
+       ('eq_bool_and',     'INSERT(t) UPDATE(t.p)'),
+       ('ne_all_bool_and', 'INSERT(t) UPDATE(t.p)'),
+       ('ne_any_bool_and', 'INSERT(t) UPDATE(t.p)'),
+       ('ne_bool_and',     'INSERT(t) UPDATE(t.p)'))
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+ assertion_name  |        actual         |       expected        | correct 
+-----------------+-----------------------+-----------------------+---------
+ eq_all_bool_and | INSERT(t) UPDATE(t.p) | INSERT(t) UPDATE(t.p) | YES
+ eq_any_bool_and | INSERT(t) UPDATE(t.p) | INSERT(t) UPDATE(t.p) | YES
+ eq_bool_and     | INSERT(t) UPDATE(t.p) | INSERT(t) UPDATE(t.p) | YES
+ ne_all_bool_and | INSERT(t) UPDATE(t.p) | INSERT(t) UPDATE(t.p) | YES
+ ne_any_bool_and | INSERT(t) UPDATE(t.p) | INSERT(t) UPDATE(t.p) | YES
+ ne_bool_and     | INSERT(t) UPDATE(t.p) | INSERT(t) UPDATE(t.p) | YES
+(6 rows)
+
+ROLLBACK;
+BEGIN TRANSACTION;
+--
+-- Expressions involving BOOL_OR aggregations
+--
+CREATE TABLE t (n INTEGER NOT NULL PRIMARY KEY, p BOOLEAN NOT NULL);
+INSERT INTO t (n, p) VALUES (0, TRUE);
+CREATE ASSERTION eq_bool_or     CHECK (TRUE  =      (SELECT BOOL_OR(p) FROM t));
+CREATE ASSERTION eq_any_bool_or CHECK (TRUE  =  ANY (SELECT BOOL_OR(p) FROM t));
+CREATE ASSERTION eq_all_bool_or CHECK (TRUE  =  ALL (SELECT BOOL_OR(p) FROM t));
+CREATE ASSERTION ne_bool_or     CHECK (FALSE <>     (SELECT BOOL_OR(p) FROM t));
+CREATE ASSERTION ne_any_bool_or CHECK (FALSE <> ANY (SELECT BOOL_OR(p) FROM t));
+CREATE ASSERTION ne_all_bool_or CHECK (FALSE <> ALL (SELECT BOOL_OR(p) FROM t));
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (operations = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('eq_all_bool_or', 'DELETE(t) UPDATE(t.p)'),
+       ('eq_any_bool_or', 'DELETE(t) UPDATE(t.p)'),
+       ('eq_bool_or',     'DELETE(t) UPDATE(t.p)'),
+       ('ne_all_bool_or', 'DELETE(t) UPDATE(t.p)'),
+       ('ne_any_bool_or', 'DELETE(t) UPDATE(t.p)'),
+       ('ne_bool_or',     'DELETE(t) UPDATE(t.p)'))
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+ assertion_name |        actual         |       expected        | correct 
+----------------+-----------------------+-----------------------+---------
+ eq_all_bool_or | DELETE(t) UPDATE(t.p) | DELETE(t) UPDATE(t.p) | YES
+ eq_any_bool_or | DELETE(t) UPDATE(t.p) | DELETE(t) UPDATE(t.p) | YES
+ eq_bool_or     | DELETE(t) UPDATE(t.p) | DELETE(t) UPDATE(t.p) | YES
+ ne_all_bool_or | DELETE(t) UPDATE(t.p) | DELETE(t) UPDATE(t.p) | YES
+ ne_any_bool_or | DELETE(t) UPDATE(t.p) | DELETE(t) UPDATE(t.p) | YES
+ ne_bool_or     | DELETE(t) UPDATE(t.p) | DELETE(t) UPDATE(t.p) | YES
+(6 rows)
+
+ROLLBACK;
+BEGIN TRANSACTION;
+--
+-- Expressions involving window functions
+--
+CREATE TABLE t (n INTEGER NOT NULL PRIMARY KEY, p BOOLEAN NOT NULL);
+-- Regular window function
+CREATE ASSERTION alternate_p CHECK (
+  NOT EXISTS (
+    SELECT FROM (
+      SELECT LAG(p, 1, NOT p) OVER n <> p
+         AND LEAD(p, 1, NOT p) OVER n <> p
+        FROM t
+      WINDOW n AS (ORDER BY n)
+    ) AS v(q)
+    WHERE NOT q
+  )
+);
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (operations = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('alternate_p', 'DELETE(t) INSERT(t) UPDATE(t.n, t.p)'))
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+ assertion_name |                actual                |               expected               | correct 
+----------------+--------------------------------------+--------------------------------------+---------
+ alternate_p    | DELETE(t) INSERT(t) UPDATE(t.n, t.p) | DELETE(t) INSERT(t) UPDATE(t.n, t.p) | YES
+(1 row)
+
+INSERT INTO t (n, p) VALUES (10, TRUE);
+INSERT INTO t (n, p) VALUES (20, FALSE);
+INSERT INTO t (n, p) VALUES (30, TRUE);
+SAVEPOINT pre_insert;
+INSERT INTO t (n, p) VALUES (40, TRUE);
+ERROR:  assertion "alternate_p" violated
+ROLLBACK TO SAVEPOINT pre_insert;
+SAVEPOINT pre_delete;
+DELETE FROM t WHERE n = 20;
+ERROR:  assertion "alternate_p" violated
+ROLLBACK TO SAVEPOINT pre_delete;
+SAVEPOINT pre_update;
+UPDATE t SET p = NOT p WHERE n = 20;
+ERROR:  assertion "alternate_p" violated
+ROLLBACK TO SAVEPOINT pre_update;
+DROP ASSERTION alternate_p;
+-- Aggregate function over a window
+CREATE TABLE s (n INTEGER NOT NULL PRIMARY KEY, m INTEGER NOT NULL);
+INSERT INTO s (n, m)
+SELECT n, (2 * n)
+  FROM GENERATE_SERIES(0, 9) AS ns(n);
+CREATE ASSERTION max_over_window CHECK ((10, 20) > ALL (SELECT n, MAX(m) OVER (ORDER BY n) FROM s));
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (operations = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('max_over_window', 'INSERT(s) UPDATE(s.m, s.n)'))
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+ assertion_name  |           actual           |          expected          | correct 
+-----------------+----------------------------+----------------------------+---------
+ max_over_window | INSERT(s) UPDATE(s.m, s.n) | INSERT(s) UPDATE(s.m, s.n) | YES
+(1 row)
+
+ROLLBACK;
+BEGIN TRANSACTION;
+--
+-- Expressions containing a comparison should have the same invalidating
+-- operations as an expression containing the inverse comparison operation
+-- and where the operands have been switched
+--
+CREATE TABLE r (n INTEGER NOT NULL PRIMARY KEY);
+INSERT INTO r (n) VALUES (9);
+-- ">" and "<" are opposites
+CREATE ASSERTION a CHECK (10 > (SELECT MIN(n) FROM r));
+CREATE ASSERTION b CHECK ((SELECT MIN(n) FROM r) < 10);
+-- "<" and ">" are opposites
+CREATE ASSERTION c CHECK (8 < (SELECT MIN(n) FROM r));
+CREATE ASSERTION d CHECK ((SELECT MIN(n) FROM r) > 8);
+-- ">=" and "<=" are opposites
+CREATE ASSERTION e CHECK (9 >= (SELECT MIN(n) FROM r));
+CREATE ASSERTION f CHECK ((SELECT MIN(n) FROM r) <= 9);
+-- "<=" and ">=" are opposites
+CREATE ASSERTION g CHECK (9 <= (SELECT MIN(n) FROM r));
+CREATE ASSERTION h CHECK ((SELECT MIN(n) FROM r) >= 9);
+SELECT (test).predicate, (test).correct
+  FROM (
+VALUES ('a', 'b'),
+       ('c', 'd'),
+       ('e', 'f'),
+       ('g', 'h'))
+    AS v(a, b)
+ CROSS JOIN LATERAL (SELECT have_identical_invalidating_operations(v.a, v.b))
+    AS w(test)
+ ORDER BY 1, 2;
+                              predicate                               | correct 
+----------------------------------------------------------------------+---------
+ assertion "a" has identical invalidating operations to assertion "b" | YES
+ assertion "c" has identical invalidating operations to assertion "d" | YES
+ assertion "e" has identical invalidating operations to assertion "f" | YES
+ assertion "g" has identical invalidating operations to assertion "h" | YES
+(4 rows)
+
+ROLLBACK;
+BEGIN TRANSACTION;
+--
+-- The INFORMATION_SCHEMA should contain the correct information
+--
+CREATE TABLE t (n INTEGER NOT NULL PRIMARY KEY, p BOOLEAN NOT NULL);
+CREATE ASSERTION a CHECK (NOT EXISTS (SELECT FROM t WHERE p));
+SELECT predicate,
+       CASE WHEN
+         truth THEN 'YES'
+               ELSE 'NO'
+       END AS correct
+  FROM ( 
+VALUES ('INFORMATION_SCHEMA.CONSTRAINT_TABLE_USAGE contains only the correct tables',
+        EXISTS (
+          SELECT FROM information_schema.constraint_table_usage
+           WHERE (constraint_name, table_name) = ('a', 't'))
+        AND NOT EXISTS (
+          SELECT FROM information_schema.constraint_table_usage
+           WHERE constraint_name = 'a'
+             AND table_name <> 't')),
+       ('INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE contains only the correct columns (t.p)',
+        EXISTS (
+          SELECT FROM information_schema.constraint_column_usage
+           WHERE (constraint_name, table_name, column_name) = ('a', 't', 'p'))
+        AND NOT EXISTS (
+          SELECT FROM information_schema.constraint_column_usage
+           WHERE constraint_name ='a'
+             AND (table_name, column_name) <> ('t', 'p'))),
+       ('INFORMATION_SCHEMA.ASSERTIONS contains only the correct assertions',
+        EXISTS (
+          SELECT FROM information_schema.assertions
+           WHERE constraint_name = 'a')
+        AND NOT EXISTS (
+          SELECT FROM information_schema.assertions
+           WHERE constraint_name <> 'a')))
+    AS v(predicate, truth);
+                                     predicate                                      | correct 
+------------------------------------------------------------------------------------+---------
+ INFORMATION_SCHEMA.CONSTRAINT_TABLE_USAGE contains only the correct tables         | YES
+ INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE contains only the correct columns (t.p) | YES
+ INFORMATION_SCHEMA.ASSERTIONS contains only the correct assertions                 | YES
+(3 rows)
+
+ROLLBACK;
+BEGIN TRANSACTION;
+-- TODO This needs rethinking
+/*
+CREATE TABLE s (n INTEGER NOT NULL PRIMARY KEY, m INTEGER NOT NULL);
+CREATE TABLE t (n INTEGER NOT NULL PRIMARY KEY, p BOOLEAN NOT NULL);
+CREATE VIEW v AS SELECT * FROM s INNER JOIN t USING (n);
+
+CREATE ASSERTION a CHECK (NOT EXISTS (SELECT FROM v));
+
+-- we should depend on the view not the tables
+SELECT OK(depends_on('a', 'v'),
+          'Assertion depends upon the view v it references');
+SELECT OK(NOT depends_on('a', 's'),
+          'Assertion does not depend upon the table s referenced in the view v');
+SELECT OK(NOT depends_on('a', 't'),
+          'Assertion does not depend upon the table t referenced in the view v');
+
+-- we should trigger on the tables and not the view
+SELECT OK(EXISTS(SELECT FROM assertion_check_operation
+                  WHERE (assertion_name, relation_name) = ('a', 's')),
+          'Assertion is checked on modifications to table s');
+SELECT OK(EXISTS(SELECT FROM assertion_check_operation
+                  WHERE (assertion_name, relation_name) = ('a', 't')),
+          'Assertion is checked on modifications to table t');
+SELECT OK(NOT EXISTS(SELECT FROM assertion_check_operation
+                      WHERE (assertion_name, relation_name) = ('a', 'v')),
+          'Assertion is not checked on modifications to view v');
+*/
+ROLLBACK;
+-- TODO test commonalities between count, min, max, etc, for optimisations
+-- TODO ensure window function aggregates are treated the same as regular aggregates
+-- TODO ensure that conflicting aggregate functions are not incorrectly optimised
+-- TODO ensure that recorded dependencies are correct (traversal into functions and views)
+BEGIN TRANSACTION;
+DROP FUNCTION have_identical_invalidating_operations;
+DROP VIEW invalidating_summary;
+DROP VIEW invalidating_by_update_of_column;
+DROP VIEW invalidating_operation;
+COMMIT;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index e224977791..64a3d783b2 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -60,7 +60,7 @@ test: create_index create_view
 # ----------
 # Another group of parallel tests
 # ----------
-test: create_aggregate create_function_3 create_cast constraints triggers inherit create_table_like typed_table vacuum drop_if_exists updatable_views rolenames roleattributes create_am hash_func
+test: create_aggregate create_function_3 create_cast constraints triggers inherit create_table_like typed_table vacuum drop_if_exists updatable_views rolenames roleattributes create_am assertions hash_func
 
 # ----------
 # sanity_check does a vacuum, affecting the sort order of SELECT *
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 9fc5f1a268..9aeb21a3ac 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -58,6 +58,7 @@ test: create_function_1
 test: create_type
 test: create_table
 test: create_function_2
+test: assertions
 test: copy
 test: copyselect
 test: copydml
diff --git a/src/test/regress/sql/assertions.sql b/src/test/regress/sql/assertions.sql
new file mode 100644
index 0000000000..f270698da1
--- /dev/null
+++ b/src/test/regress/sql/assertions.sql
@@ -0,0 +1,775 @@
+
+BEGIN TRANSACTION;
+
+--
+-- Create some helper views and functions to allow us to more easily
+-- assert which operations will cause an assertion to be checked.
+--
+
+CREATE OR REPLACE VIEW invalidating_operation
+  (assertion_name,
+   relation_name,
+   operation)
+AS
+SELECT c.conname,
+       cl.relname,
+       v.operation
+  FROM pg_trigger t
+  JOIN pg_constraint c ON (t.tgconstraint = c.oid)
+  JOIN pg_proc p ON (t.tgfoid = p.oid)
+  JOIN pg_class cl ON (t.tgrelid = cl.oid)
+  JOIN (VALUES ('INSERT', 1<<2),
+               ('DELETE', 1<<3|1<<5),
+               ('UPDATE', 1<<4))
+    AS v(operation, mask) ON ((v.mask & tgtype) > 0)
+ WHERE t.tgisinternal
+   AND p.proname = 'assertion_check';
+
+CREATE OR REPLACE VIEW invalidating_by_update_of_column
+  (assertion_name,
+   relation_name,
+   column_name)
+AS
+SELECT c.conname,
+       cl.relname,
+       a.attname
+  FROM pg_trigger t INNER JOIN pg_constraint c ON (t.tgconstraint = c.oid)
+                    INNER JOIN pg_proc p ON (t.tgfoid = p.oid)
+                    INNER JOIN pg_class cl ON (t.tgrelid = cl.oid)
+                    CROSS JOIN LATERAL UNNEST(t.tgattr) AS co(n)
+                    INNER JOIN pg_attribute a ON (a.attrelid = cl.oid AND attnum = co.n)
+ WHERE t.tgisinternal
+   AND p.proname = 'assertion_check'
+   AND (tgtype & (1 << 4)) > 0;
+
+CREATE OR REPLACE VIEW invalidating_summary
+  (assertion_name,
+   operations)
+AS
+SELECT assertion_name,
+       string_agg(operation, ' ' ORDER BY operation)
+  FROM (SELECT assertion_name,
+               operation ||'('|| string_agg(relation_name, ', ' ORDER BY relation_name) ||')'
+          FROM invalidating_operation
+         WHERE operation <> 'UPDATE'
+         GROUP BY assertion_name,
+                  operation
+         UNION
+        SELECT assertion_name,
+               operation ||'('|| string_agg(relation_name ||'.'|| column_name, ', ' ORDER BY relation_name, column_name) ||')'
+          FROM invalidating_by_update_of_column JOIN invalidating_operation USING (assertion_name, relation_name)
+         WHERE operation = 'UPDATE'
+         GROUP BY assertion_name,
+                  operation)
+    AS v(assertion_name, operation)
+ GROUP BY assertion_name;
+
+CREATE OR REPLACE FUNCTION have_identical_invalidating_operations
+  (a text, b text)
+RETURNS TABLE (predicate text, correct text)
+AS $$
+  SELECT 'assertion "'|| a ||'" has identical invalidating operations to assertion "'|| b ||'"',
+         CASE WHEN
+           NOT EXISTS (SELECT relation_name, operation
+                         FROM invalidating_operation
+                        WHERE assertion_name = a
+                       EXCEPT 
+                       SELECT relation_name, operation
+                         FROM invalidating_operation
+                        WHERE assertion_name = b) AND
+           NOT EXISTS (SELECT relation_name, column_name
+                         FROM invalidating_by_update_of_column
+                        WHERE assertion_name = a
+                       EXCEPT
+                       SELECT relation_name, column_name
+                         FROM invalidating_by_update_of_column
+                        WHERE assertion_name = b) THEN 'YES'
+                                                  ELSE 'NO' END
+$$ LANGUAGE SQL;
+
+COMMIT;
+
+
+BEGIN TRANSACTION;
+
+--
+-- Perform some basic sanity checking on creating assertions
+--
+
+CREATE TABLE test1 (a int, b text);
+
+CREATE ASSERTION foo CHECK (1 < 2);
+CREATE ASSERTION a2 CHECK ((SELECT count(*) FROM test1) < 5);
+
+DELETE FROM test1;
+INSERT INTO test1 VALUES (1, 'one');
+INSERT INTO test1 VALUES (2, 'two');
+INSERT INTO test1 VALUES (3, 'three');
+INSERT INTO test1 VALUES (4, 'four');
+
+SAVEPOINT pre_insert_too_many;
+INSERT INTO test1 VALUES (5, 'five');
+ROLLBACK TO SAVEPOINT pre_insert_too_many;
+
+SELECT constraint_schema,
+       constraint_name
+  FROM information_schema.assertions
+ ORDER BY 1, 2;
+
+\dQ
+
+ALTER ASSERTION a2 RENAME TO a3;
+
+SAVEPOINT pre_rename_foo;
+ALTER ASSERTION foo RENAME TO a3; -- fails
+ROLLBACK TO SAVEPOINT pre_rename_foo;
+
+DROP ASSERTION foo;
+
+SAVEPOINT pre_drop_test1;
+DROP TABLE test1; -- fails
+ROLLBACK TO SAVEPOINT pre_drop_test1;
+
+DROP TABLE test1 CASCADE;
+
+ROLLBACK;
+
+
+
+BEGIN TRANSACTION;
+
+--
+-- Expressions involving various operators, functions and casts
+-- 
+
+CREATE TABLE t (n INTEGER NOT NULL PRIMARY KEY, p BOOLEAN NOT NULL);
+
+INSERT INTO t (n, p) VALUES (20, TRUE);
+
+CREATE ASSERTION is_distinct_from CHECK (10 IS DISTINCT FROM (SELECT MIN(n) FROM t));
+CREATE ASSERTION is_not_distinct_from CHECK (20 IS NOT DISTINCT FROM (SELECT MIN(n) FROM t));
+CREATE ASSERTION null_if CHECK (NULLIF((SELECT BOOL_AND(p) FROM t), FALSE));
+CREATE ASSERTION coerce_to_boolean_from_min CHECK (CAST((SELECT MIN(n) FROM t) AS BOOLEAN));
+CREATE ASSERTION coerce_to_boolean_from_bool_and CHECK (CAST((SELECT BOOL_AND(p) FROM t) AS BOOLEAN));
+CREATE ASSERTION coerce_to_boolean_from_boolean CHECK (CAST((SELECT TRUE FROM t) AS BOOLEAN));
+
+ROLLBACK;
+
+
+BEGIN TRANSACTION;
+
+--
+-- Expressions involving function calls
+--
+
+CREATE TABLE t (n INTEGER NOT NULL PRIMARY KEY, m INTEGER NOT NULL, d DATE NOT NULL);
+
+CREATE FUNCTION f(n INTEGER) RETURNS INTEGER
+AS $$
+  SELECT n * 2
+$$ LANGUAGE SQL;
+
+CREATE FUNCTION r(n INTEGER) RETURNS TABLE (m INTEGER )
+AS $$
+  SELECT m
+    FROM t
+   WHERE n > r.n
+$$ LANGUAGE SQL;
+
+CREATE ASSERTION age_of_d_in_t CHECK (
+  NOT EXISTS (
+    SELECT FROM t
+     WHERE AGE(DATE '01-01-2018') > INTERVAL '10 days' -- AGE(DATE) is built-in SQL
+  )
+);
+CREATE ASSERTION use_f CHECK (f(0) = 0);
+CREATE ASSERTION use_f_in_predicate CHECK (NOT EXISTS (SELECT FROM t WHERE f(n) = 20));
+CREATE ASSERTION use_f_in_target CHECK (NOT EXISTS (SELECT f(n) FROM t));
+CREATE ASSERTION use_r_in_from CHECK (NOT EXISTS (SELECT FROM r(100)));
+CREATE ASSERTION use_r_in_target CHECK (NOT EXISTS (SELECT r(100)));
+
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (COALESCE(operations, 'NONE') = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('age_of_d_in_t',      'INSERT(t)'),
+       ('use_f',              'NONE'),
+       ('use_f_in_predicate', 'INSERT(t) UPDATE(t.n)'),
+       ('use_f_in_target',    'INSERT(t)'),
+       ('use_r_in_from',      'INSERT(t) UPDATE(t.m, t.n)'), -- TODO should _from and _target be the same?
+       ('use_r_in_target',    'INSERT(t) UPDATE(t.n)'))
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+
+CREATE FUNCTION g(n INTEGER) RETURNS INTEGER
+AS $$
+BEGIN
+  RETURN n * 2;
+END;
+$$ LANGUAGE PLPGSQL;
+
+-- Use of functions other than internal or those implemented in SQL are illegal
+CREATE ASSERTION use_g_in_from CHECK (NOT EXISTS (SELECT FROM g(100))); -- fails
+
+ROLLBACK;
+
+
+BEGIN TRANSACTION;
+
+--
+-- Expressions involving EXISTS and NOT EXISTS
+--
+
+CREATE TABLE r (n INTEGER NOT NULL PRIMARY KEY);
+CREATE TABLE s (n INTEGER NOT NULL PRIMARY KEY);
+CREATE TABLE t (n INTEGER NOT NULL PRIMARY KEY, m INTEGER NOT NULL);
+CREATE TABLE t1 (n INTEGER NOT NULL PRIMARY KEY, m INTEGER NOT NULL);
+CREATE TABLE t2 (n INTEGER NOT NULL PRIMARY KEY, m INTEGER NOT NULL);
+
+INSERT INTO r (n) VALUES (1);
+INSERT INTO t (n, m) VALUES (0, 0);
+INSERT INTO t1 (n, m) VALUES (0, 0);
+INSERT INTO t2 (n, m) VALUES (0, 0);
+
+CREATE ASSERTION exists_no_predicate CHECK (
+  EXISTS (SELECT FROM r)
+);
+
+CREATE ASSERTION exists_with_predicate CHECK (
+  EXISTS (SELECT FROM r WHERE n > 0)
+);
+
+CREATE ASSERTION not_exists_no_predicate CHECK (
+  NOT EXISTS (SELECT FROM s)
+);
+
+CREATE ASSERTION not_exists_with_predicate CHECK (
+  NOT EXISTS (SELECT FROM r WHERE n < 1)
+);
+
+CREATE ASSERTION direct_subject_of_an_exists CHECK (
+  EXISTS (SELECT n FROM t WHERE m = 0)
+);
+
+-- TODO These can be optimised, at least if the set operation is UNION.
+CREATE ASSERTION except_subject_of_an_exists CHECK (
+  EXISTS (SELECT n FROM t1 WHERE m = 0 EXCEPT SELECT n FROM t2 WHERE m = 1)
+);
+
+CREATE ASSERTION intersect_subject_of_an_exists CHECK (
+  EXISTS (SELECT n FROM t1 WHERE m = 0 INTERSECT SELECT n FROM t2 WHERE m = 0)
+);
+
+CREATE ASSERTION union_subject_of_an_exists CHECK (
+  EXISTS (SELECT n FROM t1 WHERE m = 0 UNION SELECT n FROM t2 WHERE m = 0)
+);
+
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (operations = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('exists_no_predicate',            'DELETE(r)'),
+       ('exists_with_predicate',          'DELETE(r) UPDATE(r.n)'),
+       ('not_exists_no_predicate',        'INSERT(s)'),
+       ('not_exists_with_predicate',      'INSERT(r) UPDATE(r.n)'),
+       ('direct_subject_of_an_exists',    'DELETE(t) UPDATE(t.m)'),
+       ('except_subject_of_an_exists',    'DELETE(t1) INSERT(t2) UPDATE(t1.m, t1.n, t2.m, t2.n)'),
+       ('intersect_subject_of_an_exists', 'DELETE(t1, t2) UPDATE(t1.m, t1.n, t2.m, t2.n)'),
+       ('union_subject_of_an_exists',     'DELETE(t1, t2) UPDATE(t1.m, t1.n, t2.m, t2.n)'))
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+
+ROLLBACK;
+
+
+BEGIN TRANSACTION;
+
+--
+-- Expressions involving NOT
+--
+
+CREATE TABLE r (p BOOLEAN NOT NULL PRIMARY KEY);
+
+INSERT INTO r (p) VALUES (TRUE);
+
+CREATE ASSERTION a CHECK (EXISTS (SELECT FROM r WHERE p));
+CREATE ASSERTION b CHECK (NOT EXISTS (SELECT FROM r WHERE NOT p));
+
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (operations = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('a', 'DELETE(r) UPDATE(r.p)'),
+       ('b', 'INSERT(r) UPDATE(r.p)')) -- NOT(x) and x have opposite invalidating operations
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+
+ROLLBACK;
+
+
+BEGIN TRANSACTION;
+
+--
+-- Expressions involving set operations INTERSECT, UNION, and EXCEPT
+--
+
+CREATE TABLE r (n INTEGER NOT NULL PRIMARY KEY);
+CREATE TABLE s (n INTEGER NOT NULL PRIMARY KEY);
+CREATE TABLE t (n INTEGER NOT NULL PRIMARY KEY);
+
+INSERT INTO t (n) VALUES (0);
+
+CREATE ASSERTION except_operands CHECK (NOT EXISTS (SELECT FROM r EXCEPT SELECT FROM s));
+CREATE ASSERTION intersect_operands CHECK (NOT EXISTS (SELECT FROM r INTERSECT SELECT FROM s));
+CREATE ASSERTION union_operands CHECK (EXISTS (SELECT FROM s UNION SELECT FROM t));
+
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (operations = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('except_operands',    'DELETE(s) INSERT(r)'),
+       ('intersect_operands', 'INSERT(r, s)'),
+       ('union_operands',     'DELETE(s, t)'))
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+
+ROLLBACK;
+
+
+BEGIN TRANSACTION;
+
+--
+-- Expressions involving COUNT aggregations
+--
+
+CREATE TABLE s (n INTEGER NOT NULL PRIMARY KEY, m INTEGER NOT NULL);
+
+INSERT INTO s VALUES (1, 2);
+
+CREATE ASSERTION ge_all_count CHECK (1 >= ALL (SELECT COUNT(*) FROM s));
+CREATE ASSERTION ge_any_count CHECK (1 >= ANY (SELECT COUNT(*) FROM s));
+CREATE ASSERTION ge_count     CHECK (1 >=     (SELECT COUNT(*) FROM s));
+CREATE ASSERTION gt_all_count CHECK (2 >  ALL (SELECT COUNT(*) FROM s));
+CREATE ASSERTION gt_any_count CHECK (2 >  ANY (SELECT COUNT(*) FROM s));
+CREATE ASSERTION gt_count     CHECK (2 >      (SELECT COUNT(*) FROM s));
+CREATE ASSERTION le_all_count CHECK (1 <= ALL (SELECT COUNT(*) FROM s));
+CREATE ASSERTION le_any_count CHECK (0 <= ANY (SELECT COUNT(*) FROM s));
+CREATE ASSERTION le_count     CHECK (0 <=     (SELECT COUNT(*) FROM s));
+CREATE ASSERTION lt_all_count CHECK (0 <  ALL (SELECT COUNT(*) FROM s));
+CREATE ASSERTION lt_any_count CHECK (0 <  ANY (SELECT COUNT(*) FROM s));
+CREATE ASSERTION lt_count     CHECK (0 <      (SELECT COUNT(*) FROM s));
+
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (operations = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('ge_all_count', 'INSERT(s)'),
+       ('ge_any_count', 'INSERT(s)'),
+       ('ge_count',     'INSERT(s)'),
+       ('gt_all_count', 'INSERT(s)'),
+       ('gt_any_count', 'INSERT(s)'),
+       ('gt_count',     'INSERT(s)'),
+       ('le_all_count', 'DELETE(s)'),
+       ('le_any_count', 'DELETE(s)'),
+       ('le_count',     'DELETE(s)'),
+       ('lt_all_count', 'DELETE(s)'),
+       ('lt_any_count', 'DELETE(s)'),
+       ('lt_count',     'DELETE(s)'))
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+
+ROLLBACK;
+
+
+BEGIN TRANSACTION;
+
+--
+-- Expressions involving MIN aggregations
+--
+
+CREATE TABLE s (n INTEGER NOT NULL PRIMARY KEY, m INTEGER NOT NULL);
+
+INSERT INTO s VALUES (1, 2);
+
+CREATE ASSERTION ge_any_min CHECK (1 >= ANY (SELECT MIN(n) FROM s));
+CREATE ASSERTION ge_all_min CHECK (1 >= ALL (SELECT MIN(n) FROM s));
+CREATE ASSERTION ge_min     CHECK (1 >=     (SELECT MIN(n) FROM s));
+CREATE ASSERTION gt_any_min CHECK (2 >  ANY (SELECT MIN(n) FROM s));
+CREATE ASSERTION gt_all_min CHECK (2 >  ALL (SELECT MIN(n) FROM s));
+CREATE ASSERTION gt_min     CHECK (2 >      (SELECT MIN(n) FROM s));
+CREATE ASSERTION le_any_min CHECK (0 <= ANY (SELECT MIN(n) FROM s));
+CREATE ASSERTION le_all_min CHECK (0 <= ALL (SELECT MIN(n) FROM s));
+CREATE ASSERTION le_min     CHECK (0 <=     (SELECT MIN(n) FROM s));
+CREATE ASSERTION lt_any_min CHECK (0 <  ANY (SELECT MIN(n) FROM s));
+CREATE ASSERTION lt_all_min CHECK (0 <  ALL (SELECT MIN(n) FROM s));
+CREATE ASSERTION lt_min     CHECK (0 <      (SELECT MIN(n) FROM s));
+
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (operations = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('ge_all_min', 'DELETE(s) UPDATE(s.n)'),
+       ('ge_any_min', 'DELETE(s) UPDATE(s.n)'),
+       ('ge_min',     'DELETE(s) UPDATE(s.n)'),
+       ('gt_all_min', 'DELETE(s) UPDATE(s.n)'),
+       ('gt_any_min', 'DELETE(s) UPDATE(s.n)'),
+       ('gt_min',     'DELETE(s) UPDATE(s.n)'),
+       ('le_all_min', 'INSERT(s) UPDATE(s.n)'),
+       ('le_any_min', 'INSERT(s) UPDATE(s.n)'),
+       ('le_min',     'INSERT(s) UPDATE(s.n)'),
+       ('lt_all_min', 'INSERT(s) UPDATE(s.n)'),
+       ('lt_any_min', 'INSERT(s) UPDATE(s.n)'),
+       ('lt_min',     'INSERT(s) UPDATE(s.n)'))
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+
+ROLLBACK;
+
+
+BEGIN TRANSACTION;
+
+--
+-- Expressions involving MAX aggregations
+--
+
+CREATE TABLE s (n INTEGER NOT NULL PRIMARY KEY, m INTEGER NOT NULL);
+
+INSERT INTO s VALUES (1, 2);
+
+CREATE ASSERTION ge_any_max CHECK (1 >= ANY (SELECT MAX(n) FROM s));
+CREATE ASSERTION ge_all_max CHECK (1 >= ALL (SELECT MAX(n) FROM s));
+CREATE ASSERTION ge_max     CHECK (1 >=     (SELECT MAX(n) FROM s));
+CREATE ASSERTION gt_all_max CHECK (2 >  ALL (SELECT MAX(n) FROM s));
+CREATE ASSERTION gt_any_max CHECK (2 >  ANY (SELECT MAX(n) FROM s));
+CREATE ASSERTION gt_max     CHECK (2 >      (SELECT MAX(n) FROM s));
+CREATE ASSERTION le_all_max CHECK (0 <= ALL (SELECT MAX(n) FROM s));
+CREATE ASSERTION le_any_max CHECK (0 <= ANY (SELECT MAX(n) FROM s));
+CREATE ASSERTION le_max     CHECK (0 <=     (SELECT MAX(n) FROM s));
+CREATE ASSERTION lt_all_max CHECK (0 <  ALL (SELECT MAX(n) FROM s));
+CREATE ASSERTION lt_any_max CHECK (0 <  ANY (SELECT MAX(n) FROM s));
+CREATE ASSERTION lt_max     CHECK (0 <      (SELECT MAX(n) FROM s));
+
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (operations = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('ge_all_max', 'INSERT(s) UPDATE(s.n)'),
+       ('ge_any_max', 'INSERT(s) UPDATE(s.n)'),
+       ('ge_max',     'INSERT(s) UPDATE(s.n)'),
+       ('gt_all_max', 'INSERT(s) UPDATE(s.n)'),
+       ('gt_any_max', 'INSERT(s) UPDATE(s.n)'),
+       ('gt_max',     'INSERT(s) UPDATE(s.n)'),
+       ('le_all_max', 'DELETE(s) UPDATE(s.n)'),
+       ('le_any_max', 'DELETE(s) UPDATE(s.n)'),
+       ('le_max',     'DELETE(s) UPDATE(s.n)'),
+       ('lt_all_max', 'DELETE(s) UPDATE(s.n)'),
+       ('lt_any_max', 'DELETE(s) UPDATE(s.n)'),
+       ('lt_max',     'DELETE(s) UPDATE(s.n)'))
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+
+ROLLBACK;
+
+
+BEGIN TRANSACTION;
+
+--
+-- Expressions involving BOOL_AND aggregations
+--
+
+CREATE TABLE t (n INTEGER NOT NULL PRIMARY KEY, p BOOLEAN NOT NULL);
+INSERT INTO t (n, p) VALUES (0, TRUE);
+
+CREATE ASSERTION eq_bool_and     CHECK (TRUE  =      (SELECT BOOL_AND(p) FROM t));
+CREATE ASSERTION eq_any_bool_and CHECK (TRUE  =  ANY (SELECT BOOL_AND(p) FROM t));
+CREATE ASSERTION eq_all_bool_and CHECK (TRUE  =  ALL (SELECT BOOL_AND(p) FROM t));
+CREATE ASSERTION ne_bool_and     CHECK (FALSE <>     (SELECT BOOL_AND(p) FROM t));
+CREATE ASSERTION ne_any_bool_and CHECK (FALSE <> ANY (SELECT BOOL_AND(p) FROM t));
+CREATE ASSERTION ne_all_bool_and CHECK (FALSE <> ALL (SELECT BOOL_AND(p) FROM t));
+
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (operations = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('eq_all_bool_and', 'INSERT(t) UPDATE(t.p)'),
+       ('eq_any_bool_and', 'INSERT(t) UPDATE(t.p)'),
+       ('eq_bool_and',     'INSERT(t) UPDATE(t.p)'),
+       ('ne_all_bool_and', 'INSERT(t) UPDATE(t.p)'),
+       ('ne_any_bool_and', 'INSERT(t) UPDATE(t.p)'),
+       ('ne_bool_and',     'INSERT(t) UPDATE(t.p)'))
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+
+ROLLBACK;
+
+
+BEGIN TRANSACTION;
+
+--
+-- Expressions involving BOOL_OR aggregations
+--
+
+CREATE TABLE t (n INTEGER NOT NULL PRIMARY KEY, p BOOLEAN NOT NULL);
+INSERT INTO t (n, p) VALUES (0, TRUE);
+
+CREATE ASSERTION eq_bool_or     CHECK (TRUE  =      (SELECT BOOL_OR(p) FROM t));
+CREATE ASSERTION eq_any_bool_or CHECK (TRUE  =  ANY (SELECT BOOL_OR(p) FROM t));
+CREATE ASSERTION eq_all_bool_or CHECK (TRUE  =  ALL (SELECT BOOL_OR(p) FROM t));
+CREATE ASSERTION ne_bool_or     CHECK (FALSE <>     (SELECT BOOL_OR(p) FROM t));
+CREATE ASSERTION ne_any_bool_or CHECK (FALSE <> ANY (SELECT BOOL_OR(p) FROM t));
+CREATE ASSERTION ne_all_bool_or CHECK (FALSE <> ALL (SELECT BOOL_OR(p) FROM t));
+
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (operations = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('eq_all_bool_or', 'DELETE(t) UPDATE(t.p)'),
+       ('eq_any_bool_or', 'DELETE(t) UPDATE(t.p)'),
+       ('eq_bool_or',     'DELETE(t) UPDATE(t.p)'),
+       ('ne_all_bool_or', 'DELETE(t) UPDATE(t.p)'),
+       ('ne_any_bool_or', 'DELETE(t) UPDATE(t.p)'),
+       ('ne_bool_or',     'DELETE(t) UPDATE(t.p)'))
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+
+ROLLBACK;
+
+
+BEGIN TRANSACTION;
+
+--
+-- Expressions involving window functions
+--
+
+CREATE TABLE t (n INTEGER NOT NULL PRIMARY KEY, p BOOLEAN NOT NULL);
+
+-- Regular window function
+CREATE ASSERTION alternate_p CHECK (
+  NOT EXISTS (
+    SELECT FROM (
+      SELECT LAG(p, 1, NOT p) OVER n <> p
+         AND LEAD(p, 1, NOT p) OVER n <> p
+        FROM t
+      WINDOW n AS (ORDER BY n)
+    ) AS v(q)
+    WHERE NOT q
+  )
+);
+
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (operations = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('alternate_p', 'DELETE(t) INSERT(t) UPDATE(t.n, t.p)'))
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+
+INSERT INTO t (n, p) VALUES (10, TRUE);
+INSERT INTO t (n, p) VALUES (20, FALSE);
+INSERT INTO t (n, p) VALUES (30, TRUE);
+
+SAVEPOINT pre_insert;
+INSERT INTO t (n, p) VALUES (40, TRUE);
+ROLLBACK TO SAVEPOINT pre_insert;
+
+SAVEPOINT pre_delete;
+DELETE FROM t WHERE n = 20;
+ROLLBACK TO SAVEPOINT pre_delete;
+
+SAVEPOINT pre_update;
+UPDATE t SET p = NOT p WHERE n = 20;
+ROLLBACK TO SAVEPOINT pre_update;
+
+DROP ASSERTION alternate_p;
+
+-- Aggregate function over a window
+CREATE TABLE s (n INTEGER NOT NULL PRIMARY KEY, m INTEGER NOT NULL);
+
+INSERT INTO s (n, m)
+SELECT n, (2 * n)
+  FROM GENERATE_SERIES(0, 9) AS ns(n);
+
+CREATE ASSERTION max_over_window CHECK ((10, 20) > ALL (SELECT n, MAX(m) OVER (ORDER BY n) FROM s));
+
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (operations = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('max_over_window', 'INSERT(s) UPDATE(s.m, s.n)'))
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+
+ROLLBACK;
+
+
+BEGIN TRANSACTION;
+
+--
+-- Expressions containing a comparison should have the same invalidating
+-- operations as an expression containing the inverse comparison operation
+-- and where the operands have been switched
+--
+
+CREATE TABLE r (n INTEGER NOT NULL PRIMARY KEY);
+
+INSERT INTO r (n) VALUES (9);
+
+-- ">" and "<" are opposites
+CREATE ASSERTION a CHECK (10 > (SELECT MIN(n) FROM r));
+CREATE ASSERTION b CHECK ((SELECT MIN(n) FROM r) < 10);
+
+-- "<" and ">" are opposites
+CREATE ASSERTION c CHECK (8 < (SELECT MIN(n) FROM r));
+CREATE ASSERTION d CHECK ((SELECT MIN(n) FROM r) > 8);
+
+-- ">=" and "<=" are opposites
+CREATE ASSERTION e CHECK (9 >= (SELECT MIN(n) FROM r));
+CREATE ASSERTION f CHECK ((SELECT MIN(n) FROM r) <= 9);
+
+-- "<=" and ">=" are opposites
+CREATE ASSERTION g CHECK (9 <= (SELECT MIN(n) FROM r));
+CREATE ASSERTION h CHECK ((SELECT MIN(n) FROM r) >= 9);
+
+SELECT (test).predicate, (test).correct
+  FROM (
+VALUES ('a', 'b'),
+       ('c', 'd'),
+       ('e', 'f'),
+       ('g', 'h'))
+    AS v(a, b)
+ CROSS JOIN LATERAL (SELECT have_identical_invalidating_operations(v.a, v.b))
+    AS w(test)
+ ORDER BY 1, 2;
+
+ROLLBACK;
+
+
+BEGIN TRANSACTION;
+
+--
+-- The INFORMATION_SCHEMA should contain the correct information
+--
+
+CREATE TABLE t (n INTEGER NOT NULL PRIMARY KEY, p BOOLEAN NOT NULL);
+CREATE ASSERTION a CHECK (NOT EXISTS (SELECT FROM t WHERE p));
+
+SELECT predicate,
+       CASE WHEN
+         truth THEN 'YES'
+               ELSE 'NO'
+       END AS correct
+  FROM ( 
+VALUES ('INFORMATION_SCHEMA.CONSTRAINT_TABLE_USAGE contains only the correct tables',
+        EXISTS (
+          SELECT FROM information_schema.constraint_table_usage
+           WHERE (constraint_name, table_name) = ('a', 't'))
+        AND NOT EXISTS (
+          SELECT FROM information_schema.constraint_table_usage
+           WHERE constraint_name = 'a'
+             AND table_name <> 't')),
+       ('INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE contains only the correct columns (t.p)',
+        EXISTS (
+          SELECT FROM information_schema.constraint_column_usage
+           WHERE (constraint_name, table_name, column_name) = ('a', 't', 'p'))
+        AND NOT EXISTS (
+          SELECT FROM information_schema.constraint_column_usage
+           WHERE constraint_name ='a'
+             AND (table_name, column_name) <> ('t', 'p'))),
+       ('INFORMATION_SCHEMA.ASSERTIONS contains only the correct assertions',
+        EXISTS (
+          SELECT FROM information_schema.assertions
+           WHERE constraint_name = 'a')
+        AND NOT EXISTS (
+          SELECT FROM information_schema.assertions
+           WHERE constraint_name <> 'a')))
+    AS v(predicate, truth);
+
+ROLLBACK;
+
+BEGIN TRANSACTION;
+-- TODO This needs rethinking
+/*
+CREATE TABLE s (n INTEGER NOT NULL PRIMARY KEY, m INTEGER NOT NULL);
+CREATE TABLE t (n INTEGER NOT NULL PRIMARY KEY, p BOOLEAN NOT NULL);
+CREATE VIEW v AS SELECT * FROM s INNER JOIN t USING (n);
+
+CREATE ASSERTION a CHECK (NOT EXISTS (SELECT FROM v));
+
+-- we should depend on the view not the tables
+SELECT OK(depends_on('a', 'v'),
+          'Assertion depends upon the view v it references');
+SELECT OK(NOT depends_on('a', 's'),
+          'Assertion does not depend upon the table s referenced in the view v');
+SELECT OK(NOT depends_on('a', 't'),
+          'Assertion does not depend upon the table t referenced in the view v');
+
+-- we should trigger on the tables and not the view
+SELECT OK(EXISTS(SELECT FROM assertion_check_operation
+                  WHERE (assertion_name, relation_name) = ('a', 's')),
+          'Assertion is checked on modifications to table s');
+SELECT OK(EXISTS(SELECT FROM assertion_check_operation
+                  WHERE (assertion_name, relation_name) = ('a', 't')),
+          'Assertion is checked on modifications to table t');
+SELECT OK(NOT EXISTS(SELECT FROM assertion_check_operation
+                      WHERE (assertion_name, relation_name) = ('a', 'v')),
+          'Assertion is not checked on modifications to view v');
+*/
+ROLLBACK;
+
+-- TODO test commonalities between count, min, max, etc, for optimisations
+-- TODO ensure window function aggregates are treated the same as regular aggregates
+-- TODO ensure that conflicting aggregate functions are not incorrectly optimised
+-- TODO ensure that recorded dependencies are correct (traversal into functions and views)
+
+BEGIN TRANSACTION;
+
+DROP FUNCTION have_identical_invalidating_operations;
+DROP VIEW invalidating_summary;
+DROP VIEW invalidating_by_update_of_column;
+DROP VIEW invalidating_operation;
+
+COMMIT;
-- 
2.15.0

#8Fabien COELHO
coelho@cri.ensmp.fr
In reply to: Joe Wildish (#7)
Re: Implementing SQL ASSERTION

Hello Joe,

Just a reaction to the example, which is maybe addressed in the patch
which I have not investigated.

* certain combinations of aggregates with comparison operations cannot
be invalidating.

As an example of the last point, the expression "CHECK (10 > (SELECT
COUNT(*) FROM t))" cannot be invalidated by a delete or an update but
can be invalidated by an insert.

I'm wondering about the effect of MVVC on this: if the check is performed
when the INSERT is done, concurrent inserting transactions would count the
current status which would be ok, but on commit all concurrent inserts
would be there and the count could not be ok anymore?

Maybe if the check was deferred, but this is not currently possible with
pg (eg the select can simply be put in a function), and I there might be
race conditions. ISTM that such a check would imply non trivial locking to
be okay, it is not just a matter of deciding whether to invoke the check
or not.

--
Fabien.

#9Joe Wildish
joe-postgresql.org@elusive.cx
In reply to: Fabien COELHO (#8)
Re: Implementing SQL ASSERTION

Hi Fabien,

* certain combinations of aggregates with comparison operations cannot be invalidating.

As an example of the last point, the expression "CHECK (10 > (SELECT COUNT(*) FROM t))" cannot be invalidated by a delete or an update but can be invalidated by an insert.

I'm wondering about the effect of MVVC on this: if the check is performed when the INSERT is done, concurrent inserting transactions would count the current status which would be ok, but on commit all concurrent inserts would be there and the count could not be ok anymore?

Yes, there was quite a bit of discussion in the original thread about concurrency. See here:

/messages/by-id/1384486216.5008.17.camel@vanquo.pezone.net </messages/by-id/1384486216.5008.17.camel@vanquo.pezone.net

The patch doesn’t attempt to address concurrency (beyond the obvious benefit of reducing the circumstances under which the assertion is checked). I am working under the assumption that we will find some acceptable way for that to be resolved :-) And at the moment, working in serialisable mode addresses this issue. I think that is suggested in the thread actually (essentially, if you want to use assertions, you require that transactions be performed at serialisable isolation level).

Maybe if the check was deferred, but this is not currently possible with pg (eg the select can simply be put in a function), and I there might be race conditions. ISTM that such a check would imply non trivial locking to be okay, it is not just a matter of deciding whether to invoke the check or not.

I traverse into SQL functions so that the analysis can capture invalidating operations from the expression inside the function. Only internal and SQL functions are considered legal. Other languages are rejected.

-Joe

#10Fabien COELHO
coelho@cri.ensmp.fr
In reply to: Joe Wildish (#9)
Re: Implementing SQL ASSERTION

I'm wondering about the effect of MVVC on this: if the check is
performed when the INSERT is done, concurrent inserting transactions
would count the current status which would be ok, but on commit all
concurrent inserts would be there and the count could not be ok
anymore?

The patch doesnοΏ½t attempt to address concurrency (beyond the obvious
benefit of reducing the circumstances under which the assertion is
checked). I am working under the assumption that we will find some
acceptable way for that to be resolved :-) And at the moment, working in
serialisable mode addresses this issue. I think that is suggested in the
thread actually (essentially, if you want to use assertions, you require
that transactions be performed at serialisable isolation level).

Thanks for the pointers. The "serializable" isolation level restriction
sounds reasonnable.

--
Fabien.

#11David Fetter
david@fetter.org
In reply to: Fabien COELHO (#10)
Re: Implementing SQL ASSERTION

On Mon, Jan 15, 2018 at 03:40:57PM +0100, Fabien COELHO wrote:

I'm wondering about the effect of MVVC on this: if the check is
performed when the INSERT is done, concurrent inserting transactions
would count the current status which would be ok, but on commit all
concurrent inserts would be there and the count could not be ok anymore?

The patch doesn’t attempt to address concurrency (beyond the obvious
benefit of reducing the circumstances under which the assertion is
checked). I am working under the assumption that we will find some
acceptable way for that to be resolved :-) And at the moment, working in
serialisable mode addresses this issue. I think that is suggested in the
thread actually (essentially, if you want to use assertions, you require
that transactions be performed at serialisable isolation level).

Thanks for the pointers. The "serializable" isolation level restriction
sounds reasonnable.

It sounds reasonable enough that I'd like to make a couple of Modest
Proposals™, to wit:

- We follow the SQL standard and make SERIALIZABLE the default
transaction isolation level, and

- We disallow writes at isolation levels other than SERIALIZABLE when
any ASSERTION could be in play.

That latter could range in implementation from crashingly unsubtle to
very precise.

Crashingly Unsubtle:

Disallow writes at any isolation level other than SERIALIZABLE.

Very Precise:

Disallow writes at any other isolation level when the ASSERTION
could come into play using the same machinery that enforces the
ASSERTION in the first place.

What say?

Best,
David.
--
David Fetter <david(at)fetter(dot)org> http://fetter.org/
Phone: +1 415 235 3778

Remember to vote!
Consider donating to Postgres: http://www.postgresql.org/about/donate

#12Joe Wildish
joe-postgresql.org@elusive.cx
In reply to: David Fetter (#11)
Re: Implementing SQL ASSERTION

Hi David,

On 15 Jan 2018, at 16:35, David Fetter <david@fetter.org> wrote:

It sounds reasonable enough that I'd like to make a couple of Modest
Proposals™, to wit:

- We follow the SQL standard and make SERIALIZABLE the default
transaction isolation level, and

- We disallow writes at isolation levels other than SERIALIZABLE when
any ASSERTION could be in play.

Certainly it would be easy to put a test into the assertion check function to require the isolation level be serialisable. I didn’t realise that that was also the default level as per the standard. That need not necessarily be changed, of course; it would be obvious to the user that it was a requirement as the creation of an assertion would fail without it, as would any subsequent attempts to modify the involved tables.

-Joe

#13David Fetter
david@fetter.org
In reply to: Joe Wildish (#12)
Re: Implementing SQL ASSERTION

On Mon, Jan 15, 2018 at 09:14:02PM +0000, Joe Wildish wrote:

Hi David,

On 15 Jan 2018, at 16:35, David Fetter <david@fetter.org> wrote:

It sounds reasonable enough that I'd like to make a couple of Modest
Proposals™, to wit:

- We follow the SQL standard and make SERIALIZABLE the default
transaction isolation level, and

- We disallow writes at isolation levels other than SERIALIZABLE when
any ASSERTION could be in play.

Certainly it would be easy to put a test into the assertion check
function to require the isolation level be serialisable. I didn’t
realise that that was also the default level as per the standard.
That need not necessarily be changed, of course; it would be obvious
to the user that it was a requirement as the creation of an
assertion would fail without it, as would any subsequent attempts to
modify the involved tables.

This patch no longer applies. Any chance of a rebase?

Best,
David.
--
David Fetter <david(at)fetter(dot)org> http://fetter.org/
Phone: +1 415 235 3778

Remember to vote!
Consider donating to Postgres: http://www.postgresql.org/about/donate

#14Joe Wildish
joe-postgresql.org@elusive.cx
In reply to: David Fetter (#13)
Re: Implementing SQL ASSERTION

Hi David,

This patch no longer applies. Any chance of a rebase?

Of course. I’ll look at it this weekend,

Cheers,
-Joe

#15David Fetter
david@fetter.org
In reply to: Joe Wildish (#14)
Re: Implementing SQL ASSERTION

On Thu, Mar 08, 2018 at 09:11:58PM +0000, Joe Wildish wrote:

Hi David,

This patch no longer applies. Any chance of a rebase?

Of course. I’ll look at it this weekend,

Much appreciate it!

Best,
David.
--
David Fetter <david(at)fetter(dot)org> http://fetter.org/
Phone: +1 415 235 3778

Remember to vote!
Consider donating to Postgres: http://www.postgresql.org/about/donate

#16Robert Haas
robertmhaas@gmail.com
In reply to: David Fetter (#11)
Re: Implementing SQL ASSERTION

On Mon, Jan 15, 2018 at 11:35 AM, David Fetter <david@fetter.org> wrote:

- We follow the SQL standard and make SERIALIZABLE the default
transaction isolation level, and

The consequences of such a decision would include:

- pgbench -S would run up to 10x slower, at least if these old
benchmark results are still valid:

/messages/by-id/CA+TgmoZog1wFbyrqzJUkiLSXw5sDUjJGUeY0c2BqSG-tciSB7w@mail.gmail.com

- pgbench without -S would fail outright, because it doesn't have
provision to retry failed transactions.

https://commitfest.postgresql.org/16/1419/

- Many user applications would probably also experience similar difficulties.

- Parallel query would no longer work by default, unless this patch
gets committed:

https://commitfest.postgresql.org/17/1004/

I think a good deal of work to improve the performance of serializable
would need to be done before we could even think about making it the
default -- and even then, the fact that it really requires the
application to be retry-capable seems like a pretty major obstacle.

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#17Thomas Munro
thomas.munro@enterprisedb.com
In reply to: Robert Haas (#16)
Re: Implementing SQL ASSERTION

On Sat, Mar 10, 2018 at 6:37 AM, Robert Haas <robertmhaas@gmail.com> wrote:

On Mon, Jan 15, 2018 at 11:35 AM, David Fetter <david@fetter.org> wrote:

- We follow the SQL standard and make SERIALIZABLE the default
transaction isolation level, and

The consequences of such a decision would include:

- pgbench -S would run up to 10x slower, at least if these old
benchmark results are still valid:

/messages/by-id/CA+TgmoZog1wFbyrqzJUkiLSXw5sDUjJGUeY0c2BqSG-tciSB7w@mail.gmail.com

- pgbench without -S would fail outright, because it doesn't have
provision to retry failed transactions.

https://commitfest.postgresql.org/16/1419/

- Many user applications would probably also experience similar difficulties.

- Parallel query would no longer work by default, unless this patch
gets committed:

https://commitfest.postgresql.org/17/1004/

I think a good deal of work to improve the performance of serializable
would need to be done before we could even think about making it the
default -- and even then, the fact that it really requires the
application to be retry-capable seems like a pretty major obstacle.

Also:

- It's not available on hot standbys. Experimental patches have been
developed based on the read only safe snapshot concept, but some
tricky problems remain unsolved.

- Performance is terrible (conflicts are maximised) if you use any
index type except btree, unless some of these get committed:

https://commitfest.postgresql.org/17/1172/
https://commitfest.postgresql.org/17/1183/
https://commitfest.postgresql.org/17/1466/

--
Thomas Munro
http://www.enterprisedb.com

#18Joe Wildish
joe-postgresql.org@elusive.cx
In reply to: Joe Wildish (#14)
1 attachment(s)
Re: Implementing SQL ASSERTION

This patch no longer applies. Any chance of a rebase?

Attached is a rebased version of this patch. It takes into account the ACL checking changes and a few other minor amendments.

Cheers,
-Joe

Attachments:

0001-SQL-ASSERTION-prototype.patchapplication/octet-stream; name=0001-SQL-ASSERTION-prototype.patch; x-unix-mode=0600Download
From 293d6214bc04810459c3f120aca5ea545d07b045 Mon Sep 17 00:00:00 2001
From: Joe Wildish <joe@elusive.cx>
Date: Sun, 18 Mar 2018 12:04:59 +0000
Subject: [PATCH] SQL ASSERTION prototype

---
 doc/src/sgml/ddl.sgml                      |   31 +
 doc/src/sgml/ref/allfiles.sgml             |    3 +
 doc/src/sgml/ref/alter_assertion.sgml      |  139 ++++
 doc/src/sgml/ref/create_assertion.sgml     |  133 ++++
 doc/src/sgml/ref/drop_assertion.sgml       |  115 +++
 doc/src/sgml/reference.sgml                |    3 +
 src/backend/catalog/aclchk.c               |   33 +
 src/backend/catalog/information_schema.sql |   60 +-
 src/backend/catalog/namespace.c            |  121 +++
 src/backend/catalog/objectaddress.c        |   11 +
 src/backend/catalog/pg_constraint.c        |   20 +-
 src/backend/commands/alter.c               |    6 +
 src/backend/commands/constraint.c          | 1090 +++++++++++++++++++++++++++-
 src/backend/commands/event_trigger.c       |    2 +
 src/backend/nodes/copyfuncs.c              |   14 +
 src/backend/nodes/equalfuncs.c             |   12 +
 src/backend/parser/gram.y                  |   80 +-
 src/backend/parser/parse_agg.c             |    2 +
 src/backend/parser/parse_expr.c            |    2 +
 src/backend/parser/parse_func.c            |    4 +
 src/backend/tcop/utility.c                 |   20 +
 src/backend/utils/cache/lsyscache.c        |   19 +
 src/bin/psql/command.c                     |    3 +
 src/bin/psql/describe.c                    |   92 +++
 src/bin/psql/describe.h                    |    3 +
 src/bin/psql/help.c                        |    1 +
 src/bin/psql/tab-complete.c                |   30 +-
 src/include/catalog/namespace.h            |    1 +
 src/include/catalog/pg_constraint.h        |    3 +-
 src/include/catalog/pg_constraint_fn.h     |    2 +-
 src/include/catalog/pg_proc.h              |    4 +
 src/include/commands/constraint.h          |   10 +
 src/include/nodes/nodes.h                  |    1 +
 src/include/nodes/parsenodes.h             |   11 +
 src/include/parser/parse_node.h            |    1 +
 src/include/utils/acl.h                    |    1 +
 src/include/utils/lsyscache.h              |    1 +
 src/test/regress/expected/assertions.out   |  779 ++++++++++++++++++++
 src/test/regress/parallel_schedule         |    2 +-
 src/test/regress/serial_schedule           |    1 +
 src/test/regress/sql/assertions.sql        |  775 ++++++++++++++++++++
 41 files changed, 3572 insertions(+), 69 deletions(-)
 create mode 100644 doc/src/sgml/ref/alter_assertion.sgml
 create mode 100644 doc/src/sgml/ref/create_assertion.sgml
 create mode 100644 doc/src/sgml/ref/drop_assertion.sgml
 create mode 100644 src/include/commands/constraint.h
 create mode 100644 src/test/regress/expected/assertions.out
 create mode 100644 src/test/regress/sql/assertions.sql

diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index 3a54ba9d5a..81fde6403a 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -917,6 +917,37 @@ CREATE TABLE circles (
     of the type specified in the constraint declaration.
    </para>
   </sect2>
+
+  <sect2 id="ddl-constraints-assertions">
+   <title>Assertions</title>
+   
+   <indexterm zone="ddl-constraints-assertions">
+    <primary>assertion</primary>
+   </indexterm>
+
+   <para>
+    An assertion is a constraint that is not part of a table
+    definition.  An assertion can define constraints that evaluate the
+    data across multiple rows of a table beyond what unique and
+    exclusion constraints can do, and assertions can look at the data
+    in multiple tables.
+   </para>
+
+   <para>
+    An assertion is a separate schema object and is created with the
+    command <command>CREATE ASSERTION</command>.  The constraint
+    expression is written as a <literal>CHECK</literal> clause like in
+    check constraints.  For instance, to ensure that there is always
+    at least one entry in the product table:
+<programlisting>
+CREATE ASSERTION products_not_empty CHECK ((SELECT count(*) FROM products) &gt; 0);
+</programlisting>
+    Assertions will often involve aggregate functions computed over
+    entire tables.  Note, however, that this kind of assertion can be
+   quite inefficient and should only be used on tables that are small
+    and change rarely.
+   </para>
+  </sect2>
  </sect1>
 
  <sect1 id="ddl-system-columns">
diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index 22e6893211..57badaf157 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -7,6 +7,7 @@ Complete list of usable sgml source files in this directory.
 <!-- SQL commands -->
 <!ENTITY abort              SYSTEM "abort.sgml">
 <!ENTITY alterAggregate     SYSTEM "alter_aggregate.sgml">
+<!ENTITY alterAssertion     SYSTEM "alter_assertion.sgml">
 <!ENTITY alterCollation     SYSTEM "alter_collation.sgml">
 <!ENTITY alterConversion    SYSTEM "alter_conversion.sgml">
 <!ENTITY alterDatabase      SYSTEM "alter_database.sgml">
@@ -60,6 +61,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY copyTable          SYSTEM "copy.sgml">
 <!ENTITY createAccessMethod SYSTEM "create_access_method.sgml">
 <!ENTITY createAggregate    SYSTEM "create_aggregate.sgml">
+<!ENTITY createAssertion    SYSTEM "create_assertion.sgml">
 <!ENTITY createCast         SYSTEM "create_cast.sgml">
 <!ENTITY createCollation    SYSTEM "create_collation.sgml">
 <!ENTITY createConversion   SYSTEM "create_conversion.sgml">
@@ -107,6 +109,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY do                 SYSTEM "do.sgml">
 <!ENTITY dropAccessMethod   SYSTEM "drop_access_method.sgml">
 <!ENTITY dropAggregate      SYSTEM "drop_aggregate.sgml">
+<!ENTITY dropAssertion      SYSTEM "drop_assertion.sgml">
 <!ENTITY dropCast           SYSTEM "drop_cast.sgml">
 <!ENTITY dropCollation      SYSTEM "drop_collation.sgml">
 <!ENTITY dropConversion     SYSTEM "drop_conversion.sgml">
diff --git a/doc/src/sgml/ref/alter_assertion.sgml b/doc/src/sgml/ref/alter_assertion.sgml
new file mode 100644
index 0000000000..b06417f740
--- /dev/null
+++ b/doc/src/sgml/ref/alter_assertion.sgml
@@ -0,0 +1,139 @@
+<!--
+doc/src/sgml/ref/alter_assertion.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-alterassertion">
+
+ <indexterm zone="sql-alterassertion">
+  <primary>ALTER ASSERTION</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>ALTER ASSERTION</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>ALTER ASSERTION</refname>
+  <refpurpose>change the definition of an assertion</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+ALTER ASSERTION <replaceable>name</replaceable> RENAME TO <replaceable>new_name</replaceable>
+ALTER ASSERTION <replaceable>name</replaceable> OWNER TO <replaceable>new_owner</replaceable>
+ALTER ASSERTION <replaceable>name</replaceable> SET SCHEMA <replaceable>new_schema</replaceable>
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>ALTER ASSERTION</command> changes the definition of an
+   assertion.
+  </para>
+
+  <para>
+   You must own the assertion to use <command>ALTER ASSERTION</command>.  To
+   change the schema of an assertion, you must also have
+   <literal>CREATE</literal> privilege on the new schema.  To alter
+   the owner, you must also be a direct or indirect member of the new
+   owning role, and that role must have <literal>CREATE</literal>
+   privilege on the assertion's schema.  (These restrictions enforce
+   that altering the owner doesn't do anything you couldn't do by
+   dropping and recreating the assertion.  However, a superuser can
+   alter ownership of any assertion 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 existing assertion.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_name</replaceable></term>
+    <listitem>
+     <para>
+      The new name of the assertion.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_owner</replaceable></term>
+    <listitem>
+     <para>
+      The new owner of the assertion.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_schema</replaceable></term>
+    <listitem>
+     <para>
+      The new schema for the assertion.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To rename the assertuib <literal>check_size</literal>
+   to <literal>check_count</literal>:
+<programlisting>
+ALTER ASSERTION check_size RENAME TO check_count;
+</programlisting>
+  </para>
+
+  <para>
+   To change the owner of the assertion <literal>check_size</literal>
+   to <literal>joe</literal>:
+<programlisting>
+ALTER ASSERTION check_size OWNER TO joe;
+</programlisting>
+  </para>
+
+  <para>
+   To move the assertion <literal>check_size</literal> into
+   schema <literal>myschema</literal>:
+<programlisting>
+ALTER ASSERTION check_size SET SCHEMA myschema;
+</programlisting>
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   There is no <command>ALTER ASSERTION</command> statement in the SQL
+   standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-createassertion"/></member>
+   <member><xref linkend="sql-dropassertion"/></member>
+  </simplelist>
+ </refsect1>
+</refentry>
diff --git a/doc/src/sgml/ref/create_assertion.sgml b/doc/src/sgml/ref/create_assertion.sgml
new file mode 100644
index 0000000000..b36d1b4ecb
--- /dev/null
+++ b/doc/src/sgml/ref/create_assertion.sgml
@@ -0,0 +1,133 @@
+<!--
+doc/src/sgml/ref/create_assertion.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-createassertion">
+ <indexterm zone="sql-createassertion">
+  <primary>CREATE ASSERTION</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>CREATE ASSERTION</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>CREATE ASSERTION</refname>
+  <refpurpose>define a new assertion</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+CREATE ASSERTION <replaceable class="parameter">name</replaceable> CHECK ( <replaceable class="parameter">name</replaceable> ) [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>CREATE ASSERTION</command> creates an assertion.  An
+   assertion is a check constraint that is independent of a table row
+   and a table.  It can therefore be used to enforce more complex
+   constraints across multiple table rows and across multiple tables.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><replaceable class="parameter">name</replaceable></term>
+    <listitem>
+     <para>
+      The name (optionally schema-qualified) of the assertion to
+      create.  Assertions use the same namespace as constraints on
+      tables.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>CHECK ( <replaceable class="parameter">expression</replaceable> )</literal></term>
+    <listitem>
+     <para>
+      The <literal>CHECK</literal> clause specifies an expression producing a
+      Boolean result which the database must satisfy at all times for a
+      data change operation to succeed.  Expressions evaluating
+      to TRUE or UNKNOWN succeed.  Should the result of a data change
+      operation produce a FALSE result an error exception is
+      raised and the change is not made.
+     </para>
+
+     <para>
+      The check expression typically involves subselects in order to
+      read data from tables.  See the examples below.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>DEFERRABLE</literal></term>
+    <term><literal>NOT DEFERRABLE</literal></term>
+    <term><literal>INITIALLY IMMEDIATE</literal></term>
+    <term><literal>INITIALLY DEFERRED</literal></term>
+    <listitem>
+     <para>
+      These clauses control the deferrability of the constraint.  See
+      <xref linkend="sql-createtable"/> for an explanation.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Notes</title>
+
+  <para>
+   More specialized constraint forms such as table check constraints,
+   foreign-key constraints, or exclusion constraints should be used
+   instead when applicable, because they will be more efficient.
+  </para>
+
+  <para>
+   Assertion checks are not specially optimized.  For example,
+   checking the row count of a large table in an assertion will be
+   just as slow as implementing the check manually.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   Check that the table <literal>table1</literal> has at most 30 rows:
+<programlisting>
+CREATE ASSERTION table1_max30 CHECK ((SELECT count(*) FROM table1) &lt;= 30);
+</programlisting>
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   <command>CREATE ASSERTION</command> conforms to the SQL standard.
+   The PostgreSQL implementation has certain restrictions on what
+   check expressions are allowed in assertions.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-alterassertion"/></member>
+   <member><xref linkend="sql-dropassertion"/></member>
+  </simplelist>
+ </refsect1>
+</refentry>
diff --git a/doc/src/sgml/ref/drop_assertion.sgml b/doc/src/sgml/ref/drop_assertion.sgml
new file mode 100644
index 0000000000..39a60a0d29
--- /dev/null
+++ b/doc/src/sgml/ref/drop_assertion.sgml
@@ -0,0 +1,115 @@
+<!--
+doc/src/sgml/ref/drop_assertion.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-dropassertion">
+
+ <indexterm zone="sql-dropassertion">
+  <primary>DROP ASSERTION</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>DROP ASSERTION</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>DROP ASSERTION</refname>
+  <refpurpose>remove an assertion</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+DROP ASSERTION [ IF EXISTS ] <replaceable class="parameter">name</replaceable> [, ...] [ CASCADE | RESTRICT ]
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>DROP ASSERTION</command> removes an existing assertion. To
+   execute this command the current user must be the owner of the
+   assertion.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+
+   <varlistentry>
+    <term><literal>IF EXISTS</literal></term>
+    <listitem>
+     <para>
+      Do not throw an error if the assertion 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 existing assertion.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>CASCADE</literal></term>
+    <listitem>
+     <para>
+      Automatically drop objects that depend on the assertion.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>RESTRICT</literal></term>
+    <listitem>
+     <para>
+      Refuse to drop the assertion if any objects depend on it.  This
+      is the default.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To remove the assertion <literal>check_size</literal>:
+<programlisting>
+DROP ASSERTION check_size;
+</programlisting>
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   This command conforms to the SQL standard, except that the standard
+   only allows one assertion to be dropped per command, and apart from
+   the <literal>IF EXISTS</literal> option, which is
+   a <productname>PostgreSQL</productname> extension.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-alterassertion"/></member>
+   <member><xref linkend="sql-createassertion"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index d27fb414f7..9300f2d9fc 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -35,6 +35,7 @@
 
    &abort;
    &alterAggregate;
+   &alterAssertion;
    &alterCollation;
    &alterConversion;
    &alterDatabase;
@@ -88,6 +89,7 @@
    &copyTable;
    &createAccessMethod;
    &createAggregate;
+   &createAssertion;
    &createCast;
    &createCollation;
    &createConversion;
@@ -135,6 +137,7 @@
    &do;
    &dropAccessMethod;
    &dropAggregate;
+   &dropAssertion;
    &dropCast;
    &dropCollation;
    &dropConversion;
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 0648539796..e804e6d493 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -3362,6 +3362,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_AGGREGATE:
 						msg = gettext_noop("permission denied for aggregate %s");
 						break;
+					case OBJECT_ASSERTION:
+						msg = gettext_noop("permission denied for assertion %s");
+						break;
 					case OBJECT_COLLATION:
 						msg = gettext_noop("permission denied for collation %s");
 						break;
@@ -3493,6 +3496,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_AGGREGATE:
 						msg = gettext_noop("must be owner of aggregate %s");
 						break;
+					case OBJECT_ASSERTION:
+						msg = gettext_noop("must be owner of assertion %s");
+						break;
 					case OBJECT_COLLATION:
 						msg = gettext_noop("must be owner of collation %s");
 						break;
@@ -5211,6 +5217,33 @@ pg_collation_ownercheck(Oid coll_oid, Oid roleid)
 	return has_privs_of_role(roleid, ownerId);
 }
 
+/*
+ * Ownership check for a constraint (specified by OID).
+ */
+bool
+pg_constraint_ownercheck(Oid constr_oid, Oid roleid)
+{
+	HeapTuple   tuple;
+	//Oid           ownerId;
+
+	/* Superusers bypass all permission checking. */
+	if (superuser_arg(roleid))
+		return true;
+
+	tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(constr_oid));
+	if (!HeapTupleIsValid(tuple))
+		ereport(ERROR,
+			    (errcode(ERRCODE_UNDEFINED_OBJECT),
+			     errmsg("constraint with OID %u does not exist", constr_oid)));
+
+	//FIXME: ownerId = ((Form_pg_constraint) GETSTRUCT(tuple))->conowner;
+
+	ReleaseSysCache(tuple);
+
+	//return has_privs_of_role(roleid, ownerId);
+	return true; // for now
+}
+
 /*
  * Ownership check for a conversion (specified by OID).
  */
diff --git a/src/backend/catalog/information_schema.sql b/src/backend/catalog/information_schema.sql
index f4e69f4a26..23cc0759a3 100644
--- a/src/backend/catalog/information_schema.sql
+++ b/src/backend/catalog/information_schema.sql
@@ -285,7 +285,20 @@ GRANT SELECT ON administrable_role_authorizations TO PUBLIC;
  * ASSERTIONS view
  */
 
--- feature not supported
+CREATE VIEW assertions AS
+    SELECT CAST(current_database() AS sql_identifier) AS constraint_catalog,
+           CAST(n.nspname AS sql_identifier) AS constraint_schema,
+           CAST(con.conname AS sql_identifier) AS constraint_name,
+           CAST(CASE WHEN condeferrable THEN 'YES' ELSE 'NO' END
+             AS yes_or_no) AS is_deferrable,
+           CAST(CASE WHEN condeferred THEN 'YES' ELSE 'NO' END
+             AS yes_or_no) AS initially_deferred
+    FROM pg_namespace n, pg_constraint con
+    WHERE n.oid = con.connamespace
+          AND con.conrelid = 0 AND con.contypid = 0;
+          -- TODO: AND pg_has_role(con.conowner, 'USAGE');
+
+GRANT SELECT ON assertions TO PUBLIC;
 
 
 /*
@@ -790,7 +803,7 @@ CREATE VIEW constraint_column_usage AS
            CAST(cstrname AS sql_identifier) AS constraint_name
 
     FROM (
-        /* check constraints */
+        /* assertions and check constraints */
         SELECT DISTINCT nr.nspname, r.relname, r.relowner, a.attname, nc.nspname, c.conname
           FROM pg_namespace nr, pg_class r, pg_attribute a, pg_depend d, pg_namespace nc, pg_constraint c
           WHERE nr.oid = r.relnamespace
@@ -802,7 +815,7 @@ CREATE VIEW constraint_column_usage AS
             AND d.objid = c.oid
             AND c.connamespace = nc.oid
             AND c.contype = 'c'
-            AND r.relkind IN ('r', 'p')
+            AND r.relkind IN ('r', 'p', 'v')
             AND NOT a.attisdropped
 
         UNION ALL
@@ -842,20 +855,41 @@ GRANT SELECT ON constraint_column_usage TO PUBLIC;
 
 CREATE VIEW constraint_table_usage AS
     SELECT CAST(current_database() AS sql_identifier) AS table_catalog,
-           CAST(nr.nspname AS sql_identifier) AS table_schema,
-           CAST(r.relname AS sql_identifier) AS table_name,
+           CAST(tblschema AS sql_identifier) AS table_schema,
+           CAST(tblname AS sql_identifier) AS table_name,
            CAST(current_database() AS sql_identifier) AS constraint_catalog,
-           CAST(nc.nspname AS sql_identifier) AS constraint_schema,
-           CAST(c.conname AS sql_identifier) AS constraint_name
+           CAST(cstrschema AS sql_identifier) AS constraint_schema,
+           CAST(cstrname AS sql_identifier) AS constraint_name
+
+    FROM (
+        /* assertions and check constraints */
+        SELECT DISTINCT nr.nspname, r.relname, r.relowner, nc.nspname, c.conname
+          FROM pg_namespace nr, pg_class r,
+               pg_depend d, pg_namespace nc, pg_constraint c
+         WHERE nr.oid = r.relnamespace
+           AND d.refobjid = r.oid
+           AND c.connamespace = nc.oid
+           AND d.objid = c.oid
+           AND d.refclassid = 'pg_catalog.pg_class'::regclass
+           AND d.classid = 'pg_catalog.pg_constraint'::regclass
+           AND c.contype = 'c'
+           AND r.relkind IN ('r', 'p', 'v')
 
-    FROM pg_constraint c, pg_namespace nc,
-         pg_class r, pg_namespace nr
+        UNION ALL
 
-    WHERE c.connamespace = nc.oid AND r.relnamespace = nr.oid
-          AND ( (c.contype = 'f' AND c.confrelid = r.oid)
+        /* unique/primary key constraints */
+        SELECT nr.nspname, r.relname, r.relowner, nc.nspname, c.conname
+          FROM pg_constraint c, pg_namespace nc,
+               pg_class r, pg_namespace nr
+         WHERE c.connamespace = nc.oid
+           AND r.relnamespace = nr.oid
+           AND ( (c.contype = 'f' AND c.confrelid = r.oid)
              OR (c.contype IN ('p', 'u') AND c.conrelid = r.oid) )
-          AND r.relkind IN ('r', 'p')
-          AND pg_has_role(r.relowner, 'USAGE');
+           AND r.relkind IN ('r', 'p')
+
+    ) AS x (tblschema, tblname, tblowner, cstrschema, cstrname)
+
+    WHERE pg_has_role(x.tblowner, 'USAGE');
 
 GRANT SELECT ON constraint_table_usage TO PUBLIC;
 
diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c
index 52dd400b96..0bb1e31eff 100644
--- a/src/backend/catalog/namespace.c
+++ b/src/backend/catalog/namespace.c
@@ -27,6 +27,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_authid.h"
 #include "catalog/pg_collation.h"
+#include "catalog/pg_constraint.h"
 #include "catalog/pg_conversion.h"
 #include "catalog/pg_conversion_fn.h"
 #include "catalog/pg_namespace.h"
@@ -57,9 +58,11 @@
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/syscache.h"
+#include "utils/fmgroids.h"
 #include "utils/varlena.h"
 
 
+
 /*
  * The namespace search path is a possibly-empty list of namespace OIDs.
  * In addition to the explicit list, implicitly-searched namespaces
@@ -3483,6 +3486,124 @@ PopOverrideSearchPath(void)
 }
 
 
+static Oid
+get_assertion_oid_internal(Relation pg_constraint, char *assertion_name, Oid namespaceId, List *name)
+{
+	SysScanDesc scan;
+	ScanKeyData skey[4];
+	HeapTuple	tuple;
+	Oid			conOid = InvalidOid;
+
+	ScanKeyInit(&skey[0],
+				Anum_pg_constraint_conname,
+				BTEqualStrategyNumber, F_NAMEEQ,
+				PointerGetDatum(assertion_name));
+
+	ScanKeyInit(&skey[1],
+				Anum_pg_constraint_connamespace,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(namespaceId));
+
+	ScanKeyInit(&skey[2],
+				Anum_pg_constraint_conrelid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				InvalidOid);
+
+	ScanKeyInit(&skey[3],
+				Anum_pg_constraint_contypid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				InvalidOid);
+
+	scan = systable_beginscan(pg_constraint, InvalidOid, false,
+							  NULL, 4, skey);
+
+	/*
+	 * Fetch the constraint tuple from pg_constraint.  There may be
+	 * more than one match, because constraints are not required to
+	 * have unique names; if so, error out.
+	 */
+	while (HeapTupleIsValid(tuple = systable_getnext(scan)))
+	{
+		Form_pg_constraint con = (Form_pg_constraint) GETSTRUCT(tuple);
+
+		if (OidIsValid(con->conrelid) || OidIsValid(con->contypid))
+			ereport(ERROR,
+					(errmsg("constraint \"%s\" is not an assertion",
+							NameListToString(name))));
+
+		if (strcmp(NameStr(con->conname), assertion_name) == 0)
+		{
+			if (OidIsValid(conOid))
+				ereport(ERROR,
+						(errcode(ERRCODE_DUPLICATE_OBJECT),
+								errmsg("there are multiple assertions named \"%s\"",
+									   NameListToString(name))));
+			conOid = HeapTupleGetOid(tuple);
+		}
+	}
+
+	systable_endscan(scan);
+
+	return conOid;
+}
+
+/*
+ * get_assertion_oid
+ *		Find an assertion with the specified name.
+ *		Returns constraint's OID.
+ */
+Oid
+get_assertion_oid(List *name, bool missing_ok)
+{
+	char	   *schemaname;
+	char	   *assertion_name;
+	Oid			namespaceId;
+	Relation	pg_constraint;
+	Oid			conOid = InvalidOid;
+
+	DeconstructQualifiedName(name, &schemaname, &assertion_name);
+
+	pg_constraint = heap_open(ConstraintRelationId, AccessShareLock);
+
+	if (schemaname)
+	{
+		namespaceId = LookupExplicitNamespace(schemaname, missing_ok);
+		conOid = get_assertion_oid_internal(pg_constraint, assertion_name, namespaceId, name);
+	}
+	else
+	{
+		ListCell   *l;
+
+		recomputeNamespacePath();
+
+		foreach(l, activeSearchPath)
+		{
+			namespaceId = lfirst_oid(l);
+
+			if (namespaceId == myTempNamespace)
+				continue;		/* do not look in temp namespace */
+
+			conOid = get_assertion_oid_internal(pg_constraint, assertion_name, namespaceId, name);
+
+			if (OidIsValid(conOid))
+				break;
+		}
+	}
+
+	heap_close(pg_constraint, AccessShareLock);
+
+	/* If no such constraint exists, complain */
+	if (!OidIsValid(conOid) && !missing_ok)
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+						errmsg("assertion \"%s\" does not exist",
+							   NameListToString(name))));
+
+	return conOid;
+}
+
+
+
 /*
  * get_collation_oid - find a collation by possibly qualified name
  *
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 119297b33a..1c9f186d02 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -899,6 +899,11 @@ get_object_address(ObjectType objtype, Node *object,
 				address.objectId = LookupOperWithArgs(castNode(ObjectWithArgs, object), missing_ok);
 				address.objectSubId = 0;
 				break;
+			case OBJECT_ASSERTION:
+				address.classId = ConstraintRelationId;
+				address.objectId = get_assertion_oid(castNode(List, object), missing_ok);
+				address.objectSubId = 0;
+				break;
 			case OBJECT_COLLATION:
 				address.classId = CollationRelationId;
 				address.objectId = get_collation_oid(castNode(List, object), missing_ok);
@@ -2117,6 +2122,7 @@ pg_get_object_address(PG_FUNCTION_ARGS)
 		case OBJECT_FOREIGN_TABLE:
 		case OBJECT_COLUMN:
 		case OBJECT_ATTRIBUTE:
+		case OBJECT_ASSERTION:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_STATISTIC_EXT:
@@ -2277,6 +2283,11 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
 				aclcheck_error(ACLCHECK_NOT_OWNER, objtype,
 							   strVal((Value *) object));
 			break;
+		case OBJECT_ASSERTION:
+			if (!pg_constraint_ownercheck(address.objectId, roleid))
+				aclcheck_error(ACLCHECK_NOT_OWNER, objtype,
+							   NameListToString(castNode(List, object)));
+			break;
 		case OBJECT_COLLATION:
 			if (!pg_collation_ownercheck(address.objectId, roleid))
 				aclcheck_error(ACLCHECK_NOT_OWNER, objtype,
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index 731c5e4317..08e3e4f9c8 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -429,6 +429,13 @@ ConstraintNameIsUsed(ConstraintCategory conCat, Oid objId,
 			found = true;
 			break;
 		}
+		else if (conCat == CONSTRAINT_ASSERTION
+				 && con->conrelid == InvalidOid
+				 && con->contypid == InvalidOid)
+		{
+			found = true;
+			break;
+		}
 	}
 
 	systable_endscan(conscan);
@@ -600,8 +607,7 @@ RemoveConstraintById(Oid conId)
 		 * but we have no such concept at the moment.
 		 */
 	}
-	else
-		elog(ERROR, "constraint %u is not of a known type", conId);
+	/* Else it's an assertion; nothing special for that. */
 
 	/* Fry the constraint itself */
 	CatalogTupleDelete(conDesc, &tup->t_self);
@@ -657,6 +663,16 @@ RenameConstraintById(Oid conId, const char *newname)
 				(errcode(ERRCODE_DUPLICATE_OBJECT),
 				 errmsg("constraint \"%s\" for domain %s already exists",
 						newname, format_type_be(con->contypid))));
+	if (!OidIsValid(con->conrelid) &&
+		!OidIsValid(con->contypid) &&
+		ConstraintNameIsUsed(CONSTRAINT_ASSERTION,
+							 InvalidOid,
+							 con->connamespace,
+							 newname))
+		ereport(ERROR,
+				(errcode(ERRCODE_DUPLICATE_OBJECT),
+						errmsg("assertion \"%s\" already exists",
+							   newname)));
 
 	/* OK, do the rename --- tuple is a copy, so OK to scribble on it */
 	namestrcpy(&(con->conname), newname);
diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c
index 0d63866fb0..5cb6e0fe7d 100644
--- a/src/backend/commands/alter.c
+++ b/src/backend/commands/alter.c
@@ -41,6 +41,7 @@
 #include "commands/alter.h"
 #include "commands/collationcmds.h"
 #include "commands/conversioncmds.h"
+#include "commands/constraint.h"
 #include "commands/dbcommands.h"
 #include "commands/defrem.h"
 #include "commands/event_trigger.h"
@@ -326,6 +327,9 @@ ExecRenameStmt(RenameStmt *stmt)
 {
 	switch (stmt->renameType)
 	{
+		case OBJECT_ASSERTION:
+			return RenameAssertion(stmt);
+
 		case OBJECT_TABCONSTRAINT:
 		case OBJECT_DOMCONSTRAINT:
 			return RenameConstraint(stmt);
@@ -491,6 +495,7 @@ ExecAlterObjectSchemaStmt(AlterObjectSchemaStmt *stmt,
 
 			/* generic code path */
 		case OBJECT_AGGREGATE:
+		case OBJECT_ASSERTION:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_FUNCTION:
@@ -838,6 +843,7 @@ ExecAlterOwnerStmt(AlterOwnerStmt *stmt)
 
 			/* Generic cases */
 		case OBJECT_AGGREGATE:
+		case OBJECT_ASSERTION:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_FUNCTION:
diff --git a/src/backend/commands/constraint.c b/src/backend/commands/constraint.c
index 90f19ad3dd..172508510f 100644
--- a/src/backend/commands/constraint.c
+++ b/src/backend/commands/constraint.c
@@ -11,6 +11,8 @@
  *
  *-------------------------------------------------------------------------
  */
+
+
 #include "postgres.h"
 
 #include "catalog/index.h"
@@ -20,6 +22,40 @@
 #include "utils/rel.h"
 #include "utils/tqual.h"
 
+#include "access/htup_details.h"
+#include "catalog/dependency.h"
+#include "catalog/namespace.h"
+#include "catalog/pg_aggregate.h"
+#include "catalog/pg_constraint.h"
+#include "catalog/pg_constraint_fn.h"
+#include "catalog/pg_operator.h"
+#include "catalog/pg_proc.h"
+#include "catalog/pg_language.h"
+#include "catalog/pg_trigger.h"
+#include "catalog/pg_type.h"
+#include "commands/constraint.h"
+#include "executor/functions.h"
+#include "executor/spi.h"
+#include "miscadmin.h"
+#include "nodes/makefuncs.h"
+#include "nodes/nodeFuncs.h"
+#include "optimizer/planner.h"
+#include "parser/analyze.h"
+#include "parser/parse_coerce.h"
+#include "parser/parse_expr.h"
+#include "parser/parse_node.h"
+#include "parser/parse_relation.h"
+#include "parser/parser.h"
+#include "parser/parsetree.h"
+#include "rewrite/rewriteHandler.h"
+#include "tcop/tcopprot.h"
+#include "utils/acl.h"
+#include "utils/lsyscache.h"
+#include "utils/snapmgr.h"
+#include "utils/syscache.h"
+#include "utils/ruleutils.h"
+#include "utils/fmgroids.h"
+
 
 /*
  * unique_key_recheck - trigger function to do a deferred uniqueness check.
@@ -39,15 +75,15 @@ unique_key_recheck(PG_FUNCTION_ARGS)
 {
 	TriggerData *trigdata = castNode(TriggerData, fcinfo->context);
 	const char *funcname = "unique_key_recheck";
-	HeapTuple	new_row;
+	HeapTuple new_row;
 	ItemPointerData tmptid;
-	Relation	indexRel;
-	IndexInfo  *indexInfo;
-	EState	   *estate;
+	Relation indexRel;
+	IndexInfo *indexInfo;
+	EState *estate;
 	ExprContext *econtext;
 	TupleTableSlot *slot;
-	Datum		values[INDEX_MAX_KEYS];
-	bool		isnull[INDEX_MAX_KEYS];
+	Datum values[INDEX_MAX_KEYS];
+	bool isnull[INDEX_MAX_KEYS];
 
 	/*
 	 * Make sure this is being called as an AFTER ROW trigger.  Note:
@@ -57,15 +93,15 @@ unique_key_recheck(PG_FUNCTION_ARGS)
 	if (!CALLED_AS_TRIGGER(fcinfo))
 		ereport(ERROR,
 				(errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED),
-				 errmsg("function \"%s\" was not called by trigger manager",
-						funcname)));
+					errmsg("function \"%s\" was not called by trigger manager",
+						   funcname)));
 
 	if (!TRIGGER_FIRED_AFTER(trigdata->tg_event) ||
 		!TRIGGER_FIRED_FOR_ROW(trigdata->tg_event))
 		ereport(ERROR,
 				(errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED),
-				 errmsg("function \"%s\" must be fired AFTER ROW",
-						funcname)));
+					errmsg("function \"%s\" must be fired AFTER ROW",
+						   funcname)));
 
 	/*
 	 * Get the new data that was inserted/updated.
@@ -78,9 +114,9 @@ unique_key_recheck(PG_FUNCTION_ARGS)
 	{
 		ereport(ERROR,
 				(errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED),
-				 errmsg("function \"%s\" must be fired for INSERT or UPDATE",
-						funcname)));
-		new_row = NULL;			/* keep compiler quiet */
+					errmsg("function \"%s\" must be fired for INSERT or UPDATE",
+						   funcname)));
+		new_row = NULL;            /* keep compiler quiet */
 	}
 
 	/*
@@ -194,3 +230,1031 @@ unique_key_recheck(PG_FUNCTION_ARGS)
 
 	return PointerGetDatum(NULL);
 }
+
+
+static void
+test_assertion_expr(char *name, char *expression)
+{
+	char *sql;
+	int returnCode;
+	bool isNull;
+	Datum value;
+
+	if (SPI_connect() != SPI_OK_CONNECT)
+		elog(ERROR, "SPI connect failed when executing ASSERTION statement");
+
+	sql = psprintf("SELECT %s", expression);
+	returnCode = SPI_exec(sql, 1);
+
+	if (returnCode > 0 && SPI_tuptable != NULL)
+	{
+		value = SPI_getbinval(SPI_tuptable->vals[0], SPI_tuptable->tupdesc, 1, &isNull);
+		if (isNull)
+			ereport(ERROR,
+					(errcode(ERRCODE_CHECK_VIOLATION),
+							errmsg("assertion \"%s\" truth is unknown", name)));
+		else if (!isNull && !DatumGetBool(value))
+			ereport(ERROR,
+					(errcode(ERRCODE_CHECK_VIOLATION),
+							errmsg("assertion \"%s\" violated", name)));
+	}
+	else
+		elog(ERROR, "unexpected SPI result when executing ASSERTION statement");
+
+	SPI_finish();
+}
+
+
+Datum
+assertion_check(PG_FUNCTION_ARGS)
+{
+	TriggerData *trigdata = (TriggerData *) fcinfo->context;
+	const char *funcname = "assertion_check";
+	Oid constraintOid;
+	HeapTuple tup;
+	Datum adatum;
+	bool isNull;
+
+	/*
+	 * Make sure this is being called as an AFTER STATEMENT trigger.	Note:
+	 * translatable error strings are shared with ri_triggers.c, so resist the
+	 * temptation to fold the function name into them.
+	 */
+	if (!CALLED_AS_TRIGGER(fcinfo))
+		ereport(ERROR,
+				(errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED),
+					errmsg("function \"%s\" was not called by trigger manager",
+						   funcname)));
+
+	if (!TRIGGER_FIRED_AFTER(trigdata->tg_event) ||
+		!TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event))
+		ereport(ERROR,
+				(errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED),
+					errmsg("function \"%s\" must be fired AFTER STATEMENT",
+						   funcname)));
+
+	constraintOid = trigdata->tg_trigger->tgconstraint;
+	tup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(constraintOid));
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for constraint %u", constraintOid);
+
+	// XXX bogus
+	adatum = SysCacheGetAttr(CONSTROID, tup,
+							 Anum_pg_constraint_consrc, &isNull);
+	if (isNull)
+		elog(ERROR, "constraint %u has null consrc", constraintOid);
+
+	test_assertion_expr(get_constraint_name(constraintOid), TextDatumGetCString(adatum));
+
+	ReleaseSysCache(tup);
+	return PointerGetDatum(NULL);
+}
+
+#define NO_FUNC				(0)
+#define OTHER_FUNC			(1 << 0)
+#define MIN_AGG_FUNC		(1 << 1)
+#define MAX_AGG_FUNC		(1 << 2)
+#define COUNT_AGG_FUNC		(1 << 3)
+#define EVERY_AGG_FUNC		(1 << 4)
+#define BOOL_AND_AGG_FUNC	(1 << 5)
+#define BOOL_OR_AGG_FUNC	(1 << 6)
+
+#define EQ_COMPARISONS 		(bms_make_singleton(ROWCOMPARE_EQ))
+#define NE_COMPARISONS 		(bms_make_singleton(ROWCOMPARE_NE))
+#define LTE_COMPARISONS 	(bms_add_member(bms_make_singleton(ROWCOMPARE_LE), ROWCOMPARE_LT))
+#define GTE_COMPARISONS 	(bms_add_member(bms_make_singleton(ROWCOMPARE_GE), ROWCOMPARE_GT))
+
+#define MatchesOnly(a,b)	(((a) & (b)) && !((a) & (~(b))))
+#define CanOptimise(s) 		(!bms_is_empty(s) && \
+							(bms_is_subset(s, EQ_COMPARISONS)  || \
+							 bms_is_subset(s, NE_COMPARISONS)  || \
+							 bms_is_subset(s, LTE_COMPARISONS) || \
+							 bms_is_subset(s, GTE_COMPARISONS) ))
+
+
+// TODO review TODOs in the tests
+// TODO assertions should allow unknown according to the SQL specification
+// TODO traversal into views needs rethinking - dependencies?
+// TODO pg_dump support
+
+/*
+ * DML operations that could affect the truth of an assertion. Only
+ * INSERT and DELETE are considered as part of the labeling algorithm.
+ * UPDATEs are inferred using different logic.
+ */
+typedef enum DmlOp
+{
+	INSERT,
+	DELETE,
+	INSERT_DELETE
+} DmlOp;
+
+
+/*
+ * A context used to capture operations that may invalidate an assertion.
+ */
+typedef struct AssertionInfo
+{
+	DmlOp	label;         /* current invaliding operation */
+	Expr   *expr;          /* current Expression node */
+	List   *rtable;        /* current range tables */
+	List   *operators;     /* current operators */
+	SetOperation setOp;         /* current set-operation */
+	bool	invert;		   /* ... */
+	bool	inView;        /* if the walker has entered a view */
+	bool	inTargetEntry; /* if the walker has entered a TargetEntry node */
+	bool	inComparison;  /* if the walker has entered a comparison operation */
+	bool	inExists;      /* if the walker has entered an EXISTS node */
+
+	List   *dependencies; /* ... */
+	List   *relations;    /* relation Oids for all tables involved in the assertion check */
+	List   *inserts;      /* relation Oids for which INSERT could invalidate the assertion */
+	List   *deletes;      /* relation Oids for which DELETE could invalidate the assertion */
+	List   *updates;      /* relation Oids for which UPDATE could invalidate the assertion */
+	List   *columns;      /* list of ObjectAddresses referencing involved columns */
+} AssertionInfo;
+
+
+static void initAssertionInfo(AssertionInfo *info);
+static void copyAssertionInfo(AssertionInfo *target, AssertionInfo *source);
+static DmlOp oppositeDmlOp(DmlOp operation);
+static RowCompareType oppositeCompareType(RowCompareType type);
+static DmlOp labelForComparisonWithAggFuncs(DmlOp label, RowCompareType compOp, int aggFuncs);
+static int16 triggerOnEvents(AssertionInfo *info, Oid relationId);
+static List *triggerOnColumns(AssertionInfo *info, Oid relationId);
+static bool listContainsObjectAddress(List *list, ObjectAddress *address);
+static Bitmapset * strategiesForOperators(List *operators);
+static int functionsForTargetList(List *targetList);
+static int funcMaskForFuncOid(Oid funcOid);
+static Query * queryForSQLFunction(FuncExpr *funcExpr);
+
+/* Expression visiting */
+static bool visitAllNodes(Node *node, AssertionInfo *info);
+static bool visitRangeTblRef(RangeTblRef *node, AssertionInfo *info);
+static bool visitQuery(Query *node, AssertionInfo *info);
+static bool visitSetOperationStmt(SetOperationStmt *node, AssertionInfo *info);
+static bool visitBoolExpr(BoolExpr *node, AssertionInfo *info);
+static bool visitSubLink(SubLink *node, AssertionInfo *info);
+static bool visitFuncExpr(FuncExpr *node, AssertionInfo *info);
+static bool visitExpr(Expr *node, AssertionInfo *info);
+static bool visitVar(Var *node, AssertionInfo *info);
+
+/* TargetList visiting */
+static bool visitAggrefNodes(Node *node, int *aggFuncs);
+static bool visitAggref(Aggref *node, int *aggFuncs);
+static bool visitWindowFunc(WindowFunc *node, int *aggFuncs);
+
+
+static void
+initAssertionInfo(AssertionInfo *info)
+{
+	info->label = DELETE;
+	info->expr = NULL;
+	info->rtable = NIL;
+	info->operators = NIL;
+	info->setOp = SETOP_NONE;
+	info->invert = false;
+	info->inView = false;
+	info->inTargetEntry = false;
+	info->inComparison = false;
+	info->inExists = false;
+
+	info->dependencies = NIL;
+	info->relations = NIL;
+	info->inserts = NIL;
+	info->deletes = NIL;
+	info->updates = NIL;
+	info->columns = NIL;
+}
+
+
+static void
+copyAssertionInfo(AssertionInfo *target, AssertionInfo *source)
+{
+	target->label = source->label;
+	target->expr = source->expr;
+	target->rtable = source->rtable;
+	target->operators = source->operators;
+	target->setOp = source->setOp;
+	target->invert = source->invert;
+	target->inView = source->inView;
+	target->inTargetEntry = source->inTargetEntry;
+	target->inComparison = source->inComparison;
+	target->inExists = source->inExists;
+}
+
+
+static DmlOp
+oppositeDmlOp(DmlOp operation)
+{
+	switch (operation)
+	{
+		case INSERT:
+			return DELETE;
+		case DELETE:
+			return INSERT;
+		case INSERT_DELETE:
+			return INSERT_DELETE;
+	}
+}
+
+
+static RowCompareType
+oppositeCompareType(RowCompareType type)
+{
+	switch (type)
+	{
+		case ROWCOMPARE_LE:
+			return ROWCOMPARE_GE;
+		case ROWCOMPARE_LT:
+			return ROWCOMPARE_GT;
+		case ROWCOMPARE_GT:
+			return ROWCOMPARE_LT;
+		case ROWCOMPARE_GE:
+			return ROWCOMPARE_LE;
+		default:
+			return type; /* we do not flip EQ and NE comparisons */
+	}
+}
+
+
+static DmlOp
+labelForComparisonWithAggFuncs(DmlOp label, RowCompareType compOp, int aggFuncs)
+{
+	int stronger, weaker;
+
+	switch (compOp)
+	{
+		case ROWCOMPARE_LT:
+		case ROWCOMPARE_LE:
+			stronger = MIN_AGG_FUNC;
+			weaker = MAX_AGG_FUNC | COUNT_AGG_FUNC;
+			break;
+
+		case ROWCOMPARE_EQ:
+			stronger = BOOL_AND_AGG_FUNC | EVERY_AGG_FUNC;
+			weaker = BOOL_OR_AGG_FUNC;
+			break;
+
+		case ROWCOMPARE_NE:
+			stronger = BOOL_AND_AGG_FUNC | EVERY_AGG_FUNC;
+			weaker = BOOL_OR_AGG_FUNC;
+			break;
+
+		case ROWCOMPARE_GE:
+		case ROWCOMPARE_GT:
+			stronger = MAX_AGG_FUNC | COUNT_AGG_FUNC;
+			weaker = MIN_AGG_FUNC;
+			break;
+
+		default:
+			stronger = weaker = NO_FUNC;
+	}
+
+	if (MatchesOnly(aggFuncs, weaker))
+		return label;
+	else if (MatchesOnly(aggFuncs, stronger))
+		return oppositeDmlOp(label);
+	else
+		return INSERT_DELETE;
+}
+
+
+static int16
+triggerOnEvents(AssertionInfo *info, Oid relationId)
+{
+	int16 result = 0;
+	if (list_member_oid(info->inserts, relationId))
+		result |= TRIGGER_TYPE_INSERT;
+	if (list_member_oid(info->deletes, relationId))
+		result |= TRIGGER_TYPE_DELETE | TRIGGER_TYPE_TRUNCATE;
+	if (list_member_oid(info->updates, relationId))
+		result |= TRIGGER_TYPE_UPDATE;
+	return result;
+}
+
+
+/*
+ * Returns a list of string nodes, suitable for use in the trigger
+ * definition, that contain the column names to be triggered against
+ * on UPDATE operations.
+ */
+static List *
+triggerOnColumns(AssertionInfo *info, Oid relationId)
+{
+	List     	  *columns = NIL;
+	ListCell 	  *cell;
+
+	foreach(cell, info->columns)
+	{
+		ObjectAddress *column = (ObjectAddress *) lfirst(cell);
+		AttrNumber attrNumber = (AttrNumber) column->objectSubId;
+
+		if (column->objectId == relationId)
+		{
+			Value *name = makeString(get_attname(relationId, attrNumber, false));
+			columns = lappend(columns, name);
+		}
+	}
+
+	return columns;
+}
+
+
+static bool
+listContainsObjectAddress(List *list, ObjectAddress *address)
+{
+	ListCell 	  *cell;
+	ObjectAddress *item;
+
+	foreach(cell, list)
+	{
+		item = (ObjectAddress *) lfirst(cell);
+		if (item->classId == address->classId &&
+			item->objectId == address->objectId &&
+			item->objectSubId == address->objectSubId)
+			return true;
+	}
+
+	return false;
+}
+
+
+static Bitmapset *
+strategiesForOperators(List *operators)
+{
+	ListCell  *operatorCell;
+	Bitmapset *strategies = NULL;
+
+	foreach(operatorCell, operators)
+	{
+		Oid operator = lfirst_oid(operatorCell);
+		List *interpretations = get_op_btree_interpretation(operator);
+		ListCell *interpretationCell;
+
+		foreach(interpretationCell, interpretations)
+			strategies = bms_add_member(strategies,
+										((OpBtreeInterpretation *) lfirst(interpretationCell))->strategy);
+	}
+
+	return strategies;
+}
+
+
+static int
+functionsForTargetList(List *targetList)
+{
+	int functionMask = NO_FUNC;
+	expression_tree_walker((Node *) targetList, visitAggrefNodes, &functionMask);
+	return functionMask;
+}
+
+
+static int
+funcMaskForFuncOid(Oid funcOid)
+{
+	char *name = get_func_name(funcOid);
+
+	if (name == NULL)
+		return OTHER_FUNC;
+	else if (strncmp(name, "min", strlen("min")) == 0)
+		return MIN_AGG_FUNC;
+	else if (strncmp(name, "max", strlen("max")) == 0)
+		return MAX_AGG_FUNC;
+	else if (strncmp(name, "count", strlen("count")) == 0)
+		return COUNT_AGG_FUNC;
+	else if (strncmp(name, "every", strlen("every")) == 0)
+		return EVERY_AGG_FUNC;
+	else if (strncmp(name, "bool_and", strlen("bool_and")) == 0)
+		return BOOL_AND_AGG_FUNC;
+	else if (strncmp(name, "bool_or", strlen("bool_or")) == 0)
+		return BOOL_OR_AGG_FUNC;
+	else
+		return OTHER_FUNC;
+}
+
+
+static Query *
+queryForSQLFunction(FuncExpr *funcExpr)
+{
+	Oid			funcId;
+	Relation 	procRel;
+	HeapTuple	tuple;
+	Datum		datum;
+	bool		isNull;
+	char	   *sql;
+	List	   *rawParseTree;
+	ParseState *pstate;
+	Query	   *query;
+	SQLFunctionParseInfoPtr pinfo;
+
+	funcId = funcExpr->funcid;
+	procRel = heap_open(ProcedureRelationId, ShareLock);
+
+	tuple = SearchSysCache1(PROCOID, ObjectIdGetDatum(funcId));
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for function %u", funcId);
+
+	datum = SysCacheGetAttr(PROCOID,
+							tuple,
+							Anum_pg_proc_prosrc,
+							&isNull);
+	if (isNull)
+		elog(ERROR, "null prosrc for function %u", funcId);
+
+	sql = TextDatumGetCString(datum);
+	rawParseTree = pg_parse_query(sql);
+
+	pinfo = prepare_sql_fn_parse_info(tuple,
+									  (Node *) funcExpr,
+									  funcExpr->inputcollid);
+
+	pstate = make_parsestate(NULL);
+	pstate->p_sourcetext = sql;
+	sql_fn_parser_setup(pstate, pinfo);
+
+	query = transformTopLevelStmt(pstate, linitial(rawParseTree));
+	free_parsestate(pstate);
+
+	ReleaseSysCache(tuple);
+	heap_close(procRel, NoLock);
+
+	return query;
+}
+
+
+static bool
+visitRangeTblRef(RangeTblRef *node, AssertionInfo *info)
+{
+	RangeTblEntry  *entry;
+	Oid				relationId;
+	RTEKind			rtekind;
+	char 		    relkind;
+	bool			result;
+
+	entry = rt_fetch(node->rtindex, info->rtable);
+	rtekind = entry->rtekind;
+	result = false;
+
+	if (rtekind == RTE_RELATION)
+	{
+		relationId = getrelid(node->rtindex, info->rtable);
+		relkind = get_rel_relkind(relationId);
+
+		if (relkind == RELKIND_RELATION)
+		{
+			if (info->label == INSERT || info->label == INSERT_DELETE)
+				info->inserts = list_append_unique_oid(info->inserts, relationId);
+
+			if (info->label == DELETE || info->label == INSERT_DELETE)
+				info->deletes = list_append_unique_oid(info->deletes, relationId);
+
+			if (!info->inView)
+				info->dependencies = list_append_unique_oid(info->dependencies, relationId);
+
+			info->relations = list_append_unique_oid(info->relations, relationId);
+
+		}
+		else if (relkind == RELKIND_VIEW)
+		{
+			Relation view = heap_open(relationId, AccessShareLock);
+			Query *query = get_view_query(view);
+
+			if (!info->inView)
+				info->dependencies = list_append_unique_oid(info->dependencies, relationId);
+
+			info->inView = true;
+			result = visitQuery(query, info);
+
+			heap_close(view, NoLock);
+		}
+
+	}
+	else if (rtekind == RTE_TABLEFUNC)
+	{
+		result = visitAllNodes((Node *) entry->tablefunc, info);
+	}
+	else if (rtekind == RTE_FUNCTION)
+	{
+		result = visitAllNodes((Node *) entry->functions, info);
+	}
+	else if (rtekind == RTE_SUBQUERY)
+	{
+		result = visitQuery(entry->subquery, info);
+	}
+
+	return result;
+}
+
+
+static bool
+visitQuery(Query *node, AssertionInfo *info)
+{
+	info->rtable = node->rtable;
+
+	if (info->inComparison)
+	{
+		int functions = functionsForTargetList(node->targetList);
+		Bitmapset *strategies = strategiesForOperators(info->operators);
+
+		if (CanOptimise(strategies))
+		{
+			RowCompareType rowCompareType = (RowCompareType) bms_first_member(strategies);
+			info->label = labelForComparisonWithAggFuncs(
+				info->label,
+				info->invert ? oppositeCompareType(rowCompareType) : rowCompareType,
+				functions
+			);
+		}
+		else
+		{
+			/*
+			 * Either no btree interpretation was found for the operator(s), there were
+			 * multiple interpretations that were incompatible with each other, or the
+			 * found interpretations were not able to be optimised. We must therefore
+			 * assume that both INSERT and DELETE operations may be invalidating.
+			 */
+			info->label = INSERT_DELETE;
+		}
+	}
+	else if (node->hasWindowFuncs)
+	{
+		info->label = INSERT_DELETE;
+	}
+
+	return query_tree_walker(node,
+							 visitAllNodes,
+							 info,
+							 QTW_IGNORE_RANGE_TABLE);
+}
+
+
+static bool
+visitSetOperationStmt(SetOperationStmt *node, AssertionInfo *info)
+{
+	info->setOp = node->op;
+
+	if (visitAllNodes(node->larg, info))
+		return true;
+
+	if (info->setOp == SETOP_EXCEPT)
+		info->label = oppositeDmlOp(info->label);
+
+	return visitAllNodes(node->rarg, info);
+}
+
+
+static bool
+visitBoolExpr(BoolExpr *node, AssertionInfo *info)
+{
+	if (node->boolop == NOT_EXPR)
+		info->label = oppositeDmlOp(info->label);
+
+	return expression_tree_walker((Node *) node, visitAllNodes, (void *) info);
+}
+
+
+// TODO refactor this function
+static bool
+visitSubLink(SubLink *node, AssertionInfo *info)
+{
+	List *operators;
+	bool  invert;
+	bool  inComparison;
+	bool  inExists;
+
+	switch (node->subLinkType)
+	{
+		case ARRAY_SUBLINK: // TODO write tests for this
+		case EXPR_SUBLINK:
+		{
+			Expr *expr = info->expr;
+			bool hasExpr = (expr != NULL);
+
+			if (hasExpr && IsA(expr, OpExpr))
+			{
+				OpExpr *opExpr = (OpExpr *) expr;
+				inComparison = true;
+				inExists = false;
+
+				/*
+				 * Optimisation code is written under the assumption that the sub-select is the
+				 * right operand. If it is the left operand the comparison needs to be inverted.
+				 */
+				invert = (list_nth_node(SubLink, opExpr->args, 0) == node);
+				operators = list_make1_oid(opExpr->opno);
+			}
+			else if (hasExpr && IsA(expr, FuncExpr) &&
+					((FuncExpr *)expr)->funcresulttype == BOOLOID)
+			{
+				/*
+				 * We are inside a function invocation that returns Boolean but is not an OpExpr.
+				 * Let's exploit the fact that "expr == TRUE -> expr", and pretend there is an
+				 * equality operator.
+				 */
+				inComparison = true;
+				inExists = false;
+				operators = list_make1_oid(F_BOOLEQ);
+				invert = false;
+			}
+			else if (exprType((const Node *) node) == BOOLOID)
+			{
+				/*
+				 * We are _not_ inside either an OpExpr or FuncExpr, but we are a query that can be
+				 * coerced to Boolean. We use the same logic above e.g. "expr == TRUE -> expr"
+				 */
+				inComparison = true;
+				inExists = false;
+				operators = list_make1_oid(F_BOOLEQ);
+				invert = false;
+			}
+			else
+			{
+				inComparison = false;
+				inExists = false;
+				invert = false;
+				operators = NIL;
+			}
+		}
+		break;
+
+		case ALL_SUBLINK:
+		case ANY_SUBLINK:
+		case ROWCOMPARE_SUBLINK:
+		{
+			if (IsA(node->testexpr, OpExpr))
+			{
+				OpExpr *expr = (OpExpr *) node->testexpr;
+				inComparison = true;
+				inExists = false;
+				invert = false;
+				operators = list_make1_oid(expr->opno);
+			}
+			else if (IsA(node->testexpr, RowCompareExpr))
+			{
+				RowCompareExpr *expr = (RowCompareExpr *) node->testexpr;
+				inComparison = true;
+				inExists = false;
+				invert = false;
+				operators = list_copy(expr->opnos);
+			}
+			else
+			{
+				inComparison = false;
+				inExists = false;
+				invert = false;
+				operators = NIL;
+			}
+		}
+		break;
+
+		case EXISTS_SUBLINK:
+		{
+			/* existential quantification, no operators */
+			inComparison = false;
+			inExists = true;
+			invert = false;
+			operators = NIL;
+		}
+		break;
+			// case MULTIEXPR_SUBLINK: // TODO write tests for MULTIEXPR_SUBLINK? -- or, can it only occur in UPDATEs?
+		default:
+		{
+			elog(ERROR, "unhandled sublink type %u", node->subLinkType);
+			return true;
+		}
+
+	}
+
+	info->operators = operators;
+	info->invert = invert;
+	info->inComparison = inComparison;
+	info->inExists = inExists;
+	info->setOp = SETOP_NONE;
+
+	return expression_tree_walker((Node *) node, visitAllNodes, (void *) info);
+}
+
+
+static bool
+visitTargetEntry(TargetEntry *node, AssertionInfo *info)
+{
+	info->inTargetEntry = true;
+	return expression_tree_walker((Node *) node, visitAllNodes, (void *) info);
+}
+
+
+static bool
+visitFromExpr(FromExpr *node, AssertionInfo *info)
+{
+	info->inExists = false;
+	info->inComparison = false;
+	return expression_tree_walker((Node *) node, visitAllNodes, (void *) info);
+}
+
+
+static bool
+visitFuncExpr(FuncExpr *node, AssertionInfo *info)
+{
+	Oid lang = get_func_lang(node->funcid);
+
+	info->expr = (Expr *) node;
+
+	if (!(lang == INTERNALlanguageId || lang == SQLlanguageId))
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_AMBIGUOUS_FUNCTION),
+				 errmsg("function \"%s\" uses unsupported language \"%s\"",
+				   get_func_name(node->funcid), get_language_name(lang, false))));
+		return true;
+	}
+
+	if (expression_tree_walker((Node *) node, visitAllNodes, (void *) info))
+		return true;
+
+	if (lang == SQLlanguageId)
+	{
+		Query *query = queryForSQLFunction(node);
+		Node *next;
+		if (node->funcretset)
+			next = (Node *) query;
+		else
+			next = (Node *) ((TargetEntry *) linitial(query->targetList))->expr;
+
+		return visitAllNodes(next, info);
+	}
+
+	return false;
+}
+
+
+static bool
+visitExpr(Expr *node, AssertionInfo *info)
+{
+	info->expr = node;
+	return expression_tree_walker((Node *) node, visitAllNodes, (void *) info);
+}
+
+
+static bool
+visitVar(Var *node, AssertionInfo *info)
+{
+	RangeTblEntry *entry = rt_fetch(node->varno, info->rtable);
+
+	// TODO we should do this only for Tables and not Views?
+	// TODO optimisations for set-operations
+	if (entry->rtekind == RTE_RELATION && !(info->inExists && info->setOp == SETOP_NONE))
+	{
+		Oid relationId = getrelid(node->varno, info->rtable);
+		ObjectAddress *column = (ObjectAddress *) palloc(sizeof(ObjectAddress));
+
+		column->classId = RelationRelationId;
+		column->objectId = relationId;
+		column->objectSubId = node->varattno;
+
+		if (!listContainsObjectAddress(info->columns, column))
+			info->columns = lcons(column, info->columns);
+
+		info->relations = list_append_unique_oid(info->relations, relationId);
+		info->updates = list_append_unique_oid(info->updates, relationId);
+	}
+
+	return expression_tree_walker((Node *) node, visitAllNodes, (void *) info);
+}
+
+
+static bool
+visitAllNodes(Node *node, AssertionInfo *info)
+{
+	AssertionInfo stored;
+	bool result;
+
+	copyAssertionInfo(&stored, info);
+
+	if (node == NULL)
+		result = false;
+	else if (IsA(node, RangeTblRef))
+		result = visitRangeTblRef((RangeTblRef *) node, info);
+	else if (IsA(node, Query))
+		result = visitQuery((Query *) node, info);
+	else if (IsA(node, BoolExpr))
+		result = visitBoolExpr((BoolExpr *) node, info);
+	else if (IsA(node, SubLink))
+		result = visitSubLink((SubLink *) node, info);
+	else if (IsA(node, SetOperationStmt))
+		result = visitSetOperationStmt((SetOperationStmt *) node, info);
+	else if (IsA(node, FuncExpr))
+		result = visitFuncExpr((FuncExpr *) node, info);
+	else if (IsA(node, OpExpr) ||
+			 IsA(node, DistinctExpr) ||
+			 IsA(node, NullIfExpr))
+		result = visitExpr((Expr *) node, info);
+	else if (IsA(node, TargetEntry))
+		result = visitTargetEntry((TargetEntry *) node, info);
+	else if (IsA(node, Var))
+		result = visitVar((Var *) node, info);
+	else if (IsA(node, FromExpr))
+		result = visitFromExpr((FromExpr *) node, info);
+	else
+		result = expression_tree_walker(node, visitAllNodes, (void *) info);
+
+	copyAssertionInfo(info, &stored);
+
+	return result;
+}
+
+
+static bool
+visitAggrefNodes(Node *node, int *aggFuncs)
+{
+	if (node == NULL)
+		return false;
+	else if (IsA(node, Aggref))
+		return visitAggref((Aggref *) node, aggFuncs);
+	else if(IsA(node, WindowFunc))
+		return visitWindowFunc((WindowFunc *) node, aggFuncs);
+	else
+		return expression_tree_walker(node, visitAggrefNodes, (void *) aggFuncs);
+}
+
+
+static bool
+visitAggref(Aggref *node, int *aggFuncs)
+{
+	*aggFuncs |= funcMaskForFuncOid(node->aggfnoid);
+	return expression_tree_walker((Node *) node, visitAggrefNodes, (void *) aggFuncs);
+}
+
+
+static bool
+visitWindowFunc(WindowFunc *node, int *aggFuncs)
+{
+	*aggFuncs |= funcMaskForFuncOid(node->winfnoid);
+
+	return expression_tree_walker((Node *) node, visitAggrefNodes, (void *) aggFuncs);
+}
+
+
+ObjectAddress
+CreateAssertion(CreateAssertionStmt *stmt)
+{
+	Oid namespaceId;
+	char *assertion_name;
+	AclResult aclresult;
+	Node *expr;
+	ParseState *pstate;
+	char *ccsrc;
+	char *ccbin;
+	Oid constrOid;
+	AssertionInfo info;
+	ListCell *lc;
+	ObjectAddress address;
+
+	namespaceId = QualifiedNameGetCreationNamespace(stmt->assertion_name,
+													&assertion_name);
+
+	aclresult = pg_namespace_aclcheck(namespaceId, GetUserId(), ACL_CREATE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, OBJECT_SCHEMA,
+					   get_namespace_name(namespaceId));
+
+	if (ConstraintNameIsUsed(CONSTRAINT_ASSERTION, InvalidOid, namespaceId, assertion_name))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_NAME),
+				 errmsg("assertion \"%s\" already exists", assertion_name)));
+
+	pstate = make_parsestate(NULL);
+	expr = transformExpr(pstate, stmt->constraint->raw_expr, EXPR_KIND_ASSERTION_CHECK);
+	expr = coerce_to_boolean(pstate, expr, "CHECK");
+
+	ccbin = nodeToString(expr);
+	ccsrc = deparse_expression(expr, NIL, false, false);
+
+	constrOid = CreateConstraintEntry(assertion_name,
+									  namespaceId,
+									  CONSTRAINT_CHECK, /* constraint type */
+									  stmt->constraint->deferrable,
+									  stmt->constraint->initdeferred,
+									  !stmt->constraint->skip_validation,
+									  InvalidOid, /* not a relation constraint */
+									  NULL,          /* no keys */
+									  0,          /* no keys */
+									  InvalidOid, /* not a domain constraint */
+									  InvalidOid, /* no associated index */
+									  InvalidOid, /* foreign key fields ... */
+									  NULL,
+									  NULL,
+									  NULL,
+									  NULL,
+									  0,
+									  ' ',
+									  ' ',
+									  ' ',
+									  NULL,    /* not an exclusion constraint */
+									  expr, /* tree form of check constraint */
+									  ccbin, /* binary form of check constraint */
+									  ccsrc, /* source form of check constraint */
+									  true, /* is local */
+									  0,   /* inhcount */
+									  false, /* noinherit XXX */
+									  false); /* is_internal */
+
+	initAssertionInfo(&info);
+	visitAllNodes(expr, &info);
+
+	foreach (lc, info.relations)
+	{
+		Oid relationId = lfirst_oid(lc);
+		CreateTrigStmt *trigger;
+		Relation rel;
+
+		rel = heap_open(relationId, ShareLock); // XXX
+
+		trigger = makeNode(CreateTrigStmt);
+		trigger->trigname = "AssertionTrigger";
+		trigger->relation = makeRangeVar(get_namespace_name(namespaceId),
+										 pstrdup(RelationGetRelationName(rel)),
+										 -1);
+		trigger->funcname = SystemFuncName("assertion_check");
+		trigger->args = NIL;
+		trigger->row = false;
+		trigger->timing = TRIGGER_TYPE_AFTER;
+		trigger->events = triggerOnEvents(&info, relationId);
+		trigger->columns = triggerOnColumns(&info, relationId);
+		trigger->whenClause = NULL;
+		trigger->isconstraint = true;
+		trigger->deferrable = stmt->constraint->deferrable;
+		trigger->initdeferred = stmt->constraint->initdeferred;
+		trigger->constrrel = NULL;
+
+		CreateTrigger(trigger, NULL, InvalidOid, relationId, constrOid, InvalidOid, true);
+
+		heap_close(rel, NoLock);
+	}
+
+	/*
+	 * Record dependencies between the constraint and the relations found in the
+	 * top-level expression. Dependencies to specific columns will already have
+	 * been recorded by the trigger creation.
+	 */
+	ObjectAddress myself, referenced;
+	ListCell *cell;
+
+	myself.classId = ConstraintRelationId;
+	myself.objectId = constrOid;
+	myself.objectSubId = 0;
+
+	foreach (cell, info.dependencies)
+	{
+		referenced.classId = RelationRelationId;
+		referenced.objectId = lfirst_oid(cell);
+		referenced.objectSubId = 0;
+		recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+	}
+
+	test_assertion_expr(assertion_name, ccsrc);
+
+	ObjectAddressSet(address, ConstraintRelationId, constrOid);
+
+	return address;
+}
+
+
+ObjectAddress
+RenameAssertion(RenameStmt *stmt)
+{
+	Oid           assertionOid;
+	ObjectAddress address;
+	Relation      rel;
+	HeapTuple     tuple;
+	Form_pg_constraint con;
+	AclResult     aclresult;
+
+	List *oldName = castNode(List, stmt->object);
+	char *newName = stmt->newname;
+
+	rel = heap_open(ConstraintRelationId, RowExclusiveLock);
+	assertionOid = get_assertion_oid(oldName, false);
+
+	tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(assertionOid));
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for constraint %u",
+			 assertionOid);
+	con = (Form_pg_constraint) GETSTRUCT(tuple);
+
+	if (!pg_constraint_ownercheck(assertionOid, GetUserId()))
+		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_CONVERSION,
+					   NameListToString(oldName));
+
+	aclresult = pg_namespace_aclcheck(con->connamespace, GetUserId(), ACL_CREATE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, OBJECT_SCHEMA,
+					   get_namespace_name(con->connamespace));
+
+	ReleaseSysCache(tuple);
+	RenameConstraintById(assertionOid, newName);
+	ObjectAddressSet(address, ConstraintRelationId, assertionOid);
+	heap_close(rel, NoLock);
+
+	return address;
+}
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index 549c7ea51d..95e6a6687d 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -88,6 +88,7 @@ typedef enum
 static event_trigger_support_data event_trigger_support[] = {
 	{"ACCESS METHOD", true},
 	{"AGGREGATE", true},
+	{"ASSERTION", true},
 	{"CAST", true},
 	{"CONSTRAINT", true},
 	{"COLLATION", true},
@@ -1081,6 +1082,7 @@ EventTriggerSupportsObjectType(ObjectType obtype)
 			return false;
 		case OBJECT_ACCESS_METHOD:
 		case OBJECT_AGGREGATE:
+		case OBJECT_ASSERTION:
 		case OBJECT_AMOP:
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 3ad4da64aa..0bbb8bb87a 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -4320,6 +4320,17 @@ _copyCreateSchemaStmt(const CreateSchemaStmt *from)
 	return newnode;
 }
 
+static CreateAssertionStmt *
+_copyCreateAssertionStmt(const CreateAssertionStmt *from)
+{
+	CreateAssertionStmt *newnode = makeNode(CreateAssertionStmt);
+
+	COPY_NODE_FIELD(assertion_name);
+	COPY_NODE_FIELD(constraint);
+
+	return newnode;
+}
+
 static CreateConversionStmt *
 _copyCreateConversionStmt(const CreateConversionStmt *from)
 {
@@ -5389,6 +5400,9 @@ copyObjectImpl(const void *from)
 		case T_CreateSchemaStmt:
 			retval = _copyCreateSchemaStmt(from);
 			break;
+		case T_CreateAssertionStmt:
+			retval = _copyCreateAssertionStmt(from);
+			break;
 		case T_CreateConversionStmt:
 			retval = _copyCreateConversionStmt(from);
 			break;
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 765b1be74b..9ac7971b0c 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -2121,6 +2121,15 @@ _equalCreateSchemaStmt(const CreateSchemaStmt *a, const CreateSchemaStmt *b)
 	return true;
 }
 
+static bool
+_equalCreateAssertionStmt(const CreateAssertionStmt *a, const CreateAssertionStmt *b)
+{
+	COMPARE_NODE_FIELD(assertion_name);
+	COMPARE_NODE_FIELD(constraint);
+
+	return true;
+}
+
 static bool
 _equalCreateConversionStmt(const CreateConversionStmt *a, const CreateConversionStmt *b)
 {
@@ -3521,6 +3530,9 @@ equal(const void *a, const void *b)
 		case T_CreateSchemaStmt:
 			retval = _equalCreateSchemaStmt(a, b);
 			break;
+		case T_CreateAssertionStmt:
+			retval = _equalCreateAssertionStmt(a, b);
+			break;
 		case T_CreateConversionStmt:
 			retval = _equalCreateConversionStmt(a, b);
 			break;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index cd5ba2d4d8..22926ff03a 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -259,11 +259,11 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 		CreateOpFamilyStmt AlterOpFamilyStmt CreatePLangStmt
 		CreateSchemaStmt CreateSeqStmt CreateStmt CreateStatsStmt CreateTableSpaceStmt
 		CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt
-		CreateAssertStmt CreateTransformStmt CreateTrigStmt CreateEventTrigStmt
+		CreateAssertionStmt CreateTransformStmt CreateTrigStmt CreateEventTrigStmt
 		CreateUserStmt CreateUserMappingStmt CreateRoleStmt CreatePolicyStmt
 		CreatedbStmt DeclareCursorStmt DefineStmt DeleteStmt DiscardStmt DoStmt
 		DropOpClassStmt DropOpFamilyStmt DropPLangStmt DropStmt
-		DropAssertStmt DropCastStmt DropRoleStmt
+		DropCastStmt DropRoleStmt
 		DropdbStmt DropTableSpaceStmt
 		DropTransformStmt
 		DropUserMappingStmt ExplainStmt FetchStmt
@@ -859,7 +859,7 @@ stmt :
 			| CopyStmt
 			| CreateAmStmt
 			| CreateAsStmt
-			| CreateAssertStmt
+			| CreateAssertionStmt
 			| CreateCastStmt
 			| CreateConversionStmt
 			| CreateDomainStmt
@@ -895,7 +895,6 @@ stmt :
 			| DeleteStmt
 			| DiscardStmt
 			| DoStmt
-			| DropAssertStmt
 			| DropCastStmt
 			| DropOpClassStmt
 			| DropOpFamilyStmt
@@ -5624,45 +5623,28 @@ enable_trigger:
  *
  *		QUERIES :
  *				CREATE ASSERTION ...
- *				DROP ASSERTION ...
  *
  *****************************************************************************/
 
-CreateAssertStmt:
-			CREATE ASSERTION name CHECK '(' a_expr ')'
+CreateAssertionStmt:
+			CREATE ASSERTION any_name CHECK '(' a_expr ')'
 			ConstraintAttributeSpec
 				{
-					CreateTrigStmt *n = makeNode(CreateTrigStmt);
-					n->trigname = $3;
-					n->args = list_make1($6);
-					n->isconstraint  = true;
-					processCASbits($8, @8, "ASSERTION",
-								   &n->deferrable, &n->initdeferred, NULL,
-								   NULL, yyscanner);
-
-					ereport(ERROR,
-							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-							 errmsg("CREATE ASSERTION is not yet implemented")));
-
-					$$ = (Node *)n;
-				}
-		;
-
-DropAssertStmt:
-			DROP ASSERTION name opt_drop_behavior
-				{
-					DropStmt *n = makeNode(DropStmt);
-					n->objects = NIL;
-					n->behavior = $4;
-					n->removeType = OBJECT_TRIGGER; /* XXX */
-					ereport(ERROR,
-							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-							 errmsg("DROP ASSERTION is not yet implemented")));
-					$$ = (Node *) n;
+					CreateAssertionStmt *n = makeNode(CreateAssertionStmt);
+					Constraint *c = makeNode(Constraint);
+					c->contype = CONSTR_CHECK;
+					c->location = @4;
+					c->raw_expr = $6;
+					c->cooked_expr = NULL;
+ 					processCASbits($8, @8, "ASSERTION",
+								   &c->deferrable, &c->initdeferred, NULL,
+ 								   NULL, yyscanner);
+					n->assertion_name = $3;
+					n->constraint = c;
+ 					$$ = (Node *)n;
 				}
 		;
 
-
 /*****************************************************************************
  *
  *		QUERY :
@@ -6316,6 +6298,7 @@ drop_type_any_name:
 			| MATERIALIZED VIEW						{ $$ = OBJECT_MATVIEW; }
 			| INDEX									{ $$ = OBJECT_INDEX; }
 			| FOREIGN TABLE							{ $$ = OBJECT_FOREIGN_TABLE; }
+			| ASSERTION								{ $$ = OBJECT_ASSERTION; }
 			| COLLATION								{ $$ = OBJECT_COLLATION; }
 			| CONVERSION_P							{ $$ = OBJECT_CONVERSION; }
 			| STATISTICS							{ $$ = OBJECT_STATISTIC_EXT; }
@@ -6585,6 +6568,7 @@ comment_type_any_name:
 			| TABLE								{ $$ = OBJECT_TABLE; }
 			| VIEW								{ $$ = OBJECT_VIEW; }
 			| MATERIALIZED VIEW					{ $$ = OBJECT_MATVIEW; }
+			| ASSERTION							{ $$ = OBJECT_ASSERTION; }
 			| COLLATION							{ $$ = OBJECT_COLLATION; }
 			| CONVERSION_P						{ $$ = OBJECT_CONVERSION; }
 			| FOREIGN TABLE						{ $$ = OBJECT_FOREIGN_TABLE; }
@@ -8437,6 +8421,15 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name
 					n->missing_ok = false;
 					$$ = (Node *)n;
 				}
+			| ALTER ASSERTION any_name RENAME TO name
+				{
+					RenameStmt *n = makeNode(RenameStmt);
+					n->renameType = OBJECT_ASSERTION;
+					n->object = (Node *) $3;
+					n->newname = $6;
+					n->missing_ok = false;
+					$$ = (Node *)n;
+				}
 			| ALTER COLLATION any_name RENAME TO name
 				{
 					RenameStmt *n = makeNode(RenameStmt);
@@ -9021,6 +9014,15 @@ AlterObjectSchemaStmt:
 					n->missing_ok = false;
 					$$ = (Node *)n;
 				}
+			| ALTER ASSERTION any_name SET SCHEMA name
+				{
+					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
+					n->objectType = OBJECT_ASSERTION;
+					n->object = (Node *) $3;
+					n->newschema = $6;
+					n->missing_ok = false;
+					$$ = (Node *)n;
+				}
 			| ALTER COLLATION any_name SET SCHEMA name
 				{
 					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
@@ -9306,6 +9308,14 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
 					n->newowner = $6;
 					$$ = (Node *)n;
 				}
+			| ALTER ASSERTION any_name OWNER TO RoleSpec
+				{
+					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
+					n->objectType = OBJECT_ASSERTION;
+					n->object = (Node *) $3;
+					n->newowner = $6;
+					$$ = (Node *)n;
+				}
 			| ALTER COLLATION any_name OWNER TO RoleSpec
 				{
 					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c
index 377a7ed6d0..223cea1134 100644
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -457,6 +457,7 @@ check_agglevels_and_constraints(ParseState *pstate, Node *expr)
 			break;
 		case EXPR_KIND_CHECK_CONSTRAINT:
 		case EXPR_KIND_DOMAIN_CHECK:
+		case EXPR_KIND_ASSERTION_CHECK:
 			if (isAgg)
 				err = _("aggregate functions are not allowed in check constraints");
 			else
@@ -875,6 +876,7 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
 			break;
 		case EXPR_KIND_CHECK_CONSTRAINT:
 		case EXPR_KIND_DOMAIN_CHECK:
+		case EXPR_KIND_ASSERTION_CHECK:
 			err = _("window functions are not allowed in check constraints");
 			break;
 		case EXPR_KIND_COLUMN_DEFAULT:
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 385e54a9b6..1933a615bf 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -1818,6 +1818,7 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
 		case EXPR_KIND_RETURNING:
 		case EXPR_KIND_VALUES:
 		case EXPR_KIND_VALUES_SINGLE:
+		case EXPR_KIND_ASSERTION_CHECK:
 			/* okay */
 			break;
 		case EXPR_KIND_CHECK_CONSTRAINT:
@@ -3457,6 +3458,7 @@ ParseExprKindName(ParseExprKind exprKind)
 			return "VALUES";
 		case EXPR_KIND_CHECK_CONSTRAINT:
 		case EXPR_KIND_DOMAIN_CHECK:
+		case EXPR_KIND_ASSERTION_CHECK:
 			return "CHECK";
 		case EXPR_KIND_COLUMN_DEFAULT:
 		case EXPR_KIND_FUNCTION_DEFAULT:
diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c
index ea5d5212b4..8193b7da37 100644
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -2277,6 +2277,10 @@ check_srf_call_placement(ParseState *pstate, Node *last_srf, int location)
 			/* okay, since we process this like a SELECT tlist */
 			pstate->p_hasTargetSRFs = true;
 			break;
+		case EXPR_KIND_ASSERTION_CHECK:
+			/* okay */
+			pstate->p_hasTargetSRFs = true;
+			break;
 		case EXPR_KIND_CHECK_CONSTRAINT:
 		case EXPR_KIND_DOMAIN_CHECK:
 			err = _("set-returning functions are not allowed in check constraints");
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index ed55521a0c..9ead34af3d 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -30,6 +30,7 @@
 #include "commands/cluster.h"
 #include "commands/comment.h"
 #include "commands/collationcmds.h"
+#include "commands/constraint.h"
 #include "commands/conversioncmds.h"
 #include "commands/copy.h"
 #include "commands/createas.h"
@@ -160,6 +161,7 @@ check_xact_readonly(Node *parsetree)
 		case T_RenameStmt:
 		case T_CommentStmt:
 		case T_DefineStmt:
+		case T_CreateAssertionStmt:
 		case T_CreateCastStmt:
 		case T_CreateEventTrigStmt:
 		case T_AlterEventTrigStmt:
@@ -1503,6 +1505,10 @@ ProcessUtilitySlow(ParseState *pstate,
 				address = DefineDomain((CreateDomainStmt *) parsetree);
 				break;
 
+			case T_CreateAssertionStmt:
+				address = CreateAssertion((CreateAssertionStmt *) parsetree);
+				break;
+
 			case T_CreateConversionStmt:
 				address = CreateConversionCommand((CreateConversionStmt *) parsetree);
 				break;
@@ -1913,6 +1919,9 @@ AlterObjectTypeCommandTag(ObjectType objtype)
 		case OBJECT_AGGREGATE:
 			tag = "ALTER AGGREGATE";
 			break;
+		case OBJECT_ASSERTION:
+			tag = "ALTER ASSERTION";
+			break;
 		case OBJECT_ATTRIBUTE:
 			tag = "ALTER TYPE";
 			break;
@@ -2251,6 +2260,9 @@ CreateCommandTag(Node *parsetree)
 				case OBJECT_DOMAIN:
 					tag = "DROP DOMAIN";
 					break;
+				case OBJECT_ASSERTION:
+					tag = "DROP ASSERTION";
+					break;
 				case OBJECT_COLLATION:
 					tag = "DROP COLLATION";
 					break;
@@ -2681,6 +2693,10 @@ CreateCommandTag(Node *parsetree)
 			tag = "REINDEX";
 			break;
 
+		case T_CreateAssertionStmt:
+			tag = "CREATE ASSERTION";
+			break;
+
 		case T_CreateConversionStmt:
 			tag = "CREATE CONVERSION";
 			break;
@@ -3281,6 +3297,10 @@ GetCommandLogLevel(Node *parsetree)
 			lev = LOGSTMT_ALL;	/* should this be DDL? */
 			break;
 
+		case T_CreateAssertionStmt:
+			lev = LOGSTMT_DDL;
+			break;
+
 		case T_CreateConversionStmt:
 			lev = LOGSTMT_DDL;
 			break;
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index bba595ad1d..6a0d6edb81 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -1542,6 +1542,25 @@ get_func_retset(Oid funcid)
 	return result;
 }
 
+/*
+ * get_func_lang
+ *		Given procedure id, return the function's language id.
+ */
+Oid
+get_func_lang(Oid funcid)
+{
+	HeapTuple	tp;
+	Oid			result;
+
+	tp = SearchSysCache1(PROCOID, ObjectIdGetDatum(funcid));
+	if (!HeapTupleIsValid(tp))
+		elog(ERROR, "cache lookup failed for function %u", funcid);
+
+	result = ((Form_pg_proc) GETSTRUCT(tp))->prolang;
+	ReleaseSysCache(tp);
+	return result;
+}
+
 /*
  * func_strict
  *		Given procedure id, return the function's proisstrict flag.
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 3560318749..2635b280de 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -785,6 +785,9 @@ exec_command_d(PsqlScanState scan_state, bool active_branch, const char *cmd)
 			case 'p':
 				success = permissionsList(pattern);
 				break;
+			case 'Q':
+				success = describeAssertions(pattern);
+				break;
 			case 'T':
 				success = describeTypes(pattern, show_verbose, show_system);
 				break;
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 0c3be1f504..762de44ff6 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -140,6 +140,66 @@ describeAggregates(const char *pattern, bool verbose, bool showSystem)
 	return true;
 }
 
+/* \dA
+ * Takes an optional regexp to select particular assertions
+ */
+bool
+describeAssertions(const char *pattern)
+{
+	PQExpBufferData buf;
+	PGresult   *res;
+	printQueryOpt myopt = pset.popt;
+
+	if (pset.sversion < 90400)
+	{
+		fprintf(stderr, _("The server (version %d.%d) does not support assertions.\n"),
+				pset.sversion / 10000, (pset.sversion / 100) % 100);
+		return true;
+	}
+
+	initPQExpBuffer(&buf);
+
+	printfPQExpBuffer(&buf,
+					  "SELECT n.nspname AS \"%s\",\n"
+							  "  c.conname AS \"%s\",\n"
+							  "  pg_catalog.pg_get_constraintdef(c.oid, true) AS \"%s\",\n"
+							  "  pg_catalog.obj_description(c.oid, 'pg_constraint') AS \"%s\"\n"
+							  "FROM pg_catalog.pg_constraint c\n"
+							  "     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.connamespace\n"
+							  "WHERE c.conrelid = 0 AND c.contypid = 0\n",
+					  gettext_noop("Schema"),
+					  gettext_noop("Name"),
+					  gettext_noop("Definition"),
+					  gettext_noop("Description"));
+
+	if (!pattern)
+		appendPQExpBuffer(&buf, "      AND n.nspname <> 'pg_catalog'\n"
+				"      AND n.nspname <> 'information_schema'\n");
+
+#if TODO
+	processSQLNamePattern(pset.db, &buf, pattern, true, false,
+						  "n.nspname", "c.conname", NULL,
+						  "pg_catalog.pg_constraint_is_visible(c.oid)");
+#endif
+
+	appendPQExpBuffer(&buf, "ORDER BY 1, 2, 3, 4;");
+
+	res = PSQLexec(buf.data);
+	termPQExpBuffer(&buf);
+	if (!res)
+		return false;
+
+	myopt.nullPrint = NULL;
+	myopt.title = _("List of assertions");
+	myopt.translate_header = true;
+
+	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
+
+	PQclear(res);
+	return true;
+}
+
+
 /*
  * \dA
  * Takes an optional regexp to select particular access methods
@@ -2518,6 +2578,38 @@ describeOneTableDetails(const char *schemaname,
 			PQclear(result);
 		}
 
+		/* print assertions referencing this table (none if no triggers) */
+		if (tableinfo.hastriggers)
+		{
+			printfPQExpBuffer(&buf,
+							  "SELECT conname,\n"
+									  "  pg_catalog.pg_get_constraintdef(c.oid, true) AS condef\n"
+									  "FROM pg_catalog.pg_constraint c\n"
+									  "WHERE c.oid IN (SELECT objid FROM pg_depend WHERE classid = 'pg_constraint'::pg_catalog.regclass AND refclassid = 'pg_class'::pg_catalog.regclass AND refobjid = '%s')\n"
+									  "  AND c.conrelid = 0 AND c.contypid = 0 AND c.contype = 'c'\n"
+									  "ORDER BY 1",
+							  oid);
+			result = PSQLexec(buf.data);
+			if (!result)
+				goto error_return;
+			else
+				tuples = PQntuples(result);
+
+			if (tuples > 0)
+			{
+				printTableAddFooter(&cont, _("Assertions:"));
+				for (i = 0; i < tuples; i++)
+				{
+					printfPQExpBuffer(&buf, "    \"%s\" %s",
+									  PQgetvalue(result, i, 0),
+									  PQgetvalue(result, i, 1));
+
+					printTableAddFooter(&cont, buf.data);
+				}
+			}
+			PQclear(result);
+		}
+
 		/* print rules */
 		if (tableinfo.hasrules && tableinfo.relkind != RELKIND_MATVIEW)
 		{
diff --git a/src/bin/psql/describe.h b/src/bin/psql/describe.h
index a4cc5efae0..7b405b65da 100644
--- a/src/bin/psql/describe.h
+++ b/src/bin/psql/describe.h
@@ -15,6 +15,9 @@ extern bool describeAggregates(const char *pattern, bool verbose, bool showSyste
 /* \dA */
 extern bool describeAccessMethods(const char *pattern, bool verbose);
 
+/* \dQ */
+extern bool describeAssertions(const char *pattern);
+
 /* \db */
 extern bool describeTablespaces(const char *pattern, bool verbose);
 
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index 702e742af4..9deabd195f 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -223,6 +223,7 @@ slashUsage(unsigned short int pager)
 	fprintf(output, _("  \\d[S+]                 list tables, views, and sequences\n"));
 	fprintf(output, _("  \\d[S+]  NAME           describe table, view, sequence, or index\n"));
 	fprintf(output, _("  \\da[S]  [PATTERN]      list aggregates\n"));
+	fprintf(output, _("  \\dQ     [PATTERN]      list assertions\n"));
 	fprintf(output, _("  \\dA[+]  [PATTERN]      list access methods\n"));
 	fprintf(output, _("  \\db[+]  [PATTERN]      list tablespaces\n"));
 	fprintf(output, _("  \\dc[S+] [PATTERN]      list conversions\n"));
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 08d8ef09a4..923ee42a03 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -424,6 +424,24 @@ static const SchemaQuery Query_for_list_of_aggregates[] = {
 	}
 };
 
+static const SchemaQuery Query_for_list_of_assertions = {
+	/* min_server_version */
+	0,
+	/* catname */
+	"pg_catalog.pg_constraint c",
+	/* selcondition */
+	"(c.conrelid = 0 AND c.contypid = 0)",
+	/* viscondition */
+	"TRUE", //TODO: "pg_catalog.pg_constraint_is_visible(c.oid)",
+	/* namespace */
+	"c.connamespace",
+	/* result */
+	"pg_catalog.quote_ident(c.conname)",
+	/* qualresult */
+	NULL
+};
+
+
 static const SchemaQuery Query_for_list_of_datatypes = {
 	/* min_server_version */
 	0,
@@ -1192,6 +1210,7 @@ typedef struct
 static const pgsql_thing_t words_after_create[] = {
 	{"ACCESS METHOD", NULL, NULL, NULL, THING_NO_ALTER},
 	{"AGGREGATE", NULL, NULL, Query_for_list_of_aggregates},
+	{"ASSERTION", NULL, NULL, &Query_for_list_of_assertions},
 	{"CAST", NULL, NULL, NULL}, /* Casts have complex structures for names, so
 								 * skip it */
 	{"COLLATION", "SELECT pg_catalog.quote_ident(collname) FROM pg_catalog.pg_collation WHERE collencoding IN (-1, pg_catalog.pg_char_to_encoding(pg_catalog.getdatabaseencoding())) AND substring(pg_catalog.quote_ident(collname),1,%d)='%s'"},
@@ -1618,7 +1637,7 @@ psql_completion(const char *text, int start, int end)
 		"\\a",
 		"\\connect", "\\conninfo", "\\C", "\\cd", "\\copy",
 		"\\copyright", "\\crosstabview",
-		"\\d", "\\da", "\\dA", "\\db", "\\dc", "\\dC", "\\dd", "\\ddp", "\\dD",
+		"\\d", "\\da", "\\dA", "\\dQ", "\\db", "\\dc", "\\dC", "\\dd", "\\ddp", "\\dD",
 		"\\des", "\\det", "\\deu", "\\dew", "\\dE", "\\df",
 		"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
 		"\\dm", "\\dn", "\\do", "\\dO", "\\dp",
@@ -1769,6 +1788,11 @@ psql_completion(const char *text, int start, int end)
 	else if (Matches3("ALTER", "SCHEMA", MatchAny))
 		COMPLETE_WITH_LIST2("OWNER TO", "RENAME TO");
 
+	// TODO ASSERTION
+
+	else if (Matches3("ALTER", "ASSERTION", MatchAny))
+		COMPLETE_WITH_CONST("RENAME TO ");
+
 	/* ALTER COLLATION <name> */
 	else if (Matches3("ALTER", "COLLATION", MatchAny))
 		COMPLETE_WITH_LIST3("OWNER TO", "RENAME TO", "SET SCHEMA");
@@ -2896,7 +2920,7 @@ psql_completion(const char *text, int start, int end)
 /* DROP */
 	/* Complete DROP object with CASCADE / RESTRICT */
 	else if (Matches3("DROP",
-					  "COLLATION|CONVERSION|DOMAIN|EXTENSION|LANGUAGE|PUBLICATION|SCHEMA|SEQUENCE|SERVER|SUBSCRIPTION|STATISTICS|TABLE|TYPE|VIEW",
+					  "ASSERTION|COLLATION|CONVERSION|DOMAIN|EXTENSION|LANGUAGE|PUBLICATION|SCHEMA|SEQUENCE|SERVER|SUBSCRIPTION|STATISTICS|TABLE|TYPE|VIEW",
 					  MatchAny) ||
 			 Matches4("DROP", "ACCESS", "METHOD", MatchAny) ||
 			 (Matches4("DROP", "AGGREGATE|FUNCTION|PROCEDURE|ROUTINE", MatchAny, MatchAny) &&
@@ -3628,6 +3652,8 @@ psql_completion(const char *text, int start, int end)
 	}
 	else if (TailMatchesCS1("\\da*"))
 		COMPLETE_WITH_VERSIONED_SCHEMA_QUERY(Query_for_list_of_aggregates, NULL);
+	else if (TailMatchesCS1("\\dQ*"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_assertions, NULL);
 	else if (TailMatchesCS1("\\dA*"))
 		COMPLETE_WITH_QUERY(Query_for_list_of_access_methods);
 	else if (TailMatchesCS1("\\db*"))
diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h
index 5f8cf4992e..841be6db59 100644
--- a/src/include/catalog/namespace.h
+++ b/src/include/catalog/namespace.h
@@ -140,6 +140,7 @@ extern bool OverrideSearchPathMatchesCurrent(OverrideSearchPath *path);
 extern void PushOverrideSearchPath(OverrideSearchPath *newpath);
 extern void PopOverrideSearchPath(void);
 
+extern Oid	get_assertion_oid(List *name, bool missing_ok);
 extern Oid	get_collation_oid(List *collname, bool missing_ok);
 extern Oid	get_conversion_oid(List *conname, bool missing_ok);
 extern Oid	FindDefaultConversionProc(int32 for_encoding, int32 to_encoding);
diff --git a/src/include/catalog/pg_constraint.h b/src/include/catalog/pg_constraint.h
index 8fca86d71e..060e436fcd 100644
--- a/src/include/catalog/pg_constraint.h
+++ b/src/include/catalog/pg_constraint.h
@@ -57,8 +57,7 @@ CATALOG(pg_constraint,2606)
 	 * contypid links to the pg_type row for a domain if this is a domain
 	 * constraint.  Otherwise it's 0.
 	 *
-	 * For SQL-style global ASSERTIONs, both conrelid and contypid would be
-	 * zero. This is not presently supported, however.
+	 * For SQL-style global ASSERTIONs, both conrelid and contypid are zero.
 	 */
 	Oid			contypid;		/* domain this constraint constrains */
 
diff --git a/src/include/catalog/pg_constraint_fn.h b/src/include/catalog/pg_constraint_fn.h
index d3351f4a83..9c44c4c6da 100644
--- a/src/include/catalog/pg_constraint_fn.h
+++ b/src/include/catalog/pg_constraint_fn.h
@@ -24,7 +24,7 @@ typedef enum ConstraintCategory
 {
 	CONSTRAINT_RELATION,
 	CONSTRAINT_DOMAIN,
-	CONSTRAINT_ASSERTION		/* for future expansion */
+	CONSTRAINT_ASSERTION
 } ConstraintCategory;
 
 extern Oid CreateConstraintEntry(const char *constraintName,
diff --git a/src/include/catalog/pg_proc.h b/src/include/catalog/pg_proc.h
index 0fdb42f639..463190724d 100644
--- a/src/include/catalog/pg_proc.h
+++ b/src/include/catalog/pg_proc.h
@@ -2101,6 +2101,10 @@ DESCR("oid of replica identity index if any");
 DATA(insert OID = 1250 (  unique_key_recheck	PGNSP PGUID 12 1 0 0 0 f f f t f v s 0 0 2279 "" _null_ _null_ _null_ _null_ _null_ unique_key_recheck _null_ _null_ _null_ ));
 DESCR("deferred UNIQUE constraint check");
 
+DATA(insert OID = 3556 (  assertion_check	PGNSP PGUID 12 1 0 0 0 f f f t f v s 0 0 2279 "" _null_ _null_ _null_ _null_ _null_ assertion_check _null_ _null_ _null_ ));
+DESCR("assertion check");
+
+
 /* Generic referential integrity constraint triggers */
 DATA(insert OID = 1644 (  RI_FKey_check_ins		PGNSP PGUID 12 1 0 0 0 f f f t f v s 0 0 2279 "" _null_ _null_ _null_ _null_ _null_ RI_FKey_check_ins _null_ _null_ _null_ ));
 DESCR("referential integrity FOREIGN KEY ... REFERENCES");
diff --git a/src/include/commands/constraint.h b/src/include/commands/constraint.h
new file mode 100644
index 0000000000..98aa8f996e
--- /dev/null
+++ b/src/include/commands/constraint.h
@@ -0,0 +1,10 @@
+#ifndef CONSTRAINT_H
+#define CONSTRAINT_H
+
+#include "catalog/objectaddress.h"
+#include "nodes/parsenodes.h"
+
+extern ObjectAddress CreateAssertion(CreateAssertionStmt *stmt);
+extern ObjectAddress RenameAssertion(RenameStmt *stmt);
+
+#endif
\ No newline at end of file
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index 74b094a9c3..6c9b89e139 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -414,6 +414,7 @@ typedef enum NodeTag
 	T_DropSubscriptionStmt,
 	T_CreateStatsStmt,
 	T_AlterCollationStmt,
+	T_CreateAssertionStmt,
 	T_CallStmt,
 
 	/*
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 92082b3a7a..d63a7a28ec 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -1635,6 +1635,7 @@ typedef enum ObjectType
 	OBJECT_AGGREGATE,
 	OBJECT_AMOP,
 	OBJECT_AMPROC,
+	OBJECT_ASSERTION,
 	OBJECT_ATTRIBUTE,			/* type's attribute, when distinct from column */
 	OBJECT_CAST,
 	OBJECT_COLUMN,
@@ -3283,6 +3284,16 @@ typedef struct ReindexStmt
 	int			options;		/* Reindex options flags */
 } ReindexStmt;
 
+/* ----------------------
+ *      CREATE ASSERTION Statement
+ */
+typedef struct CreateAssertionStmt
+{
+	NodeTag		type;
+	List    	*assertion_name;
+	Constraint	*constraint;
+} CreateAssertionStmt;
+
 /* ----------------------
  *		CREATE CONVERSION Statement
  * ----------------------
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
index 0230543810..54a6b114ff 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -60,6 +60,7 @@ typedef enum ParseExprKind
 	EXPR_KIND_VALUES_SINGLE,	/* single-row VALUES (in INSERT only) */
 	EXPR_KIND_CHECK_CONSTRAINT, /* CHECK constraint for a table */
 	EXPR_KIND_DOMAIN_CHECK,		/* CHECK constraint for a domain */
+	EXPR_KIND_ASSERTION_CHECK,	/* CHECK constraint for an assertion */
 	EXPR_KIND_COLUMN_DEFAULT,	/* default value for a table column */
 	EXPR_KIND_FUNCTION_DEFAULT, /* default parameter value for function */
 	EXPR_KIND_INDEX_EXPRESSION, /* index expression */
diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h
index f4d4be8d0d..2c10dd082f 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -295,6 +295,7 @@ extern bool pg_opclass_ownercheck(Oid opc_oid, Oid roleid);
 extern bool pg_opfamily_ownercheck(Oid opf_oid, Oid roleid);
 extern bool pg_database_ownercheck(Oid db_oid, Oid roleid);
 extern bool pg_collation_ownercheck(Oid coll_oid, Oid roleid);
+extern bool pg_constraint_ownercheck(Oid constr_oid, Oid roleid);
 extern bool pg_conversion_ownercheck(Oid conv_oid, Oid roleid);
 extern bool pg_ts_dict_ownercheck(Oid dict_oid, Oid roleid);
 extern bool pg_ts_config_ownercheck(Oid cfg_oid, Oid roleid);
diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h
index e55ea4035b..f618270bc8 100644
--- a/src/include/utils/lsyscache.h
+++ b/src/include/utils/lsyscache.h
@@ -114,6 +114,7 @@ extern int	get_func_nargs(Oid funcid);
 extern Oid	get_func_signature(Oid funcid, Oid **argtypes, int *nargs);
 extern Oid	get_func_variadictype(Oid funcid);
 extern bool get_func_retset(Oid funcid);
+extern Oid  get_func_lang(Oid funcid);
 extern bool func_strict(Oid funcid);
 extern char func_volatile(Oid funcid);
 extern char func_parallel(Oid funcid);
diff --git a/src/test/regress/expected/assertions.out b/src/test/regress/expected/assertions.out
new file mode 100644
index 0000000000..7fc0d710a3
--- /dev/null
+++ b/src/test/regress/expected/assertions.out
@@ -0,0 +1,779 @@
+BEGIN TRANSACTION;
+--
+-- Create some helper views and functions to allow us to more easily
+-- assert which operations will cause an assertion to be checked.
+--
+CREATE OR REPLACE VIEW invalidating_operation
+  (assertion_name,
+   relation_name,
+   operation)
+AS
+SELECT c.conname,
+       cl.relname,
+       v.operation
+  FROM pg_trigger t
+  JOIN pg_constraint c ON (t.tgconstraint = c.oid)
+  JOIN pg_proc p ON (t.tgfoid = p.oid)
+  JOIN pg_class cl ON (t.tgrelid = cl.oid)
+  JOIN (VALUES ('INSERT', 1<<2),
+               ('DELETE', 1<<3|1<<5),
+               ('UPDATE', 1<<4))
+    AS v(operation, mask) ON ((v.mask & tgtype) > 0)
+ WHERE t.tgisinternal
+   AND p.proname = 'assertion_check';
+CREATE OR REPLACE VIEW invalidating_by_update_of_column
+  (assertion_name,
+   relation_name,
+   column_name)
+AS
+SELECT c.conname,
+       cl.relname,
+       a.attname
+  FROM pg_trigger t INNER JOIN pg_constraint c ON (t.tgconstraint = c.oid)
+                    INNER JOIN pg_proc p ON (t.tgfoid = p.oid)
+                    INNER JOIN pg_class cl ON (t.tgrelid = cl.oid)
+                    CROSS JOIN LATERAL UNNEST(t.tgattr) AS co(n)
+                    INNER JOIN pg_attribute a ON (a.attrelid = cl.oid AND attnum = co.n)
+ WHERE t.tgisinternal
+   AND p.proname = 'assertion_check'
+   AND (tgtype & (1 << 4)) > 0;
+CREATE OR REPLACE VIEW invalidating_summary
+  (assertion_name,
+   operations)
+AS
+SELECT assertion_name,
+       string_agg(operation, ' ' ORDER BY operation)
+  FROM (SELECT assertion_name,
+               operation ||'('|| string_agg(relation_name, ', ' ORDER BY relation_name) ||')'
+          FROM invalidating_operation
+         WHERE operation <> 'UPDATE'
+         GROUP BY assertion_name,
+                  operation
+         UNION
+        SELECT assertion_name,
+               operation ||'('|| string_agg(relation_name ||'.'|| column_name, ', ' ORDER BY relation_name, column_name) ||')'
+          FROM invalidating_by_update_of_column JOIN invalidating_operation USING (assertion_name, relation_name)
+         WHERE operation = 'UPDATE'
+         GROUP BY assertion_name,
+                  operation)
+    AS v(assertion_name, operation)
+ GROUP BY assertion_name;
+CREATE OR REPLACE FUNCTION have_identical_invalidating_operations
+  (a text, b text)
+RETURNS TABLE (predicate text, correct text)
+AS $$
+  SELECT 'assertion "'|| a ||'" has identical invalidating operations to assertion "'|| b ||'"',
+         CASE WHEN
+           NOT EXISTS (SELECT relation_name, operation
+                         FROM invalidating_operation
+                        WHERE assertion_name = a
+                       EXCEPT 
+                       SELECT relation_name, operation
+                         FROM invalidating_operation
+                        WHERE assertion_name = b) AND
+           NOT EXISTS (SELECT relation_name, column_name
+                         FROM invalidating_by_update_of_column
+                        WHERE assertion_name = a
+                       EXCEPT
+                       SELECT relation_name, column_name
+                         FROM invalidating_by_update_of_column
+                        WHERE assertion_name = b) THEN 'YES'
+                                                  ELSE 'NO' END
+$$ LANGUAGE SQL;
+COMMIT;
+BEGIN TRANSACTION;
+--
+-- Perform some basic sanity checking on creating assertions
+--
+CREATE TABLE test1 (a int, b text);
+CREATE ASSERTION foo CHECK (1 < 2);
+CREATE ASSERTION a2 CHECK ((SELECT count(*) FROM test1) < 5);
+DELETE FROM test1;
+INSERT INTO test1 VALUES (1, 'one');
+INSERT INTO test1 VALUES (2, 'two');
+INSERT INTO test1 VALUES (3, 'three');
+INSERT INTO test1 VALUES (4, 'four');
+SAVEPOINT pre_insert_too_many;
+INSERT INTO test1 VALUES (5, 'five');
+ERROR:  assertion "a2" violated
+ROLLBACK TO SAVEPOINT pre_insert_too_many;
+SELECT constraint_schema,
+       constraint_name
+  FROM information_schema.assertions
+ ORDER BY 1, 2;
+ constraint_schema | constraint_name 
+-------------------+-----------------
+ public            | a2
+ public            | foo
+(2 rows)
+
+\dQ
+                        List of assertions
+ Schema | Name |             Definition             | Description 
+--------+------+------------------------------------+-------------
+ public | a2   | CHECK ((( SELECT count(*) AS count+| 
+        |      |    FROM test1)) < 5)               | 
+ public | foo  | CHECK (1 < 2)                      | 
+(2 rows)
+
+ALTER ASSERTION a2 RENAME TO a3;
+SAVEPOINT pre_rename_foo;
+ALTER ASSERTION foo RENAME TO a3; -- fails
+ERROR:  assertion "a3" already exists
+ROLLBACK TO SAVEPOINT pre_rename_foo;
+DROP ASSERTION foo;
+SAVEPOINT pre_drop_test1;
+DROP TABLE test1; -- fails
+ERROR:  cannot drop table test1 because other objects depend on it
+DETAIL:  constraint a3 depends on table test1
+HINT:  Use DROP ... CASCADE to drop the dependent objects too.
+ROLLBACK TO SAVEPOINT pre_drop_test1;
+DROP TABLE test1 CASCADE;
+NOTICE:  drop cascades to constraint a3
+ROLLBACK;
+BEGIN TRANSACTION;
+--
+-- Expressions involving various operators, functions and casts
+-- 
+CREATE TABLE t (n INTEGER NOT NULL PRIMARY KEY, p BOOLEAN NOT NULL);
+INSERT INTO t (n, p) VALUES (20, TRUE);
+CREATE ASSERTION is_distinct_from CHECK (10 IS DISTINCT FROM (SELECT MIN(n) FROM t));
+CREATE ASSERTION is_not_distinct_from CHECK (20 IS NOT DISTINCT FROM (SELECT MIN(n) FROM t));
+CREATE ASSERTION null_if CHECK (NULLIF((SELECT BOOL_AND(p) FROM t), FALSE));
+CREATE ASSERTION coerce_to_boolean_from_min CHECK (CAST((SELECT MIN(n) FROM t) AS BOOLEAN));
+CREATE ASSERTION coerce_to_boolean_from_bool_and CHECK (CAST((SELECT BOOL_AND(p) FROM t) AS BOOLEAN));
+CREATE ASSERTION coerce_to_boolean_from_boolean CHECK (CAST((SELECT TRUE FROM t) AS BOOLEAN));
+ROLLBACK;
+BEGIN TRANSACTION;
+--
+-- Expressions involving function calls
+--
+CREATE TABLE t (n INTEGER NOT NULL PRIMARY KEY, m INTEGER NOT NULL, d DATE NOT NULL);
+CREATE FUNCTION f(n INTEGER) RETURNS INTEGER
+AS $$
+  SELECT n * 2
+$$ LANGUAGE SQL;
+CREATE FUNCTION r(n INTEGER) RETURNS TABLE (m INTEGER )
+AS $$
+  SELECT m
+    FROM t
+   WHERE n > r.n
+$$ LANGUAGE SQL;
+CREATE ASSERTION age_of_d_in_t CHECK (
+  NOT EXISTS (
+    SELECT FROM t
+     WHERE AGE(DATE '01-01-2018') > INTERVAL '10 days' -- AGE(DATE) is built-in SQL
+  )
+);
+CREATE ASSERTION use_f CHECK (f(0) = 0);
+CREATE ASSERTION use_f_in_predicate CHECK (NOT EXISTS (SELECT FROM t WHERE f(n) = 20));
+CREATE ASSERTION use_f_in_target CHECK (NOT EXISTS (SELECT f(n) FROM t));
+CREATE ASSERTION use_r_in_from CHECK (NOT EXISTS (SELECT FROM r(100)));
+CREATE ASSERTION use_r_in_target CHECK (NOT EXISTS (SELECT r(100)));
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (COALESCE(operations, 'NONE') = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('age_of_d_in_t',      'INSERT(t)'),
+       ('use_f',              'NONE'),
+       ('use_f_in_predicate', 'INSERT(t) UPDATE(t.n)'),
+       ('use_f_in_target',    'INSERT(t)'),
+       ('use_r_in_from',      'INSERT(t) UPDATE(t.m, t.n)'), -- TODO should _from and _target be the same?
+       ('use_r_in_target',    'INSERT(t) UPDATE(t.n)'))
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+   assertion_name   |           actual           |          expected          | correct 
+--------------------+----------------------------+----------------------------+---------
+ age_of_d_in_t      | INSERT(t)                  | INSERT(t)                  | YES
+ use_f              | NONE                       | NONE                       | YES
+ use_f_in_predicate | INSERT(t) UPDATE(t.n)      | INSERT(t) UPDATE(t.n)      | YES
+ use_f_in_target    | INSERT(t)                  | INSERT(t)                  | YES
+ use_r_in_from      | INSERT(t) UPDATE(t.m, t.n) | INSERT(t) UPDATE(t.m, t.n) | YES
+ use_r_in_target    | INSERT(t) UPDATE(t.n)      | INSERT(t) UPDATE(t.n)      | YES
+(6 rows)
+
+CREATE FUNCTION g(n INTEGER) RETURNS INTEGER
+AS $$
+BEGIN
+  RETURN n * 2;
+END;
+$$ LANGUAGE PLPGSQL;
+-- Use of functions other than internal or those implemented in SQL are illegal
+CREATE ASSERTION use_g_in_from CHECK (NOT EXISTS (SELECT FROM g(100))); -- fails
+ERROR:  function "g" uses unsupported language "plpgsql"
+ROLLBACK;
+BEGIN TRANSACTION;
+--
+-- Expressions involving EXISTS and NOT EXISTS
+--
+CREATE TABLE r (n INTEGER NOT NULL PRIMARY KEY);
+CREATE TABLE s (n INTEGER NOT NULL PRIMARY KEY);
+CREATE TABLE t (n INTEGER NOT NULL PRIMARY KEY, m INTEGER NOT NULL);
+CREATE TABLE t1 (n INTEGER NOT NULL PRIMARY KEY, m INTEGER NOT NULL);
+CREATE TABLE t2 (n INTEGER NOT NULL PRIMARY KEY, m INTEGER NOT NULL);
+INSERT INTO r (n) VALUES (1);
+INSERT INTO t (n, m) VALUES (0, 0);
+INSERT INTO t1 (n, m) VALUES (0, 0);
+INSERT INTO t2 (n, m) VALUES (0, 0);
+CREATE ASSERTION exists_no_predicate CHECK (
+  EXISTS (SELECT FROM r)
+);
+CREATE ASSERTION exists_with_predicate CHECK (
+  EXISTS (SELECT FROM r WHERE n > 0)
+);
+CREATE ASSERTION not_exists_no_predicate CHECK (
+  NOT EXISTS (SELECT FROM s)
+);
+CREATE ASSERTION not_exists_with_predicate CHECK (
+  NOT EXISTS (SELECT FROM r WHERE n < 1)
+);
+CREATE ASSERTION direct_subject_of_an_exists CHECK (
+  EXISTS (SELECT n FROM t WHERE m = 0)
+);
+-- TODO These can be optimised, at least if the set operation is UNION.
+CREATE ASSERTION except_subject_of_an_exists CHECK (
+  EXISTS (SELECT n FROM t1 WHERE m = 0 EXCEPT SELECT n FROM t2 WHERE m = 1)
+);
+CREATE ASSERTION intersect_subject_of_an_exists CHECK (
+  EXISTS (SELECT n FROM t1 WHERE m = 0 INTERSECT SELECT n FROM t2 WHERE m = 0)
+);
+CREATE ASSERTION union_subject_of_an_exists CHECK (
+  EXISTS (SELECT n FROM t1 WHERE m = 0 UNION SELECT n FROM t2 WHERE m = 0)
+);
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (operations = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('exists_no_predicate',            'DELETE(r)'),
+       ('exists_with_predicate',          'DELETE(r) UPDATE(r.n)'),
+       ('not_exists_no_predicate',        'INSERT(s)'),
+       ('not_exists_with_predicate',      'INSERT(r) UPDATE(r.n)'),
+       ('direct_subject_of_an_exists',    'DELETE(t) UPDATE(t.m)'),
+       ('except_subject_of_an_exists',    'DELETE(t1) INSERT(t2) UPDATE(t1.m, t1.n, t2.m, t2.n)'),
+       ('intersect_subject_of_an_exists', 'DELETE(t1, t2) UPDATE(t1.m, t1.n, t2.m, t2.n)'),
+       ('union_subject_of_an_exists',     'DELETE(t1, t2) UPDATE(t1.m, t1.n, t2.m, t2.n)'))
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+         assertion_name         |                        actual                        |                       expected                       | correct 
+--------------------------------+------------------------------------------------------+------------------------------------------------------+---------
+ direct_subject_of_an_exists    | DELETE(t) UPDATE(t.m)                                | DELETE(t) UPDATE(t.m)                                | YES
+ except_subject_of_an_exists    | DELETE(t1) INSERT(t2) UPDATE(t1.m, t1.n, t2.m, t2.n) | DELETE(t1) INSERT(t2) UPDATE(t1.m, t1.n, t2.m, t2.n) | YES
+ exists_no_predicate            | DELETE(r)                                            | DELETE(r)                                            | YES
+ exists_with_predicate          | DELETE(r) UPDATE(r.n)                                | DELETE(r) UPDATE(r.n)                                | YES
+ intersect_subject_of_an_exists | DELETE(t1, t2) UPDATE(t1.m, t1.n, t2.m, t2.n)        | DELETE(t1, t2) UPDATE(t1.m, t1.n, t2.m, t2.n)        | YES
+ not_exists_no_predicate        | INSERT(s)                                            | INSERT(s)                                            | YES
+ not_exists_with_predicate      | INSERT(r) UPDATE(r.n)                                | INSERT(r) UPDATE(r.n)                                | YES
+ union_subject_of_an_exists     | DELETE(t1, t2) UPDATE(t1.m, t1.n, t2.m, t2.n)        | DELETE(t1, t2) UPDATE(t1.m, t1.n, t2.m, t2.n)        | YES
+(8 rows)
+
+ROLLBACK;
+BEGIN TRANSACTION;
+--
+-- Expressions involving NOT
+--
+CREATE TABLE r (p BOOLEAN NOT NULL PRIMARY KEY);
+INSERT INTO r (p) VALUES (TRUE);
+CREATE ASSERTION a CHECK (EXISTS (SELECT FROM r WHERE p));
+CREATE ASSERTION b CHECK (NOT EXISTS (SELECT FROM r WHERE NOT p));
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (operations = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('a', 'DELETE(r) UPDATE(r.p)'),
+       ('b', 'INSERT(r) UPDATE(r.p)')) -- NOT(x) and x have opposite invalidating operations
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+ assertion_name |        actual         |       expected        | correct 
+----------------+-----------------------+-----------------------+---------
+ a              | DELETE(r) UPDATE(r.p) | DELETE(r) UPDATE(r.p) | YES
+ b              | INSERT(r) UPDATE(r.p) | INSERT(r) UPDATE(r.p) | YES
+(2 rows)
+
+ROLLBACK;
+BEGIN TRANSACTION;
+--
+-- Expressions involving set operations INTERSECT, UNION, and EXCEPT
+--
+CREATE TABLE r (n INTEGER NOT NULL PRIMARY KEY);
+CREATE TABLE s (n INTEGER NOT NULL PRIMARY KEY);
+CREATE TABLE t (n INTEGER NOT NULL PRIMARY KEY);
+INSERT INTO t (n) VALUES (0);
+CREATE ASSERTION except_operands CHECK (NOT EXISTS (SELECT FROM r EXCEPT SELECT FROM s));
+CREATE ASSERTION intersect_operands CHECK (NOT EXISTS (SELECT FROM r INTERSECT SELECT FROM s));
+CREATE ASSERTION union_operands CHECK (EXISTS (SELECT FROM s UNION SELECT FROM t));
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (operations = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('except_operands',    'DELETE(s) INSERT(r)'),
+       ('intersect_operands', 'INSERT(r, s)'),
+       ('union_operands',     'DELETE(s, t)'))
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+   assertion_name   |       actual        |      expected       | correct 
+--------------------+---------------------+---------------------+---------
+ except_operands    | DELETE(s) INSERT(r) | DELETE(s) INSERT(r) | YES
+ intersect_operands | INSERT(r, s)        | INSERT(r, s)        | YES
+ union_operands     | DELETE(s, t)        | DELETE(s, t)        | YES
+(3 rows)
+
+ROLLBACK;
+BEGIN TRANSACTION;
+--
+-- Expressions involving COUNT aggregations
+--
+CREATE TABLE s (n INTEGER NOT NULL PRIMARY KEY, m INTEGER NOT NULL);
+INSERT INTO s VALUES (1, 2);
+CREATE ASSERTION ge_all_count CHECK (1 >= ALL (SELECT COUNT(*) FROM s));
+CREATE ASSERTION ge_any_count CHECK (1 >= ANY (SELECT COUNT(*) FROM s));
+CREATE ASSERTION ge_count     CHECK (1 >=     (SELECT COUNT(*) FROM s));
+CREATE ASSERTION gt_all_count CHECK (2 >  ALL (SELECT COUNT(*) FROM s));
+CREATE ASSERTION gt_any_count CHECK (2 >  ANY (SELECT COUNT(*) FROM s));
+CREATE ASSERTION gt_count     CHECK (2 >      (SELECT COUNT(*) FROM s));
+CREATE ASSERTION le_all_count CHECK (1 <= ALL (SELECT COUNT(*) FROM s));
+CREATE ASSERTION le_any_count CHECK (0 <= ANY (SELECT COUNT(*) FROM s));
+CREATE ASSERTION le_count     CHECK (0 <=     (SELECT COUNT(*) FROM s));
+CREATE ASSERTION lt_all_count CHECK (0 <  ALL (SELECT COUNT(*) FROM s));
+CREATE ASSERTION lt_any_count CHECK (0 <  ANY (SELECT COUNT(*) FROM s));
+CREATE ASSERTION lt_count     CHECK (0 <      (SELECT COUNT(*) FROM s));
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (operations = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('ge_all_count', 'INSERT(s)'),
+       ('ge_any_count', 'INSERT(s)'),
+       ('ge_count',     'INSERT(s)'),
+       ('gt_all_count', 'INSERT(s)'),
+       ('gt_any_count', 'INSERT(s)'),
+       ('gt_count',     'INSERT(s)'),
+       ('le_all_count', 'DELETE(s)'),
+       ('le_any_count', 'DELETE(s)'),
+       ('le_count',     'DELETE(s)'),
+       ('lt_all_count', 'DELETE(s)'),
+       ('lt_any_count', 'DELETE(s)'),
+       ('lt_count',     'DELETE(s)'))
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+ assertion_name |  actual   | expected  | correct 
+----------------+-----------+-----------+---------
+ ge_all_count   | INSERT(s) | INSERT(s) | YES
+ ge_any_count   | INSERT(s) | INSERT(s) | YES
+ ge_count       | INSERT(s) | INSERT(s) | YES
+ gt_all_count   | INSERT(s) | INSERT(s) | YES
+ gt_any_count   | INSERT(s) | INSERT(s) | YES
+ gt_count       | INSERT(s) | INSERT(s) | YES
+ le_all_count   | DELETE(s) | DELETE(s) | YES
+ le_any_count   | DELETE(s) | DELETE(s) | YES
+ le_count       | DELETE(s) | DELETE(s) | YES
+ lt_all_count   | DELETE(s) | DELETE(s) | YES
+ lt_any_count   | DELETE(s) | DELETE(s) | YES
+ lt_count       | DELETE(s) | DELETE(s) | YES
+(12 rows)
+
+ROLLBACK;
+BEGIN TRANSACTION;
+--
+-- Expressions involving MIN aggregations
+--
+CREATE TABLE s (n INTEGER NOT NULL PRIMARY KEY, m INTEGER NOT NULL);
+INSERT INTO s VALUES (1, 2);
+CREATE ASSERTION ge_any_min CHECK (1 >= ANY (SELECT MIN(n) FROM s));
+CREATE ASSERTION ge_all_min CHECK (1 >= ALL (SELECT MIN(n) FROM s));
+CREATE ASSERTION ge_min     CHECK (1 >=     (SELECT MIN(n) FROM s));
+CREATE ASSERTION gt_any_min CHECK (2 >  ANY (SELECT MIN(n) FROM s));
+CREATE ASSERTION gt_all_min CHECK (2 >  ALL (SELECT MIN(n) FROM s));
+CREATE ASSERTION gt_min     CHECK (2 >      (SELECT MIN(n) FROM s));
+CREATE ASSERTION le_any_min CHECK (0 <= ANY (SELECT MIN(n) FROM s));
+CREATE ASSERTION le_all_min CHECK (0 <= ALL (SELECT MIN(n) FROM s));
+CREATE ASSERTION le_min     CHECK (0 <=     (SELECT MIN(n) FROM s));
+CREATE ASSERTION lt_any_min CHECK (0 <  ANY (SELECT MIN(n) FROM s));
+CREATE ASSERTION lt_all_min CHECK (0 <  ALL (SELECT MIN(n) FROM s));
+CREATE ASSERTION lt_min     CHECK (0 <      (SELECT MIN(n) FROM s));
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (operations = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('ge_all_min', 'DELETE(s) UPDATE(s.n)'),
+       ('ge_any_min', 'DELETE(s) UPDATE(s.n)'),
+       ('ge_min',     'DELETE(s) UPDATE(s.n)'),
+       ('gt_all_min', 'DELETE(s) UPDATE(s.n)'),
+       ('gt_any_min', 'DELETE(s) UPDATE(s.n)'),
+       ('gt_min',     'DELETE(s) UPDATE(s.n)'),
+       ('le_all_min', 'INSERT(s) UPDATE(s.n)'),
+       ('le_any_min', 'INSERT(s) UPDATE(s.n)'),
+       ('le_min',     'INSERT(s) UPDATE(s.n)'),
+       ('lt_all_min', 'INSERT(s) UPDATE(s.n)'),
+       ('lt_any_min', 'INSERT(s) UPDATE(s.n)'),
+       ('lt_min',     'INSERT(s) UPDATE(s.n)'))
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+ assertion_name |        actual         |       expected        | correct 
+----------------+-----------------------+-----------------------+---------
+ ge_all_min     | DELETE(s) UPDATE(s.n) | DELETE(s) UPDATE(s.n) | YES
+ ge_any_min     | DELETE(s) UPDATE(s.n) | DELETE(s) UPDATE(s.n) | YES
+ ge_min         | DELETE(s) UPDATE(s.n) | DELETE(s) UPDATE(s.n) | YES
+ gt_all_min     | DELETE(s) UPDATE(s.n) | DELETE(s) UPDATE(s.n) | YES
+ gt_any_min     | DELETE(s) UPDATE(s.n) | DELETE(s) UPDATE(s.n) | YES
+ gt_min         | DELETE(s) UPDATE(s.n) | DELETE(s) UPDATE(s.n) | YES
+ le_all_min     | INSERT(s) UPDATE(s.n) | INSERT(s) UPDATE(s.n) | YES
+ le_any_min     | INSERT(s) UPDATE(s.n) | INSERT(s) UPDATE(s.n) | YES
+ le_min         | INSERT(s) UPDATE(s.n) | INSERT(s) UPDATE(s.n) | YES
+ lt_all_min     | INSERT(s) UPDATE(s.n) | INSERT(s) UPDATE(s.n) | YES
+ lt_any_min     | INSERT(s) UPDATE(s.n) | INSERT(s) UPDATE(s.n) | YES
+ lt_min         | INSERT(s) UPDATE(s.n) | INSERT(s) UPDATE(s.n) | YES
+(12 rows)
+
+ROLLBACK;
+BEGIN TRANSACTION;
+--
+-- Expressions involving MAX aggregations
+--
+CREATE TABLE s (n INTEGER NOT NULL PRIMARY KEY, m INTEGER NOT NULL);
+INSERT INTO s VALUES (1, 2);
+CREATE ASSERTION ge_any_max CHECK (1 >= ANY (SELECT MAX(n) FROM s));
+CREATE ASSERTION ge_all_max CHECK (1 >= ALL (SELECT MAX(n) FROM s));
+CREATE ASSERTION ge_max     CHECK (1 >=     (SELECT MAX(n) FROM s));
+CREATE ASSERTION gt_all_max CHECK (2 >  ALL (SELECT MAX(n) FROM s));
+CREATE ASSERTION gt_any_max CHECK (2 >  ANY (SELECT MAX(n) FROM s));
+CREATE ASSERTION gt_max     CHECK (2 >      (SELECT MAX(n) FROM s));
+CREATE ASSERTION le_all_max CHECK (0 <= ALL (SELECT MAX(n) FROM s));
+CREATE ASSERTION le_any_max CHECK (0 <= ANY (SELECT MAX(n) FROM s));
+CREATE ASSERTION le_max     CHECK (0 <=     (SELECT MAX(n) FROM s));
+CREATE ASSERTION lt_all_max CHECK (0 <  ALL (SELECT MAX(n) FROM s));
+CREATE ASSERTION lt_any_max CHECK (0 <  ANY (SELECT MAX(n) FROM s));
+CREATE ASSERTION lt_max     CHECK (0 <      (SELECT MAX(n) FROM s));
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (operations = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('ge_all_max', 'INSERT(s) UPDATE(s.n)'),
+       ('ge_any_max', 'INSERT(s) UPDATE(s.n)'),
+       ('ge_max',     'INSERT(s) UPDATE(s.n)'),
+       ('gt_all_max', 'INSERT(s) UPDATE(s.n)'),
+       ('gt_any_max', 'INSERT(s) UPDATE(s.n)'),
+       ('gt_max',     'INSERT(s) UPDATE(s.n)'),
+       ('le_all_max', 'DELETE(s) UPDATE(s.n)'),
+       ('le_any_max', 'DELETE(s) UPDATE(s.n)'),
+       ('le_max',     'DELETE(s) UPDATE(s.n)'),
+       ('lt_all_max', 'DELETE(s) UPDATE(s.n)'),
+       ('lt_any_max', 'DELETE(s) UPDATE(s.n)'),
+       ('lt_max',     'DELETE(s) UPDATE(s.n)'))
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+ assertion_name |        actual         |       expected        | correct 
+----------------+-----------------------+-----------------------+---------
+ ge_all_max     | INSERT(s) UPDATE(s.n) | INSERT(s) UPDATE(s.n) | YES
+ ge_any_max     | INSERT(s) UPDATE(s.n) | INSERT(s) UPDATE(s.n) | YES
+ ge_max         | INSERT(s) UPDATE(s.n) | INSERT(s) UPDATE(s.n) | YES
+ gt_all_max     | INSERT(s) UPDATE(s.n) | INSERT(s) UPDATE(s.n) | YES
+ gt_any_max     | INSERT(s) UPDATE(s.n) | INSERT(s) UPDATE(s.n) | YES
+ gt_max         | INSERT(s) UPDATE(s.n) | INSERT(s) UPDATE(s.n) | YES
+ le_all_max     | DELETE(s) UPDATE(s.n) | DELETE(s) UPDATE(s.n) | YES
+ le_any_max     | DELETE(s) UPDATE(s.n) | DELETE(s) UPDATE(s.n) | YES
+ le_max         | DELETE(s) UPDATE(s.n) | DELETE(s) UPDATE(s.n) | YES
+ lt_all_max     | DELETE(s) UPDATE(s.n) | DELETE(s) UPDATE(s.n) | YES
+ lt_any_max     | DELETE(s) UPDATE(s.n) | DELETE(s) UPDATE(s.n) | YES
+ lt_max         | DELETE(s) UPDATE(s.n) | DELETE(s) UPDATE(s.n) | YES
+(12 rows)
+
+ROLLBACK;
+BEGIN TRANSACTION;
+--
+-- Expressions involving BOOL_AND aggregations
+--
+CREATE TABLE t (n INTEGER NOT NULL PRIMARY KEY, p BOOLEAN NOT NULL);
+INSERT INTO t (n, p) VALUES (0, TRUE);
+CREATE ASSERTION eq_bool_and     CHECK (TRUE  =      (SELECT BOOL_AND(p) FROM t));
+CREATE ASSERTION eq_any_bool_and CHECK (TRUE  =  ANY (SELECT BOOL_AND(p) FROM t));
+CREATE ASSERTION eq_all_bool_and CHECK (TRUE  =  ALL (SELECT BOOL_AND(p) FROM t));
+CREATE ASSERTION ne_bool_and     CHECK (FALSE <>     (SELECT BOOL_AND(p) FROM t));
+CREATE ASSERTION ne_any_bool_and CHECK (FALSE <> ANY (SELECT BOOL_AND(p) FROM t));
+CREATE ASSERTION ne_all_bool_and CHECK (FALSE <> ALL (SELECT BOOL_AND(p) FROM t));
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (operations = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('eq_all_bool_and', 'INSERT(t) UPDATE(t.p)'),
+       ('eq_any_bool_and', 'INSERT(t) UPDATE(t.p)'),
+       ('eq_bool_and',     'INSERT(t) UPDATE(t.p)'),
+       ('ne_all_bool_and', 'INSERT(t) UPDATE(t.p)'),
+       ('ne_any_bool_and', 'INSERT(t) UPDATE(t.p)'),
+       ('ne_bool_and',     'INSERT(t) UPDATE(t.p)'))
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+ assertion_name  |        actual         |       expected        | correct 
+-----------------+-----------------------+-----------------------+---------
+ eq_all_bool_and | INSERT(t) UPDATE(t.p) | INSERT(t) UPDATE(t.p) | YES
+ eq_any_bool_and | INSERT(t) UPDATE(t.p) | INSERT(t) UPDATE(t.p) | YES
+ eq_bool_and     | INSERT(t) UPDATE(t.p) | INSERT(t) UPDATE(t.p) | YES
+ ne_all_bool_and | INSERT(t) UPDATE(t.p) | INSERT(t) UPDATE(t.p) | YES
+ ne_any_bool_and | INSERT(t) UPDATE(t.p) | INSERT(t) UPDATE(t.p) | YES
+ ne_bool_and     | INSERT(t) UPDATE(t.p) | INSERT(t) UPDATE(t.p) | YES
+(6 rows)
+
+ROLLBACK;
+BEGIN TRANSACTION;
+--
+-- Expressions involving BOOL_OR aggregations
+--
+CREATE TABLE t (n INTEGER NOT NULL PRIMARY KEY, p BOOLEAN NOT NULL);
+INSERT INTO t (n, p) VALUES (0, TRUE);
+CREATE ASSERTION eq_bool_or     CHECK (TRUE  =      (SELECT BOOL_OR(p) FROM t));
+CREATE ASSERTION eq_any_bool_or CHECK (TRUE  =  ANY (SELECT BOOL_OR(p) FROM t));
+CREATE ASSERTION eq_all_bool_or CHECK (TRUE  =  ALL (SELECT BOOL_OR(p) FROM t));
+CREATE ASSERTION ne_bool_or     CHECK (FALSE <>     (SELECT BOOL_OR(p) FROM t));
+CREATE ASSERTION ne_any_bool_or CHECK (FALSE <> ANY (SELECT BOOL_OR(p) FROM t));
+CREATE ASSERTION ne_all_bool_or CHECK (FALSE <> ALL (SELECT BOOL_OR(p) FROM t));
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (operations = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('eq_all_bool_or', 'DELETE(t) UPDATE(t.p)'),
+       ('eq_any_bool_or', 'DELETE(t) UPDATE(t.p)'),
+       ('eq_bool_or',     'DELETE(t) UPDATE(t.p)'),
+       ('ne_all_bool_or', 'DELETE(t) UPDATE(t.p)'),
+       ('ne_any_bool_or', 'DELETE(t) UPDATE(t.p)'),
+       ('ne_bool_or',     'DELETE(t) UPDATE(t.p)'))
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+ assertion_name |        actual         |       expected        | correct 
+----------------+-----------------------+-----------------------+---------
+ eq_all_bool_or | DELETE(t) UPDATE(t.p) | DELETE(t) UPDATE(t.p) | YES
+ eq_any_bool_or | DELETE(t) UPDATE(t.p) | DELETE(t) UPDATE(t.p) | YES
+ eq_bool_or     | DELETE(t) UPDATE(t.p) | DELETE(t) UPDATE(t.p) | YES
+ ne_all_bool_or | DELETE(t) UPDATE(t.p) | DELETE(t) UPDATE(t.p) | YES
+ ne_any_bool_or | DELETE(t) UPDATE(t.p) | DELETE(t) UPDATE(t.p) | YES
+ ne_bool_or     | DELETE(t) UPDATE(t.p) | DELETE(t) UPDATE(t.p) | YES
+(6 rows)
+
+ROLLBACK;
+BEGIN TRANSACTION;
+--
+-- Expressions involving window functions
+--
+CREATE TABLE t (n INTEGER NOT NULL PRIMARY KEY, p BOOLEAN NOT NULL);
+-- Regular window function
+CREATE ASSERTION alternate_p CHECK (
+  NOT EXISTS (
+    SELECT FROM (
+      SELECT LAG(p, 1, NOT p) OVER n <> p
+         AND LEAD(p, 1, NOT p) OVER n <> p
+        FROM t
+      WINDOW n AS (ORDER BY n)
+    ) AS v(q)
+    WHERE NOT q
+  )
+);
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (operations = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('alternate_p', 'DELETE(t) INSERT(t) UPDATE(t.n, t.p)'))
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+ assertion_name |                actual                |               expected               | correct 
+----------------+--------------------------------------+--------------------------------------+---------
+ alternate_p    | DELETE(t) INSERT(t) UPDATE(t.n, t.p) | DELETE(t) INSERT(t) UPDATE(t.n, t.p) | YES
+(1 row)
+
+INSERT INTO t (n, p) VALUES (10, TRUE);
+INSERT INTO t (n, p) VALUES (20, FALSE);
+INSERT INTO t (n, p) VALUES (30, TRUE);
+SAVEPOINT pre_insert;
+INSERT INTO t (n, p) VALUES (40, TRUE);
+ERROR:  assertion "alternate_p" violated
+ROLLBACK TO SAVEPOINT pre_insert;
+SAVEPOINT pre_delete;
+DELETE FROM t WHERE n = 20;
+ERROR:  assertion "alternate_p" violated
+ROLLBACK TO SAVEPOINT pre_delete;
+SAVEPOINT pre_update;
+UPDATE t SET p = NOT p WHERE n = 20;
+ERROR:  assertion "alternate_p" violated
+ROLLBACK TO SAVEPOINT pre_update;
+DROP ASSERTION alternate_p;
+-- Aggregate function over a window
+CREATE TABLE s (n INTEGER NOT NULL PRIMARY KEY, m INTEGER NOT NULL);
+INSERT INTO s (n, m)
+SELECT n, (2 * n)
+  FROM GENERATE_SERIES(0, 9) AS ns(n);
+CREATE ASSERTION max_over_window CHECK ((10, 20) > ALL (SELECT n, MAX(m) OVER (ORDER BY n) FROM s));
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (operations = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('max_over_window', 'INSERT(s) UPDATE(s.m, s.n)'))
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+ assertion_name  |           actual           |          expected          | correct 
+-----------------+----------------------------+----------------------------+---------
+ max_over_window | INSERT(s) UPDATE(s.m, s.n) | INSERT(s) UPDATE(s.m, s.n) | YES
+(1 row)
+
+ROLLBACK;
+BEGIN TRANSACTION;
+--
+-- Expressions containing a comparison should have the same invalidating
+-- operations as an expression containing the inverse comparison operation
+-- and where the operands have been switched
+--
+CREATE TABLE r (n INTEGER NOT NULL PRIMARY KEY);
+INSERT INTO r (n) VALUES (9);
+-- ">" and "<" are opposites
+CREATE ASSERTION a CHECK (10 > (SELECT MIN(n) FROM r));
+CREATE ASSERTION b CHECK ((SELECT MIN(n) FROM r) < 10);
+-- "<" and ">" are opposites
+CREATE ASSERTION c CHECK (8 < (SELECT MIN(n) FROM r));
+CREATE ASSERTION d CHECK ((SELECT MIN(n) FROM r) > 8);
+-- ">=" and "<=" are opposites
+CREATE ASSERTION e CHECK (9 >= (SELECT MIN(n) FROM r));
+CREATE ASSERTION f CHECK ((SELECT MIN(n) FROM r) <= 9);
+-- "<=" and ">=" are opposites
+CREATE ASSERTION g CHECK (9 <= (SELECT MIN(n) FROM r));
+CREATE ASSERTION h CHECK ((SELECT MIN(n) FROM r) >= 9);
+SELECT (test).predicate, (test).correct
+  FROM (
+VALUES ('a', 'b'),
+       ('c', 'd'),
+       ('e', 'f'),
+       ('g', 'h'))
+    AS v(a, b)
+ CROSS JOIN LATERAL (SELECT have_identical_invalidating_operations(v.a, v.b))
+    AS w(test)
+ ORDER BY 1, 2;
+                              predicate                               | correct 
+----------------------------------------------------------------------+---------
+ assertion "a" has identical invalidating operations to assertion "b" | YES
+ assertion "c" has identical invalidating operations to assertion "d" | YES
+ assertion "e" has identical invalidating operations to assertion "f" | YES
+ assertion "g" has identical invalidating operations to assertion "h" | YES
+(4 rows)
+
+ROLLBACK;
+BEGIN TRANSACTION;
+--
+-- The INFORMATION_SCHEMA should contain the correct information
+--
+CREATE TABLE t (n INTEGER NOT NULL PRIMARY KEY, p BOOLEAN NOT NULL);
+CREATE ASSERTION a CHECK (NOT EXISTS (SELECT FROM t WHERE p));
+SELECT predicate,
+       CASE WHEN
+         truth THEN 'YES'
+               ELSE 'NO'
+       END AS correct
+  FROM ( 
+VALUES ('INFORMATION_SCHEMA.CONSTRAINT_TABLE_USAGE contains only the correct tables',
+        EXISTS (
+          SELECT FROM information_schema.constraint_table_usage
+           WHERE (constraint_name, table_name) = ('a', 't'))
+        AND NOT EXISTS (
+          SELECT FROM information_schema.constraint_table_usage
+           WHERE constraint_name = 'a'
+             AND table_name <> 't')),
+       ('INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE contains only the correct columns (t.p)',
+        EXISTS (
+          SELECT FROM information_schema.constraint_column_usage
+           WHERE (constraint_name, table_name, column_name) = ('a', 't', 'p'))
+        AND NOT EXISTS (
+          SELECT FROM information_schema.constraint_column_usage
+           WHERE constraint_name ='a'
+             AND (table_name, column_name) <> ('t', 'p'))),
+       ('INFORMATION_SCHEMA.ASSERTIONS contains only the correct assertions',
+        EXISTS (
+          SELECT FROM information_schema.assertions
+           WHERE constraint_name = 'a')
+        AND NOT EXISTS (
+          SELECT FROM information_schema.assertions
+           WHERE constraint_name <> 'a')))
+    AS v(predicate, truth);
+                                     predicate                                      | correct 
+------------------------------------------------------------------------------------+---------
+ INFORMATION_SCHEMA.CONSTRAINT_TABLE_USAGE contains only the correct tables         | YES
+ INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE contains only the correct columns (t.p) | YES
+ INFORMATION_SCHEMA.ASSERTIONS contains only the correct assertions                 | YES
+(3 rows)
+
+ROLLBACK;
+BEGIN TRANSACTION;
+-- TODO This needs rethinking
+/*
+CREATE TABLE s (n INTEGER NOT NULL PRIMARY KEY, m INTEGER NOT NULL);
+CREATE TABLE t (n INTEGER NOT NULL PRIMARY KEY, p BOOLEAN NOT NULL);
+CREATE VIEW v AS SELECT * FROM s INNER JOIN t USING (n);
+
+CREATE ASSERTION a CHECK (NOT EXISTS (SELECT FROM v));
+
+-- we should depend on the view not the tables
+SELECT OK(depends_on('a', 'v'),
+          'Assertion depends upon the view v it references');
+SELECT OK(NOT depends_on('a', 's'),
+          'Assertion does not depend upon the table s referenced in the view v');
+SELECT OK(NOT depends_on('a', 't'),
+          'Assertion does not depend upon the table t referenced in the view v');
+
+-- we should trigger on the tables and not the view
+SELECT OK(EXISTS(SELECT FROM assertion_check_operation
+                  WHERE (assertion_name, relation_name) = ('a', 's')),
+          'Assertion is checked on modifications to table s');
+SELECT OK(EXISTS(SELECT FROM assertion_check_operation
+                  WHERE (assertion_name, relation_name) = ('a', 't')),
+          'Assertion is checked on modifications to table t');
+SELECT OK(NOT EXISTS(SELECT FROM assertion_check_operation
+                      WHERE (assertion_name, relation_name) = ('a', 'v')),
+          'Assertion is not checked on modifications to view v');
+*/
+ROLLBACK;
+-- TODO test commonalities between count, min, max, etc, for optimisations
+-- TODO ensure window function aggregates are treated the same as regular aggregates
+-- TODO ensure that conflicting aggregate functions are not incorrectly optimised
+-- TODO ensure that recorded dependencies are correct (traversal into functions and views)
+BEGIN TRANSACTION;
+DROP FUNCTION have_identical_invalidating_operations;
+DROP VIEW invalidating_summary;
+DROP VIEW invalidating_by_update_of_column;
+DROP VIEW invalidating_operation;
+COMMIT;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index ad9434fb87..4966352197 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -60,7 +60,7 @@ test: create_index create_view
 # ----------
 # Another group of parallel tests
 # ----------
-test: create_aggregate create_function_3 create_cast constraints triggers inherit create_table_like typed_table vacuum drop_if_exists updatable_views rolenames roleattributes create_am hash_func
+test: create_aggregate create_function_3 create_cast constraints triggers inherit create_table_like typed_table vacuum drop_if_exists updatable_views rolenames roleattributes create_am assertions hash_func
 
 # ----------
 # sanity_check does a vacuum, affecting the sort order of SELECT *
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 27cd49845e..e5277a278a 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -58,6 +58,7 @@ test: create_function_1
 test: create_type
 test: create_table
 test: create_function_2
+test: assertions
 test: copy
 test: copyselect
 test: copydml
diff --git a/src/test/regress/sql/assertions.sql b/src/test/regress/sql/assertions.sql
new file mode 100644
index 0000000000..f270698da1
--- /dev/null
+++ b/src/test/regress/sql/assertions.sql
@@ -0,0 +1,775 @@
+
+BEGIN TRANSACTION;
+
+--
+-- Create some helper views and functions to allow us to more easily
+-- assert which operations will cause an assertion to be checked.
+--
+
+CREATE OR REPLACE VIEW invalidating_operation
+  (assertion_name,
+   relation_name,
+   operation)
+AS
+SELECT c.conname,
+       cl.relname,
+       v.operation
+  FROM pg_trigger t
+  JOIN pg_constraint c ON (t.tgconstraint = c.oid)
+  JOIN pg_proc p ON (t.tgfoid = p.oid)
+  JOIN pg_class cl ON (t.tgrelid = cl.oid)
+  JOIN (VALUES ('INSERT', 1<<2),
+               ('DELETE', 1<<3|1<<5),
+               ('UPDATE', 1<<4))
+    AS v(operation, mask) ON ((v.mask & tgtype) > 0)
+ WHERE t.tgisinternal
+   AND p.proname = 'assertion_check';
+
+CREATE OR REPLACE VIEW invalidating_by_update_of_column
+  (assertion_name,
+   relation_name,
+   column_name)
+AS
+SELECT c.conname,
+       cl.relname,
+       a.attname
+  FROM pg_trigger t INNER JOIN pg_constraint c ON (t.tgconstraint = c.oid)
+                    INNER JOIN pg_proc p ON (t.tgfoid = p.oid)
+                    INNER JOIN pg_class cl ON (t.tgrelid = cl.oid)
+                    CROSS JOIN LATERAL UNNEST(t.tgattr) AS co(n)
+                    INNER JOIN pg_attribute a ON (a.attrelid = cl.oid AND attnum = co.n)
+ WHERE t.tgisinternal
+   AND p.proname = 'assertion_check'
+   AND (tgtype & (1 << 4)) > 0;
+
+CREATE OR REPLACE VIEW invalidating_summary
+  (assertion_name,
+   operations)
+AS
+SELECT assertion_name,
+       string_agg(operation, ' ' ORDER BY operation)
+  FROM (SELECT assertion_name,
+               operation ||'('|| string_agg(relation_name, ', ' ORDER BY relation_name) ||')'
+          FROM invalidating_operation
+         WHERE operation <> 'UPDATE'
+         GROUP BY assertion_name,
+                  operation
+         UNION
+        SELECT assertion_name,
+               operation ||'('|| string_agg(relation_name ||'.'|| column_name, ', ' ORDER BY relation_name, column_name) ||')'
+          FROM invalidating_by_update_of_column JOIN invalidating_operation USING (assertion_name, relation_name)
+         WHERE operation = 'UPDATE'
+         GROUP BY assertion_name,
+                  operation)
+    AS v(assertion_name, operation)
+ GROUP BY assertion_name;
+
+CREATE OR REPLACE FUNCTION have_identical_invalidating_operations
+  (a text, b text)
+RETURNS TABLE (predicate text, correct text)
+AS $$
+  SELECT 'assertion "'|| a ||'" has identical invalidating operations to assertion "'|| b ||'"',
+         CASE WHEN
+           NOT EXISTS (SELECT relation_name, operation
+                         FROM invalidating_operation
+                        WHERE assertion_name = a
+                       EXCEPT 
+                       SELECT relation_name, operation
+                         FROM invalidating_operation
+                        WHERE assertion_name = b) AND
+           NOT EXISTS (SELECT relation_name, column_name
+                         FROM invalidating_by_update_of_column
+                        WHERE assertion_name = a
+                       EXCEPT
+                       SELECT relation_name, column_name
+                         FROM invalidating_by_update_of_column
+                        WHERE assertion_name = b) THEN 'YES'
+                                                  ELSE 'NO' END
+$$ LANGUAGE SQL;
+
+COMMIT;
+
+
+BEGIN TRANSACTION;
+
+--
+-- Perform some basic sanity checking on creating assertions
+--
+
+CREATE TABLE test1 (a int, b text);
+
+CREATE ASSERTION foo CHECK (1 < 2);
+CREATE ASSERTION a2 CHECK ((SELECT count(*) FROM test1) < 5);
+
+DELETE FROM test1;
+INSERT INTO test1 VALUES (1, 'one');
+INSERT INTO test1 VALUES (2, 'two');
+INSERT INTO test1 VALUES (3, 'three');
+INSERT INTO test1 VALUES (4, 'four');
+
+SAVEPOINT pre_insert_too_many;
+INSERT INTO test1 VALUES (5, 'five');
+ROLLBACK TO SAVEPOINT pre_insert_too_many;
+
+SELECT constraint_schema,
+       constraint_name
+  FROM information_schema.assertions
+ ORDER BY 1, 2;
+
+\dQ
+
+ALTER ASSERTION a2 RENAME TO a3;
+
+SAVEPOINT pre_rename_foo;
+ALTER ASSERTION foo RENAME TO a3; -- fails
+ROLLBACK TO SAVEPOINT pre_rename_foo;
+
+DROP ASSERTION foo;
+
+SAVEPOINT pre_drop_test1;
+DROP TABLE test1; -- fails
+ROLLBACK TO SAVEPOINT pre_drop_test1;
+
+DROP TABLE test1 CASCADE;
+
+ROLLBACK;
+
+
+
+BEGIN TRANSACTION;
+
+--
+-- Expressions involving various operators, functions and casts
+-- 
+
+CREATE TABLE t (n INTEGER NOT NULL PRIMARY KEY, p BOOLEAN NOT NULL);
+
+INSERT INTO t (n, p) VALUES (20, TRUE);
+
+CREATE ASSERTION is_distinct_from CHECK (10 IS DISTINCT FROM (SELECT MIN(n) FROM t));
+CREATE ASSERTION is_not_distinct_from CHECK (20 IS NOT DISTINCT FROM (SELECT MIN(n) FROM t));
+CREATE ASSERTION null_if CHECK (NULLIF((SELECT BOOL_AND(p) FROM t), FALSE));
+CREATE ASSERTION coerce_to_boolean_from_min CHECK (CAST((SELECT MIN(n) FROM t) AS BOOLEAN));
+CREATE ASSERTION coerce_to_boolean_from_bool_and CHECK (CAST((SELECT BOOL_AND(p) FROM t) AS BOOLEAN));
+CREATE ASSERTION coerce_to_boolean_from_boolean CHECK (CAST((SELECT TRUE FROM t) AS BOOLEAN));
+
+ROLLBACK;
+
+
+BEGIN TRANSACTION;
+
+--
+-- Expressions involving function calls
+--
+
+CREATE TABLE t (n INTEGER NOT NULL PRIMARY KEY, m INTEGER NOT NULL, d DATE NOT NULL);
+
+CREATE FUNCTION f(n INTEGER) RETURNS INTEGER
+AS $$
+  SELECT n * 2
+$$ LANGUAGE SQL;
+
+CREATE FUNCTION r(n INTEGER) RETURNS TABLE (m INTEGER )
+AS $$
+  SELECT m
+    FROM t
+   WHERE n > r.n
+$$ LANGUAGE SQL;
+
+CREATE ASSERTION age_of_d_in_t CHECK (
+  NOT EXISTS (
+    SELECT FROM t
+     WHERE AGE(DATE '01-01-2018') > INTERVAL '10 days' -- AGE(DATE) is built-in SQL
+  )
+);
+CREATE ASSERTION use_f CHECK (f(0) = 0);
+CREATE ASSERTION use_f_in_predicate CHECK (NOT EXISTS (SELECT FROM t WHERE f(n) = 20));
+CREATE ASSERTION use_f_in_target CHECK (NOT EXISTS (SELECT f(n) FROM t));
+CREATE ASSERTION use_r_in_from CHECK (NOT EXISTS (SELECT FROM r(100)));
+CREATE ASSERTION use_r_in_target CHECK (NOT EXISTS (SELECT r(100)));
+
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (COALESCE(operations, 'NONE') = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('age_of_d_in_t',      'INSERT(t)'),
+       ('use_f',              'NONE'),
+       ('use_f_in_predicate', 'INSERT(t) UPDATE(t.n)'),
+       ('use_f_in_target',    'INSERT(t)'),
+       ('use_r_in_from',      'INSERT(t) UPDATE(t.m, t.n)'), -- TODO should _from and _target be the same?
+       ('use_r_in_target',    'INSERT(t) UPDATE(t.n)'))
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+
+CREATE FUNCTION g(n INTEGER) RETURNS INTEGER
+AS $$
+BEGIN
+  RETURN n * 2;
+END;
+$$ LANGUAGE PLPGSQL;
+
+-- Use of functions other than internal or those implemented in SQL are illegal
+CREATE ASSERTION use_g_in_from CHECK (NOT EXISTS (SELECT FROM g(100))); -- fails
+
+ROLLBACK;
+
+
+BEGIN TRANSACTION;
+
+--
+-- Expressions involving EXISTS and NOT EXISTS
+--
+
+CREATE TABLE r (n INTEGER NOT NULL PRIMARY KEY);
+CREATE TABLE s (n INTEGER NOT NULL PRIMARY KEY);
+CREATE TABLE t (n INTEGER NOT NULL PRIMARY KEY, m INTEGER NOT NULL);
+CREATE TABLE t1 (n INTEGER NOT NULL PRIMARY KEY, m INTEGER NOT NULL);
+CREATE TABLE t2 (n INTEGER NOT NULL PRIMARY KEY, m INTEGER NOT NULL);
+
+INSERT INTO r (n) VALUES (1);
+INSERT INTO t (n, m) VALUES (0, 0);
+INSERT INTO t1 (n, m) VALUES (0, 0);
+INSERT INTO t2 (n, m) VALUES (0, 0);
+
+CREATE ASSERTION exists_no_predicate CHECK (
+  EXISTS (SELECT FROM r)
+);
+
+CREATE ASSERTION exists_with_predicate CHECK (
+  EXISTS (SELECT FROM r WHERE n > 0)
+);
+
+CREATE ASSERTION not_exists_no_predicate CHECK (
+  NOT EXISTS (SELECT FROM s)
+);
+
+CREATE ASSERTION not_exists_with_predicate CHECK (
+  NOT EXISTS (SELECT FROM r WHERE n < 1)
+);
+
+CREATE ASSERTION direct_subject_of_an_exists CHECK (
+  EXISTS (SELECT n FROM t WHERE m = 0)
+);
+
+-- TODO These can be optimised, at least if the set operation is UNION.
+CREATE ASSERTION except_subject_of_an_exists CHECK (
+  EXISTS (SELECT n FROM t1 WHERE m = 0 EXCEPT SELECT n FROM t2 WHERE m = 1)
+);
+
+CREATE ASSERTION intersect_subject_of_an_exists CHECK (
+  EXISTS (SELECT n FROM t1 WHERE m = 0 INTERSECT SELECT n FROM t2 WHERE m = 0)
+);
+
+CREATE ASSERTION union_subject_of_an_exists CHECK (
+  EXISTS (SELECT n FROM t1 WHERE m = 0 UNION SELECT n FROM t2 WHERE m = 0)
+);
+
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (operations = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('exists_no_predicate',            'DELETE(r)'),
+       ('exists_with_predicate',          'DELETE(r) UPDATE(r.n)'),
+       ('not_exists_no_predicate',        'INSERT(s)'),
+       ('not_exists_with_predicate',      'INSERT(r) UPDATE(r.n)'),
+       ('direct_subject_of_an_exists',    'DELETE(t) UPDATE(t.m)'),
+       ('except_subject_of_an_exists',    'DELETE(t1) INSERT(t2) UPDATE(t1.m, t1.n, t2.m, t2.n)'),
+       ('intersect_subject_of_an_exists', 'DELETE(t1, t2) UPDATE(t1.m, t1.n, t2.m, t2.n)'),
+       ('union_subject_of_an_exists',     'DELETE(t1, t2) UPDATE(t1.m, t1.n, t2.m, t2.n)'))
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+
+ROLLBACK;
+
+
+BEGIN TRANSACTION;
+
+--
+-- Expressions involving NOT
+--
+
+CREATE TABLE r (p BOOLEAN NOT NULL PRIMARY KEY);
+
+INSERT INTO r (p) VALUES (TRUE);
+
+CREATE ASSERTION a CHECK (EXISTS (SELECT FROM r WHERE p));
+CREATE ASSERTION b CHECK (NOT EXISTS (SELECT FROM r WHERE NOT p));
+
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (operations = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('a', 'DELETE(r) UPDATE(r.p)'),
+       ('b', 'INSERT(r) UPDATE(r.p)')) -- NOT(x) and x have opposite invalidating operations
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+
+ROLLBACK;
+
+
+BEGIN TRANSACTION;
+
+--
+-- Expressions involving set operations INTERSECT, UNION, and EXCEPT
+--
+
+CREATE TABLE r (n INTEGER NOT NULL PRIMARY KEY);
+CREATE TABLE s (n INTEGER NOT NULL PRIMARY KEY);
+CREATE TABLE t (n INTEGER NOT NULL PRIMARY KEY);
+
+INSERT INTO t (n) VALUES (0);
+
+CREATE ASSERTION except_operands CHECK (NOT EXISTS (SELECT FROM r EXCEPT SELECT FROM s));
+CREATE ASSERTION intersect_operands CHECK (NOT EXISTS (SELECT FROM r INTERSECT SELECT FROM s));
+CREATE ASSERTION union_operands CHECK (EXISTS (SELECT FROM s UNION SELECT FROM t));
+
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (operations = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('except_operands',    'DELETE(s) INSERT(r)'),
+       ('intersect_operands', 'INSERT(r, s)'),
+       ('union_operands',     'DELETE(s, t)'))
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+
+ROLLBACK;
+
+
+BEGIN TRANSACTION;
+
+--
+-- Expressions involving COUNT aggregations
+--
+
+CREATE TABLE s (n INTEGER NOT NULL PRIMARY KEY, m INTEGER NOT NULL);
+
+INSERT INTO s VALUES (1, 2);
+
+CREATE ASSERTION ge_all_count CHECK (1 >= ALL (SELECT COUNT(*) FROM s));
+CREATE ASSERTION ge_any_count CHECK (1 >= ANY (SELECT COUNT(*) FROM s));
+CREATE ASSERTION ge_count     CHECK (1 >=     (SELECT COUNT(*) FROM s));
+CREATE ASSERTION gt_all_count CHECK (2 >  ALL (SELECT COUNT(*) FROM s));
+CREATE ASSERTION gt_any_count CHECK (2 >  ANY (SELECT COUNT(*) FROM s));
+CREATE ASSERTION gt_count     CHECK (2 >      (SELECT COUNT(*) FROM s));
+CREATE ASSERTION le_all_count CHECK (1 <= ALL (SELECT COUNT(*) FROM s));
+CREATE ASSERTION le_any_count CHECK (0 <= ANY (SELECT COUNT(*) FROM s));
+CREATE ASSERTION le_count     CHECK (0 <=     (SELECT COUNT(*) FROM s));
+CREATE ASSERTION lt_all_count CHECK (0 <  ALL (SELECT COUNT(*) FROM s));
+CREATE ASSERTION lt_any_count CHECK (0 <  ANY (SELECT COUNT(*) FROM s));
+CREATE ASSERTION lt_count     CHECK (0 <      (SELECT COUNT(*) FROM s));
+
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (operations = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('ge_all_count', 'INSERT(s)'),
+       ('ge_any_count', 'INSERT(s)'),
+       ('ge_count',     'INSERT(s)'),
+       ('gt_all_count', 'INSERT(s)'),
+       ('gt_any_count', 'INSERT(s)'),
+       ('gt_count',     'INSERT(s)'),
+       ('le_all_count', 'DELETE(s)'),
+       ('le_any_count', 'DELETE(s)'),
+       ('le_count',     'DELETE(s)'),
+       ('lt_all_count', 'DELETE(s)'),
+       ('lt_any_count', 'DELETE(s)'),
+       ('lt_count',     'DELETE(s)'))
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+
+ROLLBACK;
+
+
+BEGIN TRANSACTION;
+
+--
+-- Expressions involving MIN aggregations
+--
+
+CREATE TABLE s (n INTEGER NOT NULL PRIMARY KEY, m INTEGER NOT NULL);
+
+INSERT INTO s VALUES (1, 2);
+
+CREATE ASSERTION ge_any_min CHECK (1 >= ANY (SELECT MIN(n) FROM s));
+CREATE ASSERTION ge_all_min CHECK (1 >= ALL (SELECT MIN(n) FROM s));
+CREATE ASSERTION ge_min     CHECK (1 >=     (SELECT MIN(n) FROM s));
+CREATE ASSERTION gt_any_min CHECK (2 >  ANY (SELECT MIN(n) FROM s));
+CREATE ASSERTION gt_all_min CHECK (2 >  ALL (SELECT MIN(n) FROM s));
+CREATE ASSERTION gt_min     CHECK (2 >      (SELECT MIN(n) FROM s));
+CREATE ASSERTION le_any_min CHECK (0 <= ANY (SELECT MIN(n) FROM s));
+CREATE ASSERTION le_all_min CHECK (0 <= ALL (SELECT MIN(n) FROM s));
+CREATE ASSERTION le_min     CHECK (0 <=     (SELECT MIN(n) FROM s));
+CREATE ASSERTION lt_any_min CHECK (0 <  ANY (SELECT MIN(n) FROM s));
+CREATE ASSERTION lt_all_min CHECK (0 <  ALL (SELECT MIN(n) FROM s));
+CREATE ASSERTION lt_min     CHECK (0 <      (SELECT MIN(n) FROM s));
+
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (operations = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('ge_all_min', 'DELETE(s) UPDATE(s.n)'),
+       ('ge_any_min', 'DELETE(s) UPDATE(s.n)'),
+       ('ge_min',     'DELETE(s) UPDATE(s.n)'),
+       ('gt_all_min', 'DELETE(s) UPDATE(s.n)'),
+       ('gt_any_min', 'DELETE(s) UPDATE(s.n)'),
+       ('gt_min',     'DELETE(s) UPDATE(s.n)'),
+       ('le_all_min', 'INSERT(s) UPDATE(s.n)'),
+       ('le_any_min', 'INSERT(s) UPDATE(s.n)'),
+       ('le_min',     'INSERT(s) UPDATE(s.n)'),
+       ('lt_all_min', 'INSERT(s) UPDATE(s.n)'),
+       ('lt_any_min', 'INSERT(s) UPDATE(s.n)'),
+       ('lt_min',     'INSERT(s) UPDATE(s.n)'))
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+
+ROLLBACK;
+
+
+BEGIN TRANSACTION;
+
+--
+-- Expressions involving MAX aggregations
+--
+
+CREATE TABLE s (n INTEGER NOT NULL PRIMARY KEY, m INTEGER NOT NULL);
+
+INSERT INTO s VALUES (1, 2);
+
+CREATE ASSERTION ge_any_max CHECK (1 >= ANY (SELECT MAX(n) FROM s));
+CREATE ASSERTION ge_all_max CHECK (1 >= ALL (SELECT MAX(n) FROM s));
+CREATE ASSERTION ge_max     CHECK (1 >=     (SELECT MAX(n) FROM s));
+CREATE ASSERTION gt_all_max CHECK (2 >  ALL (SELECT MAX(n) FROM s));
+CREATE ASSERTION gt_any_max CHECK (2 >  ANY (SELECT MAX(n) FROM s));
+CREATE ASSERTION gt_max     CHECK (2 >      (SELECT MAX(n) FROM s));
+CREATE ASSERTION le_all_max CHECK (0 <= ALL (SELECT MAX(n) FROM s));
+CREATE ASSERTION le_any_max CHECK (0 <= ANY (SELECT MAX(n) FROM s));
+CREATE ASSERTION le_max     CHECK (0 <=     (SELECT MAX(n) FROM s));
+CREATE ASSERTION lt_all_max CHECK (0 <  ALL (SELECT MAX(n) FROM s));
+CREATE ASSERTION lt_any_max CHECK (0 <  ANY (SELECT MAX(n) FROM s));
+CREATE ASSERTION lt_max     CHECK (0 <      (SELECT MAX(n) FROM s));
+
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (operations = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('ge_all_max', 'INSERT(s) UPDATE(s.n)'),
+       ('ge_any_max', 'INSERT(s) UPDATE(s.n)'),
+       ('ge_max',     'INSERT(s) UPDATE(s.n)'),
+       ('gt_all_max', 'INSERT(s) UPDATE(s.n)'),
+       ('gt_any_max', 'INSERT(s) UPDATE(s.n)'),
+       ('gt_max',     'INSERT(s) UPDATE(s.n)'),
+       ('le_all_max', 'DELETE(s) UPDATE(s.n)'),
+       ('le_any_max', 'DELETE(s) UPDATE(s.n)'),
+       ('le_max',     'DELETE(s) UPDATE(s.n)'),
+       ('lt_all_max', 'DELETE(s) UPDATE(s.n)'),
+       ('lt_any_max', 'DELETE(s) UPDATE(s.n)'),
+       ('lt_max',     'DELETE(s) UPDATE(s.n)'))
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+
+ROLLBACK;
+
+
+BEGIN TRANSACTION;
+
+--
+-- Expressions involving BOOL_AND aggregations
+--
+
+CREATE TABLE t (n INTEGER NOT NULL PRIMARY KEY, p BOOLEAN NOT NULL);
+INSERT INTO t (n, p) VALUES (0, TRUE);
+
+CREATE ASSERTION eq_bool_and     CHECK (TRUE  =      (SELECT BOOL_AND(p) FROM t));
+CREATE ASSERTION eq_any_bool_and CHECK (TRUE  =  ANY (SELECT BOOL_AND(p) FROM t));
+CREATE ASSERTION eq_all_bool_and CHECK (TRUE  =  ALL (SELECT BOOL_AND(p) FROM t));
+CREATE ASSERTION ne_bool_and     CHECK (FALSE <>     (SELECT BOOL_AND(p) FROM t));
+CREATE ASSERTION ne_any_bool_and CHECK (FALSE <> ANY (SELECT BOOL_AND(p) FROM t));
+CREATE ASSERTION ne_all_bool_and CHECK (FALSE <> ALL (SELECT BOOL_AND(p) FROM t));
+
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (operations = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('eq_all_bool_and', 'INSERT(t) UPDATE(t.p)'),
+       ('eq_any_bool_and', 'INSERT(t) UPDATE(t.p)'),
+       ('eq_bool_and',     'INSERT(t) UPDATE(t.p)'),
+       ('ne_all_bool_and', 'INSERT(t) UPDATE(t.p)'),
+       ('ne_any_bool_and', 'INSERT(t) UPDATE(t.p)'),
+       ('ne_bool_and',     'INSERT(t) UPDATE(t.p)'))
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+
+ROLLBACK;
+
+
+BEGIN TRANSACTION;
+
+--
+-- Expressions involving BOOL_OR aggregations
+--
+
+CREATE TABLE t (n INTEGER NOT NULL PRIMARY KEY, p BOOLEAN NOT NULL);
+INSERT INTO t (n, p) VALUES (0, TRUE);
+
+CREATE ASSERTION eq_bool_or     CHECK (TRUE  =      (SELECT BOOL_OR(p) FROM t));
+CREATE ASSERTION eq_any_bool_or CHECK (TRUE  =  ANY (SELECT BOOL_OR(p) FROM t));
+CREATE ASSERTION eq_all_bool_or CHECK (TRUE  =  ALL (SELECT BOOL_OR(p) FROM t));
+CREATE ASSERTION ne_bool_or     CHECK (FALSE <>     (SELECT BOOL_OR(p) FROM t));
+CREATE ASSERTION ne_any_bool_or CHECK (FALSE <> ANY (SELECT BOOL_OR(p) FROM t));
+CREATE ASSERTION ne_all_bool_or CHECK (FALSE <> ALL (SELECT BOOL_OR(p) FROM t));
+
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (operations = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('eq_all_bool_or', 'DELETE(t) UPDATE(t.p)'),
+       ('eq_any_bool_or', 'DELETE(t) UPDATE(t.p)'),
+       ('eq_bool_or',     'DELETE(t) UPDATE(t.p)'),
+       ('ne_all_bool_or', 'DELETE(t) UPDATE(t.p)'),
+       ('ne_any_bool_or', 'DELETE(t) UPDATE(t.p)'),
+       ('ne_bool_or',     'DELETE(t) UPDATE(t.p)'))
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+
+ROLLBACK;
+
+
+BEGIN TRANSACTION;
+
+--
+-- Expressions involving window functions
+--
+
+CREATE TABLE t (n INTEGER NOT NULL PRIMARY KEY, p BOOLEAN NOT NULL);
+
+-- Regular window function
+CREATE ASSERTION alternate_p CHECK (
+  NOT EXISTS (
+    SELECT FROM (
+      SELECT LAG(p, 1, NOT p) OVER n <> p
+         AND LEAD(p, 1, NOT p) OVER n <> p
+        FROM t
+      WINDOW n AS (ORDER BY n)
+    ) AS v(q)
+    WHERE NOT q
+  )
+);
+
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (operations = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('alternate_p', 'DELETE(t) INSERT(t) UPDATE(t.n, t.p)'))
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+
+INSERT INTO t (n, p) VALUES (10, TRUE);
+INSERT INTO t (n, p) VALUES (20, FALSE);
+INSERT INTO t (n, p) VALUES (30, TRUE);
+
+SAVEPOINT pre_insert;
+INSERT INTO t (n, p) VALUES (40, TRUE);
+ROLLBACK TO SAVEPOINT pre_insert;
+
+SAVEPOINT pre_delete;
+DELETE FROM t WHERE n = 20;
+ROLLBACK TO SAVEPOINT pre_delete;
+
+SAVEPOINT pre_update;
+UPDATE t SET p = NOT p WHERE n = 20;
+ROLLBACK TO SAVEPOINT pre_update;
+
+DROP ASSERTION alternate_p;
+
+-- Aggregate function over a window
+CREATE TABLE s (n INTEGER NOT NULL PRIMARY KEY, m INTEGER NOT NULL);
+
+INSERT INTO s (n, m)
+SELECT n, (2 * n)
+  FROM GENERATE_SERIES(0, 9) AS ns(n);
+
+CREATE ASSERTION max_over_window CHECK ((10, 20) > ALL (SELECT n, MAX(m) OVER (ORDER BY n) FROM s));
+
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (operations = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('max_over_window', 'INSERT(s) UPDATE(s.m, s.n)'))
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+
+ROLLBACK;
+
+
+BEGIN TRANSACTION;
+
+--
+-- Expressions containing a comparison should have the same invalidating
+-- operations as an expression containing the inverse comparison operation
+-- and where the operands have been switched
+--
+
+CREATE TABLE r (n INTEGER NOT NULL PRIMARY KEY);
+
+INSERT INTO r (n) VALUES (9);
+
+-- ">" and "<" are opposites
+CREATE ASSERTION a CHECK (10 > (SELECT MIN(n) FROM r));
+CREATE ASSERTION b CHECK ((SELECT MIN(n) FROM r) < 10);
+
+-- "<" and ">" are opposites
+CREATE ASSERTION c CHECK (8 < (SELECT MIN(n) FROM r));
+CREATE ASSERTION d CHECK ((SELECT MIN(n) FROM r) > 8);
+
+-- ">=" and "<=" are opposites
+CREATE ASSERTION e CHECK (9 >= (SELECT MIN(n) FROM r));
+CREATE ASSERTION f CHECK ((SELECT MIN(n) FROM r) <= 9);
+
+-- "<=" and ">=" are opposites
+CREATE ASSERTION g CHECK (9 <= (SELECT MIN(n) FROM r));
+CREATE ASSERTION h CHECK ((SELECT MIN(n) FROM r) >= 9);
+
+SELECT (test).predicate, (test).correct
+  FROM (
+VALUES ('a', 'b'),
+       ('c', 'd'),
+       ('e', 'f'),
+       ('g', 'h'))
+    AS v(a, b)
+ CROSS JOIN LATERAL (SELECT have_identical_invalidating_operations(v.a, v.b))
+    AS w(test)
+ ORDER BY 1, 2;
+
+ROLLBACK;
+
+
+BEGIN TRANSACTION;
+
+--
+-- The INFORMATION_SCHEMA should contain the correct information
+--
+
+CREATE TABLE t (n INTEGER NOT NULL PRIMARY KEY, p BOOLEAN NOT NULL);
+CREATE ASSERTION a CHECK (NOT EXISTS (SELECT FROM t WHERE p));
+
+SELECT predicate,
+       CASE WHEN
+         truth THEN 'YES'
+               ELSE 'NO'
+       END AS correct
+  FROM ( 
+VALUES ('INFORMATION_SCHEMA.CONSTRAINT_TABLE_USAGE contains only the correct tables',
+        EXISTS (
+          SELECT FROM information_schema.constraint_table_usage
+           WHERE (constraint_name, table_name) = ('a', 't'))
+        AND NOT EXISTS (
+          SELECT FROM information_schema.constraint_table_usage
+           WHERE constraint_name = 'a'
+             AND table_name <> 't')),
+       ('INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE contains only the correct columns (t.p)',
+        EXISTS (
+          SELECT FROM information_schema.constraint_column_usage
+           WHERE (constraint_name, table_name, column_name) = ('a', 't', 'p'))
+        AND NOT EXISTS (
+          SELECT FROM information_schema.constraint_column_usage
+           WHERE constraint_name ='a'
+             AND (table_name, column_name) <> ('t', 'p'))),
+       ('INFORMATION_SCHEMA.ASSERTIONS contains only the correct assertions',
+        EXISTS (
+          SELECT FROM information_schema.assertions
+           WHERE constraint_name = 'a')
+        AND NOT EXISTS (
+          SELECT FROM information_schema.assertions
+           WHERE constraint_name <> 'a')))
+    AS v(predicate, truth);
+
+ROLLBACK;
+
+BEGIN TRANSACTION;
+-- TODO This needs rethinking
+/*
+CREATE TABLE s (n INTEGER NOT NULL PRIMARY KEY, m INTEGER NOT NULL);
+CREATE TABLE t (n INTEGER NOT NULL PRIMARY KEY, p BOOLEAN NOT NULL);
+CREATE VIEW v AS SELECT * FROM s INNER JOIN t USING (n);
+
+CREATE ASSERTION a CHECK (NOT EXISTS (SELECT FROM v));
+
+-- we should depend on the view not the tables
+SELECT OK(depends_on('a', 'v'),
+          'Assertion depends upon the view v it references');
+SELECT OK(NOT depends_on('a', 's'),
+          'Assertion does not depend upon the table s referenced in the view v');
+SELECT OK(NOT depends_on('a', 't'),
+          'Assertion does not depend upon the table t referenced in the view v');
+
+-- we should trigger on the tables and not the view
+SELECT OK(EXISTS(SELECT FROM assertion_check_operation
+                  WHERE (assertion_name, relation_name) = ('a', 's')),
+          'Assertion is checked on modifications to table s');
+SELECT OK(EXISTS(SELECT FROM assertion_check_operation
+                  WHERE (assertion_name, relation_name) = ('a', 't')),
+          'Assertion is checked on modifications to table t');
+SELECT OK(NOT EXISTS(SELECT FROM assertion_check_operation
+                      WHERE (assertion_name, relation_name) = ('a', 'v')),
+          'Assertion is not checked on modifications to view v');
+*/
+ROLLBACK;
+
+-- TODO test commonalities between count, min, max, etc, for optimisations
+-- TODO ensure window function aggregates are treated the same as regular aggregates
+-- TODO ensure that conflicting aggregate functions are not incorrectly optimised
+-- TODO ensure that recorded dependencies are correct (traversal into functions and views)
+
+BEGIN TRANSACTION;
+
+DROP FUNCTION have_identical_invalidating_operations;
+DROP VIEW invalidating_summary;
+DROP VIEW invalidating_by_update_of_column;
+DROP VIEW invalidating_operation;
+
+COMMIT;
-- 
2.15.0

#19David Fetter
david@fetter.org
In reply to: Joe Wildish (#18)
Re: Implementing SQL ASSERTION

On Sun, Mar 18, 2018 at 12:29:50PM +0000, Joe Wildish wrote:

This patch no longer applies. Any chance of a rebase?

Attached is a rebased version of this patch. It takes into account the ACL checking changes and a few other minor amendments.

Thanks!

Best,
David.
--
David Fetter <david(at)fetter(dot)org> http://fetter.org/
Phone: +1 415 235 3778

Remember to vote!
Consider donating to Postgres: http://www.postgresql.org/about/donate

#20David Fetter
david@fetter.org
In reply to: Joe Wildish (#18)
Re: Implementing SQL ASSERTION

On Sun, Mar 18, 2018 at 12:29:50PM +0000, Joe Wildish wrote:

This patch no longer applies. Any chance of a rebase?

Attached is a rebased version of this patch. It takes into account
the ACL checking changes and a few other minor amendments.

Sorry to bother you again, but this now doesn't compile atop master.

Best,
David.
--
David Fetter <david(at)fetter(dot)org> http://fetter.org/
Phone: +1 415 235 3778

Remember to vote!
Consider donating to Postgres: http://www.postgresql.org/about/donate

#21Joe Wildish
joe-postgresql.org@elusive.cx
In reply to: David Fetter (#20)
1 attachment(s)
Re: Implementing SQL ASSERTION

On 28 Mar 2018, at 16:13, David Fetter <david@fetter.org> wrote:

Sorry to bother you again, but this now doesn't compile atop master.

Attached is a rebased patch for the prototype.

Cheers,
-Joe

Attachments:

0001-SQL-ASSERTION-prototype.patchapplication/octet-stream; name=0001-SQL-ASSERTION-prototype.patch; x-unix-mode=0600Download
From bf4acbf4defacc89adc6ae08eea98314164a6052 Mon Sep 17 00:00:00 2001
From: Joe Wildish <joe@elusive.cx>
Date: Sun, 29 Apr 2018 19:10:26 +0100
Subject: [PATCH] SQL ASSERTION prototype

---
 doc/src/sgml/ddl.sgml                      |   31 +
 doc/src/sgml/ref/allfiles.sgml             |    3 +
 doc/src/sgml/ref/alter_assertion.sgml      |  139 ++++
 doc/src/sgml/ref/create_assertion.sgml     |  133 ++++
 doc/src/sgml/ref/drop_assertion.sgml       |  115 +++
 doc/src/sgml/reference.sgml                |    3 +
 src/backend/catalog/aclchk.c               |   33 +
 src/backend/catalog/information_schema.sql |   60 +-
 src/backend/catalog/namespace.c            |  121 +++
 src/backend/catalog/objectaddress.c        |   11 +
 src/backend/catalog/pg_constraint.c        |   20 +-
 src/backend/commands/alter.c               |    6 +
 src/backend/commands/constraint.c          | 1101 +++++++++++++++++++++++++++-
 src/backend/commands/event_trigger.c       |    6 +
 src/backend/nodes/copyfuncs.c              |   14 +
 src/backend/nodes/equalfuncs.c             |   12 +
 src/backend/parser/gram.y                  |   80 +-
 src/backend/parser/parse_agg.c             |    2 +
 src/backend/parser/parse_expr.c            |    2 +
 src/backend/parser/parse_func.c            |    4 +
 src/backend/tcop/utility.c                 |   20 +
 src/backend/utils/cache/lsyscache.c        |   19 +
 src/bin/psql/command.c                     |    3 +
 src/bin/psql/describe.c                    |   92 +++
 src/bin/psql/describe.h                    |    3 +
 src/bin/psql/help.c                        |    1 +
 src/bin/psql/tab-complete.c                |   30 +-
 src/include/catalog/namespace.h            |    1 +
 src/include/catalog/pg_constraint.h        |    5 +-
 src/include/catalog/pg_proc.dat            |    4 +
 src/include/commands/constraint.h          |   10 +
 src/include/nodes/nodes.h                  |    1 +
 src/include/nodes/parsenodes.h             |   11 +
 src/include/parser/parse_node.h            |    1 +
 src/include/utils/acl.h                    |    1 +
 src/include/utils/lsyscache.h              |    1 +
 src/test/regress/expected/assertions.out   |  779 ++++++++++++++++++++
 src/test/regress/parallel_schedule         |    2 +-
 src/test/regress/serial_schedule           |    1 +
 src/test/regress/sql/assertions.sql        |  775 ++++++++++++++++++++
 40 files changed, 3587 insertions(+), 69 deletions(-)
 create mode 100644 doc/src/sgml/ref/alter_assertion.sgml
 create mode 100644 doc/src/sgml/ref/create_assertion.sgml
 create mode 100644 doc/src/sgml/ref/drop_assertion.sgml
 create mode 100644 src/include/commands/constraint.h
 create mode 100644 src/test/regress/expected/assertions.out
 create mode 100644 src/test/regress/sql/assertions.sql

diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index 34da0d8d57..9340931fb5 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -917,6 +917,37 @@ CREATE TABLE circles (
     of the type specified in the constraint declaration.
    </para>
   </sect2>
+
+  <sect2 id="ddl-constraints-assertions">
+   <title>Assertions</title>
+   
+   <indexterm zone="ddl-constraints-assertions">
+    <primary>assertion</primary>
+   </indexterm>
+
+   <para>
+    An assertion is a constraint that is not part of a table
+    definition.  An assertion can define constraints that evaluate the
+    data across multiple rows of a table beyond what unique and
+    exclusion constraints can do, and assertions can look at the data
+    in multiple tables.
+   </para>
+
+   <para>
+    An assertion is a separate schema object and is created with the
+    command <command>CREATE ASSERTION</command>.  The constraint
+    expression is written as a <literal>CHECK</literal> clause like in
+    check constraints.  For instance, to ensure that there is always
+    at least one entry in the product table:
+<programlisting>
+CREATE ASSERTION products_not_empty CHECK ((SELECT count(*) FROM products) &gt; 0);
+</programlisting>
+    Assertions will often involve aggregate functions computed over
+    entire tables.  Note, however, that this kind of assertion can be
+   quite inefficient and should only be used on tables that are small
+    and change rarely.
+   </para>
+  </sect2>
  </sect1>
 
  <sect1 id="ddl-system-columns">
diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index c81c87ef41..c06efc48c3 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -7,6 +7,7 @@ Complete list of usable sgml source files in this directory.
 <!-- SQL commands -->
 <!ENTITY abort              SYSTEM "abort.sgml">
 <!ENTITY alterAggregate     SYSTEM "alter_aggregate.sgml">
+<!ENTITY alterAssertion     SYSTEM "alter_assertion.sgml">
 <!ENTITY alterCollation     SYSTEM "alter_collation.sgml">
 <!ENTITY alterConversion    SYSTEM "alter_conversion.sgml">
 <!ENTITY alterDatabase      SYSTEM "alter_database.sgml">
@@ -60,6 +61,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY copyTable          SYSTEM "copy.sgml">
 <!ENTITY createAccessMethod SYSTEM "create_access_method.sgml">
 <!ENTITY createAggregate    SYSTEM "create_aggregate.sgml">
+<!ENTITY createAssertion    SYSTEM "create_assertion.sgml">
 <!ENTITY createCast         SYSTEM "create_cast.sgml">
 <!ENTITY createCollation    SYSTEM "create_collation.sgml">
 <!ENTITY createConversion   SYSTEM "create_conversion.sgml">
@@ -107,6 +109,7 @@ Complete list of usable sgml source files in this directory.
 <!ENTITY do                 SYSTEM "do.sgml">
 <!ENTITY dropAccessMethod   SYSTEM "drop_access_method.sgml">
 <!ENTITY dropAggregate      SYSTEM "drop_aggregate.sgml">
+<!ENTITY dropAssertion      SYSTEM "drop_assertion.sgml">
 <!ENTITY dropCast           SYSTEM "drop_cast.sgml">
 <!ENTITY dropCollation      SYSTEM "drop_collation.sgml">
 <!ENTITY dropConversion     SYSTEM "drop_conversion.sgml">
diff --git a/doc/src/sgml/ref/alter_assertion.sgml b/doc/src/sgml/ref/alter_assertion.sgml
new file mode 100644
index 0000000000..b06417f740
--- /dev/null
+++ b/doc/src/sgml/ref/alter_assertion.sgml
@@ -0,0 +1,139 @@
+<!--
+doc/src/sgml/ref/alter_assertion.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-alterassertion">
+
+ <indexterm zone="sql-alterassertion">
+  <primary>ALTER ASSERTION</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>ALTER ASSERTION</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>ALTER ASSERTION</refname>
+  <refpurpose>change the definition of an assertion</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+ALTER ASSERTION <replaceable>name</replaceable> RENAME TO <replaceable>new_name</replaceable>
+ALTER ASSERTION <replaceable>name</replaceable> OWNER TO <replaceable>new_owner</replaceable>
+ALTER ASSERTION <replaceable>name</replaceable> SET SCHEMA <replaceable>new_schema</replaceable>
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>ALTER ASSERTION</command> changes the definition of an
+   assertion.
+  </para>
+
+  <para>
+   You must own the assertion to use <command>ALTER ASSERTION</command>.  To
+   change the schema of an assertion, you must also have
+   <literal>CREATE</literal> privilege on the new schema.  To alter
+   the owner, you must also be a direct or indirect member of the new
+   owning role, and that role must have <literal>CREATE</literal>
+   privilege on the assertion's schema.  (These restrictions enforce
+   that altering the owner doesn't do anything you couldn't do by
+   dropping and recreating the assertion.  However, a superuser can
+   alter ownership of any assertion 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 existing assertion.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_name</replaceable></term>
+    <listitem>
+     <para>
+      The new name of the assertion.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_owner</replaceable></term>
+    <listitem>
+     <para>
+      The new owner of the assertion.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_schema</replaceable></term>
+    <listitem>
+     <para>
+      The new schema for the assertion.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To rename the assertuib <literal>check_size</literal>
+   to <literal>check_count</literal>:
+<programlisting>
+ALTER ASSERTION check_size RENAME TO check_count;
+</programlisting>
+  </para>
+
+  <para>
+   To change the owner of the assertion <literal>check_size</literal>
+   to <literal>joe</literal>:
+<programlisting>
+ALTER ASSERTION check_size OWNER TO joe;
+</programlisting>
+  </para>
+
+  <para>
+   To move the assertion <literal>check_size</literal> into
+   schema <literal>myschema</literal>:
+<programlisting>
+ALTER ASSERTION check_size SET SCHEMA myschema;
+</programlisting>
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   There is no <command>ALTER ASSERTION</command> statement in the SQL
+   standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-createassertion"/></member>
+   <member><xref linkend="sql-dropassertion"/></member>
+  </simplelist>
+ </refsect1>
+</refentry>
diff --git a/doc/src/sgml/ref/create_assertion.sgml b/doc/src/sgml/ref/create_assertion.sgml
new file mode 100644
index 0000000000..b36d1b4ecb
--- /dev/null
+++ b/doc/src/sgml/ref/create_assertion.sgml
@@ -0,0 +1,133 @@
+<!--
+doc/src/sgml/ref/create_assertion.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-createassertion">
+ <indexterm zone="sql-createassertion">
+  <primary>CREATE ASSERTION</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>CREATE ASSERTION</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>CREATE ASSERTION</refname>
+  <refpurpose>define a new assertion</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+CREATE ASSERTION <replaceable class="parameter">name</replaceable> CHECK ( <replaceable class="parameter">name</replaceable> ) [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>CREATE ASSERTION</command> creates an assertion.  An
+   assertion is a check constraint that is independent of a table row
+   and a table.  It can therefore be used to enforce more complex
+   constraints across multiple table rows and across multiple tables.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><replaceable class="parameter">name</replaceable></term>
+    <listitem>
+     <para>
+      The name (optionally schema-qualified) of the assertion to
+      create.  Assertions use the same namespace as constraints on
+      tables.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>CHECK ( <replaceable class="parameter">expression</replaceable> )</literal></term>
+    <listitem>
+     <para>
+      The <literal>CHECK</literal> clause specifies an expression producing a
+      Boolean result which the database must satisfy at all times for a
+      data change operation to succeed.  Expressions evaluating
+      to TRUE or UNKNOWN succeed.  Should the result of a data change
+      operation produce a FALSE result an error exception is
+      raised and the change is not made.
+     </para>
+
+     <para>
+      The check expression typically involves subselects in order to
+      read data from tables.  See the examples below.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>DEFERRABLE</literal></term>
+    <term><literal>NOT DEFERRABLE</literal></term>
+    <term><literal>INITIALLY IMMEDIATE</literal></term>
+    <term><literal>INITIALLY DEFERRED</literal></term>
+    <listitem>
+     <para>
+      These clauses control the deferrability of the constraint.  See
+      <xref linkend="sql-createtable"/> for an explanation.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Notes</title>
+
+  <para>
+   More specialized constraint forms such as table check constraints,
+   foreign-key constraints, or exclusion constraints should be used
+   instead when applicable, because they will be more efficient.
+  </para>
+
+  <para>
+   Assertion checks are not specially optimized.  For example,
+   checking the row count of a large table in an assertion will be
+   just as slow as implementing the check manually.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   Check that the table <literal>table1</literal> has at most 30 rows:
+<programlisting>
+CREATE ASSERTION table1_max30 CHECK ((SELECT count(*) FROM table1) &lt;= 30);
+</programlisting>
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   <command>CREATE ASSERTION</command> conforms to the SQL standard.
+   The PostgreSQL implementation has certain restrictions on what
+   check expressions are allowed in assertions.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-alterassertion"/></member>
+   <member><xref linkend="sql-dropassertion"/></member>
+  </simplelist>
+ </refsect1>
+</refentry>
diff --git a/doc/src/sgml/ref/drop_assertion.sgml b/doc/src/sgml/ref/drop_assertion.sgml
new file mode 100644
index 0000000000..39a60a0d29
--- /dev/null
+++ b/doc/src/sgml/ref/drop_assertion.sgml
@@ -0,0 +1,115 @@
+<!--
+doc/src/sgml/ref/drop_assertion.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-dropassertion">
+
+ <indexterm zone="sql-dropassertion">
+  <primary>DROP ASSERTION</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>DROP ASSERTION</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>DROP ASSERTION</refname>
+  <refpurpose>remove an assertion</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+DROP ASSERTION [ IF EXISTS ] <replaceable class="parameter">name</replaceable> [, ...] [ CASCADE | RESTRICT ]
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>DROP ASSERTION</command> removes an existing assertion. To
+   execute this command the current user must be the owner of the
+   assertion.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+
+   <varlistentry>
+    <term><literal>IF EXISTS</literal></term>
+    <listitem>
+     <para>
+      Do not throw an error if the assertion 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 existing assertion.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>CASCADE</literal></term>
+    <listitem>
+     <para>
+      Automatically drop objects that depend on the assertion.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>RESTRICT</literal></term>
+    <listitem>
+     <para>
+      Refuse to drop the assertion if any objects depend on it.  This
+      is the default.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To remove the assertion <literal>check_size</literal>:
+<programlisting>
+DROP ASSERTION check_size;
+</programlisting>
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   This command conforms to the SQL standard, except that the standard
+   only allows one assertion to be dropped per command, and apart from
+   the <literal>IF EXISTS</literal> option, which is
+   a <productname>PostgreSQL</productname> extension.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-alterassertion"/></member>
+   <member><xref linkend="sql-createassertion"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index 73ef212c08..cca66b6391 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -35,6 +35,7 @@
 
    &abort;
    &alterAggregate;
+   &alterAssertion;
    &alterCollation;
    &alterConversion;
    &alterDatabase;
@@ -88,6 +89,7 @@
    &copyTable;
    &createAccessMethod;
    &createAggregate;
+   &createAssertion;
    &createCast;
    &createCollation;
    &createConversion;
@@ -135,6 +137,7 @@
    &do;
    &dropAccessMethod;
    &dropAggregate;
+   &dropAssertion;
    &dropCast;
    &dropCollation;
    &dropConversion;
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 578e4c6592..bec1ad9381 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -3363,6 +3363,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_AGGREGATE:
 						msg = gettext_noop("permission denied for aggregate %s");
 						break;
+					case OBJECT_ASSERTION:
+						msg = gettext_noop("permission denied for assertion %s");
+						break;
 					case OBJECT_COLLATION:
 						msg = gettext_noop("permission denied for collation %s");
 						break;
@@ -3494,6 +3497,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_AGGREGATE:
 						msg = gettext_noop("must be owner of aggregate %s");
 						break;
+					case OBJECT_ASSERTION:
+						msg = gettext_noop("must be owner of assertion %s");
+						break;
 					case OBJECT_COLLATION:
 						msg = gettext_noop("must be owner of collation %s");
 						break;
@@ -5214,6 +5220,33 @@ pg_collation_ownercheck(Oid coll_oid, Oid roleid)
 	return has_privs_of_role(roleid, ownerId);
 }
 
+/*
+ * Ownership check for a constraint (specified by OID).
+ */
+bool
+pg_constraint_ownercheck(Oid constr_oid, Oid roleid)
+{
+	HeapTuple   tuple;
+	//Oid           ownerId;
+
+	/* Superusers bypass all permission checking. */
+	if (superuser_arg(roleid))
+		return true;
+
+	tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(constr_oid));
+	if (!HeapTupleIsValid(tuple))
+		ereport(ERROR,
+			    (errcode(ERRCODE_UNDEFINED_OBJECT),
+			     errmsg("constraint with OID %u does not exist", constr_oid)));
+
+	//FIXME: ownerId = ((Form_pg_constraint) GETSTRUCT(tuple))->conowner;
+
+	ReleaseSysCache(tuple);
+
+	//return has_privs_of_role(roleid, ownerId);
+	return true; // for now
+}
+
 /*
  * Ownership check for a conversion (specified by OID).
  */
diff --git a/src/backend/catalog/information_schema.sql b/src/backend/catalog/information_schema.sql
index f4e69f4a26..23cc0759a3 100644
--- a/src/backend/catalog/information_schema.sql
+++ b/src/backend/catalog/information_schema.sql
@@ -285,7 +285,20 @@ GRANT SELECT ON administrable_role_authorizations TO PUBLIC;
  * ASSERTIONS view
  */
 
--- feature not supported
+CREATE VIEW assertions AS
+    SELECT CAST(current_database() AS sql_identifier) AS constraint_catalog,
+           CAST(n.nspname AS sql_identifier) AS constraint_schema,
+           CAST(con.conname AS sql_identifier) AS constraint_name,
+           CAST(CASE WHEN condeferrable THEN 'YES' ELSE 'NO' END
+             AS yes_or_no) AS is_deferrable,
+           CAST(CASE WHEN condeferred THEN 'YES' ELSE 'NO' END
+             AS yes_or_no) AS initially_deferred
+    FROM pg_namespace n, pg_constraint con
+    WHERE n.oid = con.connamespace
+          AND con.conrelid = 0 AND con.contypid = 0;
+          -- TODO: AND pg_has_role(con.conowner, 'USAGE');
+
+GRANT SELECT ON assertions TO PUBLIC;
 
 
 /*
@@ -790,7 +803,7 @@ CREATE VIEW constraint_column_usage AS
            CAST(cstrname AS sql_identifier) AS constraint_name
 
     FROM (
-        /* check constraints */
+        /* assertions and check constraints */
         SELECT DISTINCT nr.nspname, r.relname, r.relowner, a.attname, nc.nspname, c.conname
           FROM pg_namespace nr, pg_class r, pg_attribute a, pg_depend d, pg_namespace nc, pg_constraint c
           WHERE nr.oid = r.relnamespace
@@ -802,7 +815,7 @@ CREATE VIEW constraint_column_usage AS
             AND d.objid = c.oid
             AND c.connamespace = nc.oid
             AND c.contype = 'c'
-            AND r.relkind IN ('r', 'p')
+            AND r.relkind IN ('r', 'p', 'v')
             AND NOT a.attisdropped
 
         UNION ALL
@@ -842,20 +855,41 @@ GRANT SELECT ON constraint_column_usage TO PUBLIC;
 
 CREATE VIEW constraint_table_usage AS
     SELECT CAST(current_database() AS sql_identifier) AS table_catalog,
-           CAST(nr.nspname AS sql_identifier) AS table_schema,
-           CAST(r.relname AS sql_identifier) AS table_name,
+           CAST(tblschema AS sql_identifier) AS table_schema,
+           CAST(tblname AS sql_identifier) AS table_name,
            CAST(current_database() AS sql_identifier) AS constraint_catalog,
-           CAST(nc.nspname AS sql_identifier) AS constraint_schema,
-           CAST(c.conname AS sql_identifier) AS constraint_name
+           CAST(cstrschema AS sql_identifier) AS constraint_schema,
+           CAST(cstrname AS sql_identifier) AS constraint_name
+
+    FROM (
+        /* assertions and check constraints */
+        SELECT DISTINCT nr.nspname, r.relname, r.relowner, nc.nspname, c.conname
+          FROM pg_namespace nr, pg_class r,
+               pg_depend d, pg_namespace nc, pg_constraint c
+         WHERE nr.oid = r.relnamespace
+           AND d.refobjid = r.oid
+           AND c.connamespace = nc.oid
+           AND d.objid = c.oid
+           AND d.refclassid = 'pg_catalog.pg_class'::regclass
+           AND d.classid = 'pg_catalog.pg_constraint'::regclass
+           AND c.contype = 'c'
+           AND r.relkind IN ('r', 'p', 'v')
 
-    FROM pg_constraint c, pg_namespace nc,
-         pg_class r, pg_namespace nr
+        UNION ALL
 
-    WHERE c.connamespace = nc.oid AND r.relnamespace = nr.oid
-          AND ( (c.contype = 'f' AND c.confrelid = r.oid)
+        /* unique/primary key constraints */
+        SELECT nr.nspname, r.relname, r.relowner, nc.nspname, c.conname
+          FROM pg_constraint c, pg_namespace nc,
+               pg_class r, pg_namespace nr
+         WHERE c.connamespace = nc.oid
+           AND r.relnamespace = nr.oid
+           AND ( (c.contype = 'f' AND c.confrelid = r.oid)
              OR (c.contype IN ('p', 'u') AND c.conrelid = r.oid) )
-          AND r.relkind IN ('r', 'p')
-          AND pg_has_role(r.relowner, 'USAGE');
+           AND r.relkind IN ('r', 'p')
+
+    ) AS x (tblschema, tblname, tblowner, cstrschema, cstrname)
+
+    WHERE pg_has_role(x.tblowner, 'USAGE');
 
 GRANT SELECT ON constraint_table_usage TO PUBLIC;
 
diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c
index 0f67a122ed..17f9c82f3d 100644
--- a/src/backend/catalog/namespace.c
+++ b/src/backend/catalog/namespace.c
@@ -27,6 +27,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_authid.h"
 #include "catalog/pg_collation.h"
+#include "catalog/pg_constraint.h"
 #include "catalog/pg_conversion.h"
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
@@ -56,9 +57,11 @@
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/syscache.h"
+#include "utils/fmgroids.h"
 #include "utils/varlena.h"
 
 
+
 /*
  * The namespace search path is a possibly-empty list of namespace OIDs.
  * In addition to the explicit list, implicitly-searched namespaces
@@ -3502,6 +3505,124 @@ PopOverrideSearchPath(void)
 }
 
 
+static Oid
+get_assertion_oid_internal(Relation pg_constraint, char *assertion_name, Oid namespaceId, List *name)
+{
+	SysScanDesc scan;
+	ScanKeyData skey[4];
+	HeapTuple	tuple;
+	Oid			conOid = InvalidOid;
+
+	ScanKeyInit(&skey[0],
+				Anum_pg_constraint_conname,
+				BTEqualStrategyNumber, F_NAMEEQ,
+				PointerGetDatum(assertion_name));
+
+	ScanKeyInit(&skey[1],
+				Anum_pg_constraint_connamespace,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(namespaceId));
+
+	ScanKeyInit(&skey[2],
+				Anum_pg_constraint_conrelid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				InvalidOid);
+
+	ScanKeyInit(&skey[3],
+				Anum_pg_constraint_contypid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				InvalidOid);
+
+	scan = systable_beginscan(pg_constraint, InvalidOid, false,
+							  NULL, 4, skey);
+
+	/*
+	 * Fetch the constraint tuple from pg_constraint.  There may be
+	 * more than one match, because constraints are not required to
+	 * have unique names; if so, error out.
+	 */
+	while (HeapTupleIsValid(tuple = systable_getnext(scan)))
+	{
+		Form_pg_constraint con = (Form_pg_constraint) GETSTRUCT(tuple);
+
+		if (OidIsValid(con->conrelid) || OidIsValid(con->contypid))
+			ereport(ERROR,
+					(errmsg("constraint \"%s\" is not an assertion",
+							NameListToString(name))));
+
+		if (strcmp(NameStr(con->conname), assertion_name) == 0)
+		{
+			if (OidIsValid(conOid))
+				ereport(ERROR,
+						(errcode(ERRCODE_DUPLICATE_OBJECT),
+								errmsg("there are multiple assertions named \"%s\"",
+									   NameListToString(name))));
+			conOid = HeapTupleGetOid(tuple);
+		}
+	}
+
+	systable_endscan(scan);
+
+	return conOid;
+}
+
+/*
+ * get_assertion_oid
+ *		Find an assertion with the specified name.
+ *		Returns constraint's OID.
+ */
+Oid
+get_assertion_oid(List *name, bool missing_ok)
+{
+	char	   *schemaname;
+	char	   *assertion_name;
+	Oid			namespaceId;
+	Relation	pg_constraint;
+	Oid			conOid = InvalidOid;
+
+	DeconstructQualifiedName(name, &schemaname, &assertion_name);
+
+	pg_constraint = heap_open(ConstraintRelationId, AccessShareLock);
+
+	if (schemaname)
+	{
+		namespaceId = LookupExplicitNamespace(schemaname, missing_ok);
+		conOid = get_assertion_oid_internal(pg_constraint, assertion_name, namespaceId, name);
+	}
+	else
+	{
+		ListCell   *l;
+
+		recomputeNamespacePath();
+
+		foreach(l, activeSearchPath)
+		{
+			namespaceId = lfirst_oid(l);
+
+			if (namespaceId == myTempNamespace)
+				continue;		/* do not look in temp namespace */
+
+			conOid = get_assertion_oid_internal(pg_constraint, assertion_name, namespaceId, name);
+
+			if (OidIsValid(conOid))
+				break;
+		}
+	}
+
+	heap_close(pg_constraint, AccessShareLock);
+
+	/* If no such constraint exists, complain */
+	if (!OidIsValid(conOid) && !missing_ok)
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+						errmsg("assertion \"%s\" does not exist",
+							   NameListToString(name))));
+
+	return conOid;
+}
+
+
+
 /*
  * get_collation_oid - find a collation by possibly qualified name
  *
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index ef3ea64bd0..a513266a9e 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -898,6 +898,11 @@ get_object_address(ObjectType objtype, Node *object,
 				address.objectId = LookupOperWithArgs(castNode(ObjectWithArgs, object), missing_ok);
 				address.objectSubId = 0;
 				break;
+			case OBJECT_ASSERTION:
+				address.classId = ConstraintRelationId;
+				address.objectId = get_assertion_oid(castNode(List, object), missing_ok);
+				address.objectSubId = 0;
+				break;
 			case OBJECT_COLLATION:
 				address.classId = CollationRelationId;
 				address.objectId = get_collation_oid(castNode(List, object), missing_ok);
@@ -2116,6 +2121,7 @@ pg_get_object_address(PG_FUNCTION_ARGS)
 		case OBJECT_FOREIGN_TABLE:
 		case OBJECT_COLUMN:
 		case OBJECT_ATTRIBUTE:
+		case OBJECT_ASSERTION:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_STATISTIC_EXT:
@@ -2276,6 +2282,11 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
 				aclcheck_error(ACLCHECK_NOT_OWNER, objtype,
 							   strVal((Value *) object));
 			break;
+		case OBJECT_ASSERTION:
+			if (!pg_constraint_ownercheck(address.objectId, roleid))
+				aclcheck_error(ACLCHECK_NOT_OWNER, objtype,
+							   NameListToString(castNode(List, object)));
+			break;
 		case OBJECT_COLLATION:
 			if (!pg_collation_ownercheck(address.objectId, roleid))
 				aclcheck_error(ACLCHECK_NOT_OWNER, objtype,
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index 7a6d158f89..975ca60192 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -692,6 +692,13 @@ ConstraintNameIsUsed(ConstraintCategory conCat, Oid objId,
 			found = true;
 			break;
 		}
+		else if (conCat == CONSTRAINT_ASSERTION
+				 && con->conrelid == InvalidOid
+				 && con->contypid == InvalidOid)
+		{
+			found = true;
+			break;
+		}
 	}
 
 	systable_endscan(conscan);
@@ -863,8 +870,7 @@ RemoveConstraintById(Oid conId)
 		 * but we have no such concept at the moment.
 		 */
 	}
-	else
-		elog(ERROR, "constraint %u is not of a known type", conId);
+	/* Else it's an assertion; nothing special for that. */
 
 	/* Fry the constraint itself */
 	CatalogTupleDelete(conDesc, &tup->t_self);
@@ -920,6 +926,16 @@ RenameConstraintById(Oid conId, const char *newname)
 				(errcode(ERRCODE_DUPLICATE_OBJECT),
 				 errmsg("constraint \"%s\" for domain %s already exists",
 						newname, format_type_be(con->contypid))));
+	if (!OidIsValid(con->conrelid) &&
+		!OidIsValid(con->contypid) &&
+		ConstraintNameIsUsed(CONSTRAINT_ASSERTION,
+							 InvalidOid,
+							 con->connamespace,
+							 newname))
+		ereport(ERROR,
+				(errcode(ERRCODE_DUPLICATE_OBJECT),
+						errmsg("assertion \"%s\" already exists",
+							   newname)));
 
 	/* OK, do the rename --- tuple is a copy, so OK to scribble on it */
 	namestrcpy(&(con->conname), newname);
diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c
index eff325cc7d..a9aa44ee5c 100644
--- a/src/backend/commands/alter.c
+++ b/src/backend/commands/alter.c
@@ -41,6 +41,7 @@
 #include "commands/alter.h"
 #include "commands/collationcmds.h"
 #include "commands/conversioncmds.h"
+#include "commands/constraint.h"
 #include "commands/dbcommands.h"
 #include "commands/defrem.h"
 #include "commands/event_trigger.h"
@@ -326,6 +327,9 @@ ExecRenameStmt(RenameStmt *stmt)
 {
 	switch (stmt->renameType)
 	{
+		case OBJECT_ASSERTION:
+			return RenameAssertion(stmt);
+
 		case OBJECT_TABCONSTRAINT:
 		case OBJECT_DOMCONSTRAINT:
 			return RenameConstraint(stmt);
@@ -491,6 +495,7 @@ ExecAlterObjectSchemaStmt(AlterObjectSchemaStmt *stmt,
 
 			/* generic code path */
 		case OBJECT_AGGREGATE:
+		case OBJECT_ASSERTION:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_FUNCTION:
@@ -838,6 +843,7 @@ ExecAlterOwnerStmt(AlterOwnerStmt *stmt)
 
 			/* Generic cases */
 		case OBJECT_AGGREGATE:
+		case OBJECT_ASSERTION:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_FUNCTION:
diff --git a/src/backend/commands/constraint.c b/src/backend/commands/constraint.c
index 90f19ad3dd..663f63d966 100644
--- a/src/backend/commands/constraint.c
+++ b/src/backend/commands/constraint.c
@@ -11,6 +11,8 @@
  *
  *-------------------------------------------------------------------------
  */
+
+
 #include "postgres.h"
 
 #include "catalog/index.h"
@@ -20,6 +22,39 @@
 #include "utils/rel.h"
 #include "utils/tqual.h"
 
+#include "access/htup_details.h"
+#include "catalog/dependency.h"
+#include "catalog/namespace.h"
+#include "catalog/pg_aggregate.h"
+#include "catalog/pg_constraint.h"
+#include "catalog/pg_operator.h"
+#include "catalog/pg_proc.h"
+#include "catalog/pg_language.h"
+#include "catalog/pg_trigger.h"
+#include "catalog/pg_type.h"
+#include "commands/constraint.h"
+#include "executor/functions.h"
+#include "executor/spi.h"
+#include "miscadmin.h"
+#include "nodes/makefuncs.h"
+#include "nodes/nodeFuncs.h"
+#include "optimizer/planner.h"
+#include "parser/analyze.h"
+#include "parser/parse_coerce.h"
+#include "parser/parse_expr.h"
+#include "parser/parse_node.h"
+#include "parser/parse_relation.h"
+#include "parser/parser.h"
+#include "parser/parsetree.h"
+#include "rewrite/rewriteHandler.h"
+#include "tcop/tcopprot.h"
+#include "utils/acl.h"
+#include "utils/lsyscache.h"
+#include "utils/snapmgr.h"
+#include "utils/syscache.h"
+#include "utils/ruleutils.h"
+#include "utils/fmgroids.h"
+
 
 /*
  * unique_key_recheck - trigger function to do a deferred uniqueness check.
@@ -39,15 +74,15 @@ unique_key_recheck(PG_FUNCTION_ARGS)
 {
 	TriggerData *trigdata = castNode(TriggerData, fcinfo->context);
 	const char *funcname = "unique_key_recheck";
-	HeapTuple	new_row;
+	HeapTuple new_row;
 	ItemPointerData tmptid;
-	Relation	indexRel;
-	IndexInfo  *indexInfo;
-	EState	   *estate;
+	Relation indexRel;
+	IndexInfo *indexInfo;
+	EState *estate;
 	ExprContext *econtext;
 	TupleTableSlot *slot;
-	Datum		values[INDEX_MAX_KEYS];
-	bool		isnull[INDEX_MAX_KEYS];
+	Datum values[INDEX_MAX_KEYS];
+	bool isnull[INDEX_MAX_KEYS];
 
 	/*
 	 * Make sure this is being called as an AFTER ROW trigger.  Note:
@@ -57,15 +92,15 @@ unique_key_recheck(PG_FUNCTION_ARGS)
 	if (!CALLED_AS_TRIGGER(fcinfo))
 		ereport(ERROR,
 				(errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED),
-				 errmsg("function \"%s\" was not called by trigger manager",
-						funcname)));
+					errmsg("function \"%s\" was not called by trigger manager",
+						   funcname)));
 
 	if (!TRIGGER_FIRED_AFTER(trigdata->tg_event) ||
 		!TRIGGER_FIRED_FOR_ROW(trigdata->tg_event))
 		ereport(ERROR,
 				(errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED),
-				 errmsg("function \"%s\" must be fired AFTER ROW",
-						funcname)));
+					errmsg("function \"%s\" must be fired AFTER ROW",
+						   funcname)));
 
 	/*
 	 * Get the new data that was inserted/updated.
@@ -78,9 +113,9 @@ unique_key_recheck(PG_FUNCTION_ARGS)
 	{
 		ereport(ERROR,
 				(errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED),
-				 errmsg("function \"%s\" must be fired for INSERT or UPDATE",
-						funcname)));
-		new_row = NULL;			/* keep compiler quiet */
+					errmsg("function \"%s\" must be fired for INSERT or UPDATE",
+						   funcname)));
+		new_row = NULL;            /* keep compiler quiet */
 	}
 
 	/*
@@ -194,3 +229,1043 @@ unique_key_recheck(PG_FUNCTION_ARGS)
 
 	return PointerGetDatum(NULL);
 }
+
+
+static void
+test_assertion_expr(char *name, char *expression)
+{
+	char *sql;
+	int returnCode;
+	bool isNull;
+	Datum value;
+
+	if (SPI_connect() != SPI_OK_CONNECT)
+		elog(ERROR, "SPI connect failed when executing ASSERTION statement");
+
+	sql = psprintf("SELECT %s", expression);
+	returnCode = SPI_exec(sql, 1);
+
+	if (returnCode > 0 && SPI_tuptable != NULL)
+	{
+		value = SPI_getbinval(SPI_tuptable->vals[0], SPI_tuptable->tupdesc, 1, &isNull);
+		if (isNull)
+			ereport(ERROR,
+					(errcode(ERRCODE_CHECK_VIOLATION),
+							errmsg("assertion \"%s\" truth is unknown", name)));
+		else if (!isNull && !DatumGetBool(value))
+			ereport(ERROR,
+					(errcode(ERRCODE_CHECK_VIOLATION),
+							errmsg("assertion \"%s\" violated", name)));
+	}
+	else
+		elog(ERROR, "unexpected SPI result when executing ASSERTION statement");
+
+	SPI_finish();
+}
+
+
+Datum
+assertion_check(PG_FUNCTION_ARGS)
+{
+	TriggerData *trigdata = (TriggerData *) fcinfo->context;
+	const char *funcname = "assertion_check";
+	Oid constraintOid;
+	HeapTuple tup;
+	Datum adatum;
+	bool isNull;
+
+	/*
+	 * Make sure this is being called as an AFTER STATEMENT trigger.	Note:
+	 * translatable error strings are shared with ri_triggers.c, so resist the
+	 * temptation to fold the function name into them.
+	 */
+	if (!CALLED_AS_TRIGGER(fcinfo))
+		ereport(ERROR,
+				(errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED),
+					errmsg("function \"%s\" was not called by trigger manager",
+						   funcname)));
+
+	if (!TRIGGER_FIRED_AFTER(trigdata->tg_event) ||
+		!TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event))
+		ereport(ERROR,
+				(errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED),
+					errmsg("function \"%s\" must be fired AFTER STATEMENT",
+						   funcname)));
+
+	constraintOid = trigdata->tg_trigger->tgconstraint;
+	tup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(constraintOid));
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for constraint %u", constraintOid);
+
+	// XXX bogus
+	adatum = SysCacheGetAttr(CONSTROID, tup,
+							 Anum_pg_constraint_consrc, &isNull);
+	if (isNull)
+		elog(ERROR, "constraint %u has null consrc", constraintOid);
+
+	test_assertion_expr(get_constraint_name(constraintOid), TextDatumGetCString(adatum));
+
+	ReleaseSysCache(tup);
+	return PointerGetDatum(NULL);
+}
+
+#define NO_FUNC				(0)
+#define OTHER_FUNC			(1 << 0)
+#define MIN_AGG_FUNC		(1 << 1)
+#define MAX_AGG_FUNC		(1 << 2)
+#define COUNT_AGG_FUNC		(1 << 3)
+#define EVERY_AGG_FUNC		(1 << 4)
+#define BOOL_AND_AGG_FUNC	(1 << 5)
+#define BOOL_OR_AGG_FUNC	(1 << 6)
+
+#define EQ_COMPARISONS 		(bms_make_singleton(ROWCOMPARE_EQ))
+#define NE_COMPARISONS 		(bms_make_singleton(ROWCOMPARE_NE))
+#define LTE_COMPARISONS 	(bms_add_member(bms_make_singleton(ROWCOMPARE_LE), ROWCOMPARE_LT))
+#define GTE_COMPARISONS 	(bms_add_member(bms_make_singleton(ROWCOMPARE_GE), ROWCOMPARE_GT))
+
+#define MatchesOnly(a,b)	(((a) & (b)) && !((a) & (~(b))))
+#define CanOptimise(s) 		(!bms_is_empty(s) && \
+							(bms_is_subset(s, EQ_COMPARISONS)  || \
+							 bms_is_subset(s, NE_COMPARISONS)  || \
+							 bms_is_subset(s, LTE_COMPARISONS) || \
+							 bms_is_subset(s, GTE_COMPARISONS) ))
+
+
+// TODO review TODOs in the tests
+// TODO assertions should allow unknown according to the SQL specification
+// TODO traversal into views needs rethinking - dependencies?
+// TODO pg_dump support
+
+/*
+ * DML operations that could affect the truth of an assertion. Only
+ * INSERT and DELETE are considered as part of the labeling algorithm.
+ * UPDATEs are inferred using different logic.
+ */
+typedef enum DmlOp
+{
+	INSERT,
+	DELETE,
+	INSERT_DELETE
+} DmlOp;
+
+
+/*
+ * A context used to capture operations that may invalidate an assertion.
+ */
+typedef struct AssertionInfo
+{
+	DmlOp	label;         /* current invaliding operation */
+	Expr   *expr;          /* current Expression node */
+	List   *rtable;        /* current range tables */
+	List   *operators;     /* current operators */
+	SetOperation setOp;         /* current set-operation */
+	bool	invert;		   /* ... */
+	bool	inView;        /* if the walker has entered a view */
+	bool	inTargetEntry; /* if the walker has entered a TargetEntry node */
+	bool	inComparison;  /* if the walker has entered a comparison operation */
+	bool	inExists;      /* if the walker has entered an EXISTS node */
+
+	List   *dependencies; /* ... */
+	List   *relations;    /* relation Oids for all tables involved in the assertion check */
+	List   *inserts;      /* relation Oids for which INSERT could invalidate the assertion */
+	List   *deletes;      /* relation Oids for which DELETE could invalidate the assertion */
+	List   *updates;      /* relation Oids for which UPDATE could invalidate the assertion */
+	List   *columns;      /* list of ObjectAddresses referencing involved columns */
+} AssertionInfo;
+
+
+static void initAssertionInfo(AssertionInfo *info);
+static void copyAssertionInfo(AssertionInfo *target, AssertionInfo *source);
+static DmlOp oppositeDmlOp(DmlOp operation);
+static RowCompareType oppositeCompareType(RowCompareType type);
+static DmlOp labelForComparisonWithAggFuncs(DmlOp label, RowCompareType compOp, int aggFuncs);
+static int16 triggerOnEvents(AssertionInfo *info, Oid relationId);
+static List *triggerOnColumns(AssertionInfo *info, Oid relationId);
+static bool listContainsObjectAddress(List *list, ObjectAddress *address);
+static Bitmapset * strategiesForOperators(List *operators);
+static int functionsForTargetList(List *targetList);
+static int funcMaskForFuncOid(Oid funcOid);
+static Query * queryForSQLFunction(FuncExpr *funcExpr);
+
+/* Expression visiting */
+static bool visitAllNodes(Node *node, AssertionInfo *info);
+static bool visitRangeTblRef(RangeTblRef *node, AssertionInfo *info);
+static bool visitQuery(Query *node, AssertionInfo *info);
+static bool visitSetOperationStmt(SetOperationStmt *node, AssertionInfo *info);
+static bool visitBoolExpr(BoolExpr *node, AssertionInfo *info);
+static bool visitSubLink(SubLink *node, AssertionInfo *info);
+static bool visitFuncExpr(FuncExpr *node, AssertionInfo *info);
+static bool visitExpr(Expr *node, AssertionInfo *info);
+static bool visitVar(Var *node, AssertionInfo *info);
+
+/* TargetList visiting */
+static bool visitAggrefNodes(Node *node, int *aggFuncs);
+static bool visitAggref(Aggref *node, int *aggFuncs);
+static bool visitWindowFunc(WindowFunc *node, int *aggFuncs);
+
+
+static void
+initAssertionInfo(AssertionInfo *info)
+{
+	info->label = DELETE;
+	info->expr = NULL;
+	info->rtable = NIL;
+	info->operators = NIL;
+	info->setOp = SETOP_NONE;
+	info->invert = false;
+	info->inView = false;
+	info->inTargetEntry = false;
+	info->inComparison = false;
+	info->inExists = false;
+
+	info->dependencies = NIL;
+	info->relations = NIL;
+	info->inserts = NIL;
+	info->deletes = NIL;
+	info->updates = NIL;
+	info->columns = NIL;
+}
+
+
+static void
+copyAssertionInfo(AssertionInfo *target, AssertionInfo *source)
+{
+	target->label = source->label;
+	target->expr = source->expr;
+	target->rtable = source->rtable;
+	target->operators = source->operators;
+	target->setOp = source->setOp;
+	target->invert = source->invert;
+	target->inView = source->inView;
+	target->inTargetEntry = source->inTargetEntry;
+	target->inComparison = source->inComparison;
+	target->inExists = source->inExists;
+}
+
+
+static DmlOp
+oppositeDmlOp(DmlOp operation)
+{
+	switch (operation)
+	{
+		case INSERT:
+			return DELETE;
+		case DELETE:
+			return INSERT;
+		case INSERT_DELETE:
+			return INSERT_DELETE;
+	}
+}
+
+
+static RowCompareType
+oppositeCompareType(RowCompareType type)
+{
+	switch (type)
+	{
+		case ROWCOMPARE_LE:
+			return ROWCOMPARE_GE;
+		case ROWCOMPARE_LT:
+			return ROWCOMPARE_GT;
+		case ROWCOMPARE_GT:
+			return ROWCOMPARE_LT;
+		case ROWCOMPARE_GE:
+			return ROWCOMPARE_LE;
+		default:
+			return type; /* we do not flip EQ and NE comparisons */
+	}
+}
+
+
+static DmlOp
+labelForComparisonWithAggFuncs(DmlOp label, RowCompareType compOp, int aggFuncs)
+{
+	int stronger, weaker;
+
+	switch (compOp)
+	{
+		case ROWCOMPARE_LT:
+		case ROWCOMPARE_LE:
+			stronger = MIN_AGG_FUNC;
+			weaker = MAX_AGG_FUNC | COUNT_AGG_FUNC;
+			break;
+
+		case ROWCOMPARE_EQ:
+			stronger = BOOL_AND_AGG_FUNC | EVERY_AGG_FUNC;
+			weaker = BOOL_OR_AGG_FUNC;
+			break;
+
+		case ROWCOMPARE_NE:
+			stronger = BOOL_AND_AGG_FUNC | EVERY_AGG_FUNC;
+			weaker = BOOL_OR_AGG_FUNC;
+			break;
+
+		case ROWCOMPARE_GE:
+		case ROWCOMPARE_GT:
+			stronger = MAX_AGG_FUNC | COUNT_AGG_FUNC;
+			weaker = MIN_AGG_FUNC;
+			break;
+
+		default:
+			stronger = weaker = NO_FUNC;
+	}
+
+	if (MatchesOnly(aggFuncs, weaker))
+		return label;
+	else if (MatchesOnly(aggFuncs, stronger))
+		return oppositeDmlOp(label);
+	else
+		return INSERT_DELETE;
+}
+
+
+static int16
+triggerOnEvents(AssertionInfo *info, Oid relationId)
+{
+	int16 result = 0;
+	if (list_member_oid(info->inserts, relationId))
+		result |= TRIGGER_TYPE_INSERT;
+	if (list_member_oid(info->deletes, relationId))
+		result |= TRIGGER_TYPE_DELETE | TRIGGER_TYPE_TRUNCATE;
+	if (list_member_oid(info->updates, relationId))
+		result |= TRIGGER_TYPE_UPDATE;
+	return result;
+}
+
+
+/*
+ * Returns a list of string nodes, suitable for use in the trigger
+ * definition, that contain the column names to be triggered against
+ * on UPDATE operations.
+ */
+static List *
+triggerOnColumns(AssertionInfo *info, Oid relationId)
+{
+	List     	  *columns = NIL;
+	ListCell 	  *cell;
+
+	foreach(cell, info->columns)
+	{
+		ObjectAddress *column = (ObjectAddress *) lfirst(cell);
+		AttrNumber attrNumber = (AttrNumber) column->objectSubId;
+
+		if (column->objectId == relationId)
+		{
+			Value *name = makeString(get_attname(relationId, attrNumber, false));
+			columns = lappend(columns, name);
+		}
+	}
+
+	return columns;
+}
+
+
+static bool
+listContainsObjectAddress(List *list, ObjectAddress *address)
+{
+	ListCell 	  *cell;
+	ObjectAddress *item;
+
+	foreach(cell, list)
+	{
+		item = (ObjectAddress *) lfirst(cell);
+		if (item->classId == address->classId &&
+			item->objectId == address->objectId &&
+			item->objectSubId == address->objectSubId)
+			return true;
+	}
+
+	return false;
+}
+
+
+static Bitmapset *
+strategiesForOperators(List *operators)
+{
+	ListCell  *operatorCell;
+	Bitmapset *strategies = NULL;
+
+	foreach(operatorCell, operators)
+	{
+		Oid operator = lfirst_oid(operatorCell);
+		List *interpretations = get_op_btree_interpretation(operator);
+		ListCell *interpretationCell;
+
+		foreach(interpretationCell, interpretations)
+			strategies = bms_add_member(strategies,
+										((OpBtreeInterpretation *) lfirst(interpretationCell))->strategy);
+	}
+
+	return strategies;
+}
+
+
+static int
+functionsForTargetList(List *targetList)
+{
+	int functionMask = NO_FUNC;
+	expression_tree_walker((Node *) targetList, visitAggrefNodes, &functionMask);
+	return functionMask;
+}
+
+
+static int
+funcMaskForFuncOid(Oid funcOid)
+{
+	char *name = get_func_name(funcOid);
+
+	if (name == NULL)
+		return OTHER_FUNC;
+	else if (strncmp(name, "min", strlen("min")) == 0)
+		return MIN_AGG_FUNC;
+	else if (strncmp(name, "max", strlen("max")) == 0)
+		return MAX_AGG_FUNC;
+	else if (strncmp(name, "count", strlen("count")) == 0)
+		return COUNT_AGG_FUNC;
+	else if (strncmp(name, "every", strlen("every")) == 0)
+		return EVERY_AGG_FUNC;
+	else if (strncmp(name, "bool_and", strlen("bool_and")) == 0)
+		return BOOL_AND_AGG_FUNC;
+	else if (strncmp(name, "bool_or", strlen("bool_or")) == 0)
+		return BOOL_OR_AGG_FUNC;
+	else
+		return OTHER_FUNC;
+}
+
+
+static Query *
+queryForSQLFunction(FuncExpr *funcExpr)
+{
+	Oid			funcId;
+	Relation 	procRel;
+	HeapTuple	tuple;
+	Datum		datum;
+	bool		isNull;
+	char	   *sql;
+	List	   *rawParseTree;
+	ParseState *pstate;
+	Query	   *query;
+	SQLFunctionParseInfoPtr pinfo;
+
+	funcId = funcExpr->funcid;
+	procRel = heap_open(ProcedureRelationId, ShareLock);
+
+	tuple = SearchSysCache1(PROCOID, ObjectIdGetDatum(funcId));
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for function %u", funcId);
+
+	datum = SysCacheGetAttr(PROCOID,
+							tuple,
+							Anum_pg_proc_prosrc,
+							&isNull);
+	if (isNull)
+		elog(ERROR, "null prosrc for function %u", funcId);
+
+	sql = TextDatumGetCString(datum);
+	rawParseTree = pg_parse_query(sql);
+
+	pinfo = prepare_sql_fn_parse_info(tuple,
+									  (Node *) funcExpr,
+									  funcExpr->inputcollid);
+
+	pstate = make_parsestate(NULL);
+	pstate->p_sourcetext = sql;
+	sql_fn_parser_setup(pstate, pinfo);
+
+	query = transformTopLevelStmt(pstate, linitial(rawParseTree));
+	free_parsestate(pstate);
+
+	ReleaseSysCache(tuple);
+	heap_close(procRel, NoLock);
+
+	return query;
+}
+
+
+static bool
+visitRangeTblRef(RangeTblRef *node, AssertionInfo *info)
+{
+	RangeTblEntry  *entry;
+	Oid				relationId;
+	RTEKind			rtekind;
+	char 		    relkind;
+	bool			result;
+
+	entry = rt_fetch(node->rtindex, info->rtable);
+	rtekind = entry->rtekind;
+	result = false;
+
+	if (rtekind == RTE_RELATION)
+	{
+		relationId = getrelid(node->rtindex, info->rtable);
+		relkind = get_rel_relkind(relationId);
+
+		if (relkind == RELKIND_RELATION)
+		{
+			if (info->label == INSERT || info->label == INSERT_DELETE)
+				info->inserts = list_append_unique_oid(info->inserts, relationId);
+
+			if (info->label == DELETE || info->label == INSERT_DELETE)
+				info->deletes = list_append_unique_oid(info->deletes, relationId);
+
+			if (!info->inView)
+				info->dependencies = list_append_unique_oid(info->dependencies, relationId);
+
+			info->relations = list_append_unique_oid(info->relations, relationId);
+
+		}
+		else if (relkind == RELKIND_VIEW)
+		{
+			Relation view = heap_open(relationId, AccessShareLock);
+			Query *query = get_view_query(view);
+
+			if (!info->inView)
+				info->dependencies = list_append_unique_oid(info->dependencies, relationId);
+
+			info->inView = true;
+			result = visitQuery(query, info);
+
+			heap_close(view, NoLock);
+		}
+
+	}
+	else if (rtekind == RTE_TABLEFUNC)
+	{
+		result = visitAllNodes((Node *) entry->tablefunc, info);
+	}
+	else if (rtekind == RTE_FUNCTION)
+	{
+		result = visitAllNodes((Node *) entry->functions, info);
+	}
+	else if (rtekind == RTE_SUBQUERY)
+	{
+		result = visitQuery(entry->subquery, info);
+	}
+
+	return result;
+}
+
+
+static bool
+visitQuery(Query *node, AssertionInfo *info)
+{
+	info->rtable = node->rtable;
+
+	if (info->inComparison)
+	{
+		int functions = functionsForTargetList(node->targetList);
+		Bitmapset *strategies = strategiesForOperators(info->operators);
+
+		if (CanOptimise(strategies))
+		{
+			RowCompareType rowCompareType = (RowCompareType) bms_first_member(strategies);
+			info->label = labelForComparisonWithAggFuncs(
+				info->label,
+				info->invert ? oppositeCompareType(rowCompareType) : rowCompareType,
+				functions
+			);
+		}
+		else
+		{
+			/*
+			 * Either no btree interpretation was found for the operator(s), there were
+			 * multiple interpretations that were incompatible with each other, or the
+			 * found interpretations were not able to be optimised. We must therefore
+			 * assume that both INSERT and DELETE operations may be invalidating.
+			 */
+			info->label = INSERT_DELETE;
+		}
+	}
+	else if (node->hasWindowFuncs)
+	{
+		info->label = INSERT_DELETE;
+	}
+
+	return query_tree_walker(node,
+							 visitAllNodes,
+							 info,
+							 QTW_IGNORE_RANGE_TABLE);
+}
+
+
+static bool
+visitSetOperationStmt(SetOperationStmt *node, AssertionInfo *info)
+{
+	info->setOp = node->op;
+
+	if (visitAllNodes(node->larg, info))
+		return true;
+
+	if (info->setOp == SETOP_EXCEPT)
+		info->label = oppositeDmlOp(info->label);
+
+	return visitAllNodes(node->rarg, info);
+}
+
+
+static bool
+visitBoolExpr(BoolExpr *node, AssertionInfo *info)
+{
+	if (node->boolop == NOT_EXPR)
+		info->label = oppositeDmlOp(info->label);
+
+	return expression_tree_walker((Node *) node, visitAllNodes, (void *) info);
+}
+
+
+// TODO refactor this function
+static bool
+visitSubLink(SubLink *node, AssertionInfo *info)
+{
+	List *operators;
+	bool  invert;
+	bool  inComparison;
+	bool  inExists;
+
+	switch (node->subLinkType)
+	{
+		case ARRAY_SUBLINK: // TODO write tests for this
+		case EXPR_SUBLINK:
+		{
+			Expr *expr = info->expr;
+			bool hasExpr = (expr != NULL);
+
+			if (hasExpr && IsA(expr, OpExpr))
+			{
+				OpExpr *opExpr = (OpExpr *) expr;
+				inComparison = true;
+				inExists = false;
+
+				/*
+				 * Optimisation code is written under the assumption that the sub-select is the
+				 * right operand. If it is the left operand the comparison needs to be inverted.
+				 */
+				invert = (list_nth_node(SubLink, opExpr->args, 0) == node);
+				operators = list_make1_oid(opExpr->opno);
+			}
+			else if (hasExpr && IsA(expr, FuncExpr) &&
+					((FuncExpr *)expr)->funcresulttype == BOOLOID)
+			{
+				/*
+				 * We are inside a function invocation that returns Boolean but is not an OpExpr.
+				 * Let's exploit the fact that "expr == TRUE -> expr", and pretend there is an
+				 * equality operator.
+				 */
+				inComparison = true;
+				inExists = false;
+				operators = list_make1_oid(F_BOOLEQ);
+				invert = false;
+			}
+			else if (exprType((const Node *) node) == BOOLOID)
+			{
+				/*
+				 * We are _not_ inside either an OpExpr or FuncExpr, but we are a query that can be
+				 * coerced to Boolean. We use the same logic above e.g. "expr == TRUE -> expr"
+				 */
+				inComparison = true;
+				inExists = false;
+				operators = list_make1_oid(F_BOOLEQ);
+				invert = false;
+			}
+			else
+			{
+				inComparison = false;
+				inExists = false;
+				invert = false;
+				operators = NIL;
+			}
+		}
+		break;
+
+		case ALL_SUBLINK:
+		case ANY_SUBLINK:
+		case ROWCOMPARE_SUBLINK:
+		{
+			if (IsA(node->testexpr, OpExpr))
+			{
+				OpExpr *expr = (OpExpr *) node->testexpr;
+				inComparison = true;
+				inExists = false;
+				invert = false;
+				operators = list_make1_oid(expr->opno);
+			}
+			else if (IsA(node->testexpr, RowCompareExpr))
+			{
+				RowCompareExpr *expr = (RowCompareExpr *) node->testexpr;
+				inComparison = true;
+				inExists = false;
+				invert = false;
+				operators = list_copy(expr->opnos);
+			}
+			else
+			{
+				inComparison = false;
+				inExists = false;
+				invert = false;
+				operators = NIL;
+			}
+		}
+		break;
+
+		case EXISTS_SUBLINK:
+		{
+			/* existential quantification, no operators */
+			inComparison = false;
+			inExists = true;
+			invert = false;
+			operators = NIL;
+		}
+		break;
+			// case MULTIEXPR_SUBLINK: // TODO write tests for MULTIEXPR_SUBLINK? -- or, can it only occur in UPDATEs?
+		default:
+		{
+			elog(ERROR, "unhandled sublink type %u", node->subLinkType);
+			return true;
+		}
+
+	}
+
+	info->operators = operators;
+	info->invert = invert;
+	info->inComparison = inComparison;
+	info->inExists = inExists;
+	info->setOp = SETOP_NONE;
+
+	return expression_tree_walker((Node *) node, visitAllNodes, (void *) info);
+}
+
+
+static bool
+visitTargetEntry(TargetEntry *node, AssertionInfo *info)
+{
+	info->inTargetEntry = true;
+	return expression_tree_walker((Node *) node, visitAllNodes, (void *) info);
+}
+
+
+static bool
+visitFromExpr(FromExpr *node, AssertionInfo *info)
+{
+	info->inExists = false;
+	info->inComparison = false;
+	return expression_tree_walker((Node *) node, visitAllNodes, (void *) info);
+}
+
+
+static bool
+visitFuncExpr(FuncExpr *node, AssertionInfo *info)
+{
+	Oid lang = get_func_lang(node->funcid);
+
+	info->expr = (Expr *) node;
+
+	if (!(lang == INTERNALlanguageId || lang == SQLlanguageId))
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_AMBIGUOUS_FUNCTION),
+				 errmsg("function \"%s\" uses unsupported language \"%s\"",
+				   get_func_name(node->funcid), get_language_name(lang, false))));
+		return true;
+	}
+
+	if (expression_tree_walker((Node *) node, visitAllNodes, (void *) info))
+		return true;
+
+	if (lang == SQLlanguageId)
+	{
+		Query *query = queryForSQLFunction(node);
+		Node *next;
+		if (node->funcretset)
+			next = (Node *) query;
+		else
+			next = (Node *) ((TargetEntry *) linitial(query->targetList))->expr;
+
+		return visitAllNodes(next, info);
+	}
+
+	return false;
+}
+
+
+static bool
+visitExpr(Expr *node, AssertionInfo *info)
+{
+	info->expr = node;
+	return expression_tree_walker((Node *) node, visitAllNodes, (void *) info);
+}
+
+
+static bool
+visitVar(Var *node, AssertionInfo *info)
+{
+	RangeTblEntry *entry = rt_fetch(node->varno, info->rtable);
+
+	// TODO we should do this only for Tables and not Views?
+	// TODO optimisations for set-operations
+	if (entry->rtekind == RTE_RELATION && !(info->inExists && info->setOp == SETOP_NONE))
+	{
+		Oid relationId = getrelid(node->varno, info->rtable);
+		ObjectAddress *column = (ObjectAddress *) palloc(sizeof(ObjectAddress));
+
+		column->classId = RelationRelationId;
+		column->objectId = relationId;
+		column->objectSubId = node->varattno;
+
+		if (!listContainsObjectAddress(info->columns, column))
+			info->columns = lcons(column, info->columns);
+
+		info->relations = list_append_unique_oid(info->relations, relationId);
+		info->updates = list_append_unique_oid(info->updates, relationId);
+	}
+
+	return expression_tree_walker((Node *) node, visitAllNodes, (void *) info);
+}
+
+
+static bool
+visitAllNodes(Node *node, AssertionInfo *info)
+{
+	AssertionInfo stored;
+	bool result;
+
+	copyAssertionInfo(&stored, info);
+
+	if (node == NULL)
+		result = false;
+	else if (IsA(node, RangeTblRef))
+		result = visitRangeTblRef((RangeTblRef *) node, info);
+	else if (IsA(node, Query))
+		result = visitQuery((Query *) node, info);
+	else if (IsA(node, BoolExpr))
+		result = visitBoolExpr((BoolExpr *) node, info);
+	else if (IsA(node, SubLink))
+		result = visitSubLink((SubLink *) node, info);
+	else if (IsA(node, SetOperationStmt))
+		result = visitSetOperationStmt((SetOperationStmt *) node, info);
+	else if (IsA(node, FuncExpr))
+		result = visitFuncExpr((FuncExpr *) node, info);
+	else if (IsA(node, OpExpr) ||
+			 IsA(node, DistinctExpr) ||
+			 IsA(node, NullIfExpr))
+		result = visitExpr((Expr *) node, info);
+	else if (IsA(node, TargetEntry))
+		result = visitTargetEntry((TargetEntry *) node, info);
+	else if (IsA(node, Var))
+		result = visitVar((Var *) node, info);
+	else if (IsA(node, FromExpr))
+		result = visitFromExpr((FromExpr *) node, info);
+	else
+		result = expression_tree_walker(node, visitAllNodes, (void *) info);
+
+	copyAssertionInfo(info, &stored);
+
+	return result;
+}
+
+
+static bool
+visitAggrefNodes(Node *node, int *aggFuncs)
+{
+	if (node == NULL)
+		return false;
+	else if (IsA(node, Aggref))
+		return visitAggref((Aggref *) node, aggFuncs);
+	else if(IsA(node, WindowFunc))
+		return visitWindowFunc((WindowFunc *) node, aggFuncs);
+	else
+		return expression_tree_walker(node, visitAggrefNodes, (void *) aggFuncs);
+}
+
+
+static bool
+visitAggref(Aggref *node, int *aggFuncs)
+{
+	*aggFuncs |= funcMaskForFuncOid(node->aggfnoid);
+	return expression_tree_walker((Node *) node, visitAggrefNodes, (void *) aggFuncs);
+}
+
+
+static bool
+visitWindowFunc(WindowFunc *node, int *aggFuncs)
+{
+	*aggFuncs |= funcMaskForFuncOid(node->winfnoid);
+
+	return expression_tree_walker((Node *) node, visitAggrefNodes, (void *) aggFuncs);
+}
+
+
+ObjectAddress
+CreateAssertion(CreateAssertionStmt *stmt)
+{
+	Oid namespaceId;
+	char *assertion_name;
+	AclResult aclresult;
+	Node *expr;
+	ParseState *pstate;
+	char *ccsrc;
+	char *ccbin;
+	Oid constrOid;
+	AssertionInfo info;
+	ListCell *lc;
+	ObjectAddress address;
+
+	namespaceId = QualifiedNameGetCreationNamespace(stmt->assertion_name,
+													&assertion_name);
+
+	aclresult = pg_namespace_aclcheck(namespaceId, GetUserId(), ACL_CREATE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, OBJECT_SCHEMA,
+					   get_namespace_name(namespaceId));
+
+	if (ConstraintNameIsUsed(CONSTRAINT_ASSERTION, InvalidOid, namespaceId, assertion_name))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_NAME),
+				 errmsg("assertion \"%s\" already exists", assertion_name)));
+
+	pstate = make_parsestate(NULL);
+	expr = transformExpr(pstate, stmt->constraint->raw_expr, EXPR_KIND_ASSERTION_CHECK);
+	expr = coerce_to_boolean(pstate, expr, "CHECK");
+
+	ccbin = nodeToString(expr);
+	ccsrc = deparse_expression(expr, NIL, false, false);
+
+	constrOid = CreateConstraintEntry(assertion_name,
+									  namespaceId,
+									  CONSTRAINT_CHECK, /* constraint type */
+									  stmt->constraint->deferrable,
+									  stmt->constraint->initdeferred,
+									  !stmt->constraint->skip_validation,
+									  InvalidOid, /* no parent constraint */
+									  InvalidOid, /* not a relation constraint */
+									  NULL,       /* no keys */
+									  0,          /* no keys */
+									  0,          /* no keys */
+									  InvalidOid, /* not a domain constraint */
+									  InvalidOid, /* no associated index */
+									  InvalidOid, /* foreign key fields ... */
+									  NULL,
+									  NULL,
+									  NULL,
+									  NULL,
+									  0,
+									  ' ',
+									  ' ',
+									  ' ',
+									  NULL,    /* not an exclusion constraint */
+									  expr, /* tree form of check constraint */
+									  ccbin, /* binary form of check constraint */
+									  ccsrc, /* source form of check constraint */
+									  true, /* is local */
+									  0,   /* inhcount */
+									  false, /* noinherit XXX */
+									  false); /* is_internal */
+
+	initAssertionInfo(&info);
+	visitAllNodes(expr, &info);
+
+	foreach (lc, info.relations)
+	{
+		Oid relationId = lfirst_oid(lc);
+		CreateTrigStmt *trigger;
+		Relation rel;
+
+		rel = heap_open(relationId, ShareLock); // XXX
+
+		trigger = makeNode(CreateTrigStmt);
+		trigger->trigname = "AssertionTrigger";
+		trigger->relation = makeRangeVar(get_namespace_name(namespaceId),
+										 pstrdup(RelationGetRelationName(rel)),
+										 -1);
+		trigger->funcname = SystemFuncName("assertion_check");
+		trigger->args = NIL;
+		trigger->row = false;
+		trigger->timing = TRIGGER_TYPE_AFTER;
+		trigger->events = triggerOnEvents(&info, relationId);
+		trigger->columns = triggerOnColumns(&info, relationId);
+		trigger->whenClause = NULL;
+		trigger->isconstraint = true;
+		trigger->deferrable = stmt->constraint->deferrable;
+		trigger->initdeferred = stmt->constraint->initdeferred;
+		trigger->constrrel = NULL;
+
+		CreateTrigger(trigger,
+		              NULL,
+			      InvalidOid,
+			      relationId,
+			      constrOid,
+			      InvalidOid, /* no index */
+			      InvalidOid, /* no func - use trigger->funcname */
+			      InvalidOid, /* no parent */
+			      NULL,       /* no when */
+			      true,       /* is_internal */
+			      false);
+
+		heap_close(rel, NoLock);
+	}
+
+	/*
+	 * Record dependencies between the constraint and the relations found in the
+	 * top-level expression. Dependencies to specific columns will already have
+	 * been recorded by the trigger creation.
+	 */
+	ObjectAddress myself, referenced;
+	ListCell *cell;
+
+	myself.classId = ConstraintRelationId;
+	myself.objectId = constrOid;
+	myself.objectSubId = 0;
+
+	foreach (cell, info.dependencies)
+	{
+		referenced.classId = RelationRelationId;
+		referenced.objectId = lfirst_oid(cell);
+		referenced.objectSubId = 0;
+		recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+	}
+
+	test_assertion_expr(assertion_name, ccsrc);
+
+	ObjectAddressSet(address, ConstraintRelationId, constrOid);
+
+	return address;
+}
+
+
+ObjectAddress
+RenameAssertion(RenameStmt *stmt)
+{
+	Oid           assertionOid;
+	ObjectAddress address;
+	Relation      rel;
+	HeapTuple     tuple;
+	Form_pg_constraint con;
+	AclResult     aclresult;
+
+	List *oldName = castNode(List, stmt->object);
+	char *newName = stmt->newname;
+
+	rel = heap_open(ConstraintRelationId, RowExclusiveLock);
+	assertionOid = get_assertion_oid(oldName, false);
+
+	tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(assertionOid));
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for constraint %u",
+			 assertionOid);
+	con = (Form_pg_constraint) GETSTRUCT(tuple);
+
+	if (!pg_constraint_ownercheck(assertionOid, GetUserId()))
+		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_CONVERSION,
+					   NameListToString(oldName));
+
+	aclresult = pg_namespace_aclcheck(con->connamespace, GetUserId(), ACL_CREATE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, OBJECT_SCHEMA,
+					   get_namespace_name(con->connamespace));
+
+	ReleaseSysCache(tuple);
+	RenameConstraintById(assertionOid, newName);
+	ObjectAddressSet(address, ConstraintRelationId, assertionOid);
+	heap_close(rel, NoLock);
+
+	return address;
+}
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index eecc85d14e..ae8b919ffe 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -88,6 +88,7 @@ typedef enum
 static event_trigger_support_data event_trigger_support[] = {
 	{"ACCESS METHOD", true},
 	{"AGGREGATE", true},
+	{"ASSERTION", true},
 	{"CAST", true},
 	{"CONSTRAINT", true},
 	{"COLLATION", true},
@@ -1103,6 +1104,7 @@ EventTriggerSupportsObjectType(ObjectType obtype)
 			return false;
 		case OBJECT_ACCESS_METHOD:
 		case OBJECT_AGGREGATE:
+		case OBJECT_ASSERTION:
 		case OBJECT_AMOP:
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
@@ -2214,6 +2216,8 @@ stringify_grant_objtype(ObjectType objtype)
 {
 	switch (objtype)
 	{
+		case OBJECT_ASSERTION:
+			return "ASSERTION";
 		case OBJECT_COLUMN:
 			return "COLUMN";
 		case OBJECT_TABLE:
@@ -2296,6 +2300,8 @@ stringify_adefprivs_objtype(ObjectType objtype)
 {
 	switch (objtype)
 	{
+		case OBJECT_ASSERTION:
+			return "ASSERTIONS";
 		case OBJECT_COLUMN:
 			return "COLUMNS";
 		case OBJECT_TABLE:
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 7c045a7afe..35ed7a503b 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -4358,6 +4358,17 @@ _copyCreateSchemaStmt(const CreateSchemaStmt *from)
 	return newnode;
 }
 
+static CreateAssertionStmt *
+_copyCreateAssertionStmt(const CreateAssertionStmt *from)
+{
+	CreateAssertionStmt *newnode = makeNode(CreateAssertionStmt);
+
+	COPY_NODE_FIELD(assertion_name);
+	COPY_NODE_FIELD(constraint);
+
+	return newnode;
+}
+
 static CreateConversionStmt *
 _copyCreateConversionStmt(const CreateConversionStmt *from)
 {
@@ -5433,6 +5444,9 @@ copyObjectImpl(const void *from)
 		case T_CreateSchemaStmt:
 			retval = _copyCreateSchemaStmt(from);
 			break;
+		case T_CreateAssertionStmt:
+			retval = _copyCreateAssertionStmt(from);
+			break;
 		case T_CreateConversionStmt:
 			retval = _copyCreateConversionStmt(from);
 			break;
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 6a971d0141..b69ed08c88 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -2112,6 +2112,15 @@ _equalCreateSchemaStmt(const CreateSchemaStmt *a, const CreateSchemaStmt *b)
 	return true;
 }
 
+static bool
+_equalCreateAssertionStmt(const CreateAssertionStmt *a, const CreateAssertionStmt *b)
+{
+	COMPARE_NODE_FIELD(assertion_name);
+	COMPARE_NODE_FIELD(constraint);
+
+	return true;
+}
+
 static bool
 _equalCreateConversionStmt(const CreateConversionStmt *a, const CreateConversionStmt *b)
 {
@@ -3510,6 +3519,9 @@ equal(const void *a, const void *b)
 		case T_CreateSchemaStmt:
 			retval = _equalCreateSchemaStmt(a, b);
 			break;
+		case T_CreateAssertionStmt:
+			retval = _equalCreateAssertionStmt(a, b);
+			break;
 		case T_CreateConversionStmt:
 			retval = _equalCreateConversionStmt(a, b);
 			break;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 5a36367446..3dab57f478 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -259,11 +259,11 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 		CreateOpFamilyStmt AlterOpFamilyStmt CreatePLangStmt
 		CreateSchemaStmt CreateSeqStmt CreateStmt CreateStatsStmt CreateTableSpaceStmt
 		CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt
-		CreateAssertStmt CreateTransformStmt CreateTrigStmt CreateEventTrigStmt
+		CreateAssertionStmt CreateTransformStmt CreateTrigStmt CreateEventTrigStmt
 		CreateUserStmt CreateUserMappingStmt CreateRoleStmt CreatePolicyStmt
 		CreatedbStmt DeclareCursorStmt DefineStmt DeleteStmt DiscardStmt DoStmt
 		DropOpClassStmt DropOpFamilyStmt DropPLangStmt DropStmt
-		DropAssertStmt DropCastStmt DropRoleStmt
+		DropCastStmt DropRoleStmt
 		DropdbStmt DropTableSpaceStmt
 		DropTransformStmt
 		DropUserMappingStmt ExplainStmt FetchStmt
@@ -860,7 +860,7 @@ stmt :
 			| CopyStmt
 			| CreateAmStmt
 			| CreateAsStmt
-			| CreateAssertStmt
+			| CreateAssertionStmt
 			| CreateCastStmt
 			| CreateConversionStmt
 			| CreateDomainStmt
@@ -896,7 +896,6 @@ stmt :
 			| DeleteStmt
 			| DiscardStmt
 			| DoStmt
-			| DropAssertStmt
 			| DropCastStmt
 			| DropOpClassStmt
 			| DropOpFamilyStmt
@@ -5636,45 +5635,28 @@ enable_trigger:
  *
  *		QUERIES :
  *				CREATE ASSERTION ...
- *				DROP ASSERTION ...
  *
  *****************************************************************************/
 
-CreateAssertStmt:
-			CREATE ASSERTION name CHECK '(' a_expr ')'
+CreateAssertionStmt:
+			CREATE ASSERTION any_name CHECK '(' a_expr ')'
 			ConstraintAttributeSpec
 				{
-					CreateTrigStmt *n = makeNode(CreateTrigStmt);
-					n->trigname = $3;
-					n->args = list_make1($6);
-					n->isconstraint  = true;
-					processCASbits($8, @8, "ASSERTION",
-								   &n->deferrable, &n->initdeferred, NULL,
-								   NULL, yyscanner);
-
-					ereport(ERROR,
-							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-							 errmsg("CREATE ASSERTION is not yet implemented")));
-
-					$$ = (Node *)n;
-				}
-		;
-
-DropAssertStmt:
-			DROP ASSERTION name opt_drop_behavior
-				{
-					DropStmt *n = makeNode(DropStmt);
-					n->objects = NIL;
-					n->behavior = $4;
-					n->removeType = OBJECT_TRIGGER; /* XXX */
-					ereport(ERROR,
-							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-							 errmsg("DROP ASSERTION is not yet implemented")));
-					$$ = (Node *) n;
+					CreateAssertionStmt *n = makeNode(CreateAssertionStmt);
+					Constraint *c = makeNode(Constraint);
+					c->contype = CONSTR_CHECK;
+					c->location = @4;
+					c->raw_expr = $6;
+					c->cooked_expr = NULL;
+ 					processCASbits($8, @8, "ASSERTION",
+								   &c->deferrable, &c->initdeferred, NULL,
+ 								   NULL, yyscanner);
+					n->assertion_name = $3;
+					n->constraint = c;
+ 					$$ = (Node *)n;
 				}
 		;
 
-
 /*****************************************************************************
  *
  *		QUERY :
@@ -6328,6 +6310,7 @@ drop_type_any_name:
 			| MATERIALIZED VIEW						{ $$ = OBJECT_MATVIEW; }
 			| INDEX									{ $$ = OBJECT_INDEX; }
 			| FOREIGN TABLE							{ $$ = OBJECT_FOREIGN_TABLE; }
+			| ASSERTION								{ $$ = OBJECT_ASSERTION; }
 			| COLLATION								{ $$ = OBJECT_COLLATION; }
 			| CONVERSION_P							{ $$ = OBJECT_CONVERSION; }
 			| STATISTICS							{ $$ = OBJECT_STATISTIC_EXT; }
@@ -6597,6 +6580,7 @@ comment_type_any_name:
 			| TABLE								{ $$ = OBJECT_TABLE; }
 			| VIEW								{ $$ = OBJECT_VIEW; }
 			| MATERIALIZED VIEW					{ $$ = OBJECT_MATVIEW; }
+			| ASSERTION							{ $$ = OBJECT_ASSERTION; }
 			| COLLATION							{ $$ = OBJECT_COLLATION; }
 			| CONVERSION_P						{ $$ = OBJECT_CONVERSION; }
 			| FOREIGN TABLE						{ $$ = OBJECT_FOREIGN_TABLE; }
@@ -8459,6 +8443,15 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name
 					n->missing_ok = false;
 					$$ = (Node *)n;
 				}
+			| ALTER ASSERTION any_name RENAME TO name
+				{
+					RenameStmt *n = makeNode(RenameStmt);
+					n->renameType = OBJECT_ASSERTION;
+					n->object = (Node *) $3;
+					n->newname = $6;
+					n->missing_ok = false;
+					$$ = (Node *)n;
+				}
 			| ALTER COLLATION any_name RENAME TO name
 				{
 					RenameStmt *n = makeNode(RenameStmt);
@@ -9043,6 +9036,15 @@ AlterObjectSchemaStmt:
 					n->missing_ok = false;
 					$$ = (Node *)n;
 				}
+			| ALTER ASSERTION any_name SET SCHEMA name
+				{
+					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
+					n->objectType = OBJECT_ASSERTION;
+					n->object = (Node *) $3;
+					n->newschema = $6;
+					n->missing_ok = false;
+					$$ = (Node *)n;
+				}
 			| ALTER COLLATION any_name SET SCHEMA name
 				{
 					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
@@ -9328,6 +9330,14 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
 					n->newowner = $6;
 					$$ = (Node *)n;
 				}
+			| ALTER ASSERTION any_name OWNER TO RoleSpec
+				{
+					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
+					n->objectType = OBJECT_ASSERTION;
+					n->object = (Node *) $3;
+					n->newowner = $6;
+					$$ = (Node *)n;
+				}
 			| ALTER COLLATION any_name OWNER TO RoleSpec
 				{
 					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c
index 61727e1d71..09126a050d 100644
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -457,6 +457,7 @@ check_agglevels_and_constraints(ParseState *pstate, Node *expr)
 			break;
 		case EXPR_KIND_CHECK_CONSTRAINT:
 		case EXPR_KIND_DOMAIN_CHECK:
+		case EXPR_KIND_ASSERTION_CHECK:
 			if (isAgg)
 				err = _("aggregate functions are not allowed in check constraints");
 			else
@@ -875,6 +876,7 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
 			break;
 		case EXPR_KIND_CHECK_CONSTRAINT:
 		case EXPR_KIND_DOMAIN_CHECK:
+		case EXPR_KIND_ASSERTION_CHECK:
 			err = _("window functions are not allowed in check constraints");
 			break;
 		case EXPR_KIND_COLUMN_DEFAULT:
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 385e54a9b6..1933a615bf 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -1818,6 +1818,7 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
 		case EXPR_KIND_RETURNING:
 		case EXPR_KIND_VALUES:
 		case EXPR_KIND_VALUES_SINGLE:
+		case EXPR_KIND_ASSERTION_CHECK:
 			/* okay */
 			break;
 		case EXPR_KIND_CHECK_CONSTRAINT:
@@ -3457,6 +3458,7 @@ ParseExprKindName(ParseExprKind exprKind)
 			return "VALUES";
 		case EXPR_KIND_CHECK_CONSTRAINT:
 		case EXPR_KIND_DOMAIN_CHECK:
+		case EXPR_KIND_ASSERTION_CHECK:
 			return "CHECK";
 		case EXPR_KIND_COLUMN_DEFAULT:
 		case EXPR_KIND_FUNCTION_DEFAULT:
diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c
index ea5d5212b4..8193b7da37 100644
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -2277,6 +2277,10 @@ check_srf_call_placement(ParseState *pstate, Node *last_srf, int location)
 			/* okay, since we process this like a SELECT tlist */
 			pstate->p_hasTargetSRFs = true;
 			break;
+		case EXPR_KIND_ASSERTION_CHECK:
+			/* okay */
+			pstate->p_hasTargetSRFs = true;
+			break;
 		case EXPR_KIND_CHECK_CONSTRAINT:
 		case EXPR_KIND_DOMAIN_CHECK:
 			err = _("set-returning functions are not allowed in check constraints");
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 287addf429..22997dfd0b 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -30,6 +30,7 @@
 #include "commands/cluster.h"
 #include "commands/comment.h"
 #include "commands/collationcmds.h"
+#include "commands/constraint.h"
 #include "commands/conversioncmds.h"
 #include "commands/copy.h"
 #include "commands/createas.h"
@@ -161,6 +162,7 @@ check_xact_readonly(Node *parsetree)
 		case T_RenameStmt:
 		case T_CommentStmt:
 		case T_DefineStmt:
+		case T_CreateAssertionStmt:
 		case T_CreateCastStmt:
 		case T_CreateEventTrigStmt:
 		case T_AlterEventTrigStmt:
@@ -1503,6 +1505,10 @@ ProcessUtilitySlow(ParseState *pstate,
 				address = DefineDomain((CreateDomainStmt *) parsetree);
 				break;
 
+			case T_CreateAssertionStmt:
+				address = CreateAssertion((CreateAssertionStmt *) parsetree);
+				break;
+
 			case T_CreateConversionStmt:
 				address = CreateConversionCommand((CreateConversionStmt *) parsetree);
 				break;
@@ -1913,6 +1919,9 @@ AlterObjectTypeCommandTag(ObjectType objtype)
 		case OBJECT_AGGREGATE:
 			tag = "ALTER AGGREGATE";
 			break;
+		case OBJECT_ASSERTION:
+			tag = "ALTER ASSERTION";
+			break;
 		case OBJECT_ATTRIBUTE:
 			tag = "ALTER TYPE";
 			break;
@@ -2251,6 +2260,9 @@ CreateCommandTag(Node *parsetree)
 				case OBJECT_DOMAIN:
 					tag = "DROP DOMAIN";
 					break;
+				case OBJECT_ASSERTION:
+					tag = "DROP ASSERTION";
+					break;
 				case OBJECT_COLLATION:
 					tag = "DROP COLLATION";
 					break;
@@ -2681,6 +2693,10 @@ CreateCommandTag(Node *parsetree)
 			tag = "REINDEX";
 			break;
 
+		case T_CreateAssertionStmt:
+			tag = "CREATE ASSERTION";
+			break;
+
 		case T_CreateConversionStmt:
 			tag = "CREATE CONVERSION";
 			break;
@@ -3281,6 +3297,10 @@ GetCommandLogLevel(Node *parsetree)
 			lev = LOGSTMT_ALL;	/* should this be DDL? */
 			break;
 
+		case T_CreateAssertionStmt:
+			lev = LOGSTMT_DDL;
+			break;
+
 		case T_CreateConversionStmt:
 			lev = LOGSTMT_DDL;
 			break;
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index bba595ad1d..6a0d6edb81 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -1542,6 +1542,25 @@ get_func_retset(Oid funcid)
 	return result;
 }
 
+/*
+ * get_func_lang
+ *		Given procedure id, return the function's language id.
+ */
+Oid
+get_func_lang(Oid funcid)
+{
+	HeapTuple	tp;
+	Oid			result;
+
+	tp = SearchSysCache1(PROCOID, ObjectIdGetDatum(funcid));
+	if (!HeapTupleIsValid(tp))
+		elog(ERROR, "cache lookup failed for function %u", funcid);
+
+	result = ((Form_pg_proc) GETSTRUCT(tp))->prolang;
+	ReleaseSysCache(tp);
+	return result;
+}
+
 /*
  * func_strict
  *		Given procedure id, return the function's proisstrict flag.
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 4c85f43f09..f9345997eb 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -785,6 +785,9 @@ exec_command_d(PsqlScanState scan_state, bool active_branch, const char *cmd)
 			case 'p':
 				success = permissionsList(pattern);
 				break;
+			case 'Q':
+				success = describeAssertions(pattern);
+				break;
 			case 'T':
 				success = describeTypes(pattern, show_verbose, show_system);
 				break;
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 410131e5c7..4f01061948 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -140,6 +140,66 @@ describeAggregates(const char *pattern, bool verbose, bool showSystem)
 	return true;
 }
 
+/* \dA
+ * Takes an optional regexp to select particular assertions
+ */
+bool
+describeAssertions(const char *pattern)
+{
+	PQExpBufferData buf;
+	PGresult   *res;
+	printQueryOpt myopt = pset.popt;
+
+	if (pset.sversion < 90400)
+	{
+		fprintf(stderr, _("The server (version %d.%d) does not support assertions.\n"),
+				pset.sversion / 10000, (pset.sversion / 100) % 100);
+		return true;
+	}
+
+	initPQExpBuffer(&buf);
+
+	printfPQExpBuffer(&buf,
+					  "SELECT n.nspname AS \"%s\",\n"
+							  "  c.conname AS \"%s\",\n"
+							  "  pg_catalog.pg_get_constraintdef(c.oid, true) AS \"%s\",\n"
+							  "  pg_catalog.obj_description(c.oid, 'pg_constraint') AS \"%s\"\n"
+							  "FROM pg_catalog.pg_constraint c\n"
+							  "     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.connamespace\n"
+							  "WHERE c.conrelid = 0 AND c.contypid = 0\n",
+					  gettext_noop("Schema"),
+					  gettext_noop("Name"),
+					  gettext_noop("Definition"),
+					  gettext_noop("Description"));
+
+	if (!pattern)
+		appendPQExpBuffer(&buf, "      AND n.nspname <> 'pg_catalog'\n"
+				"      AND n.nspname <> 'information_schema'\n");
+
+#if TODO
+	processSQLNamePattern(pset.db, &buf, pattern, true, false,
+						  "n.nspname", "c.conname", NULL,
+						  "pg_catalog.pg_constraint_is_visible(c.oid)");
+#endif
+
+	appendPQExpBuffer(&buf, "ORDER BY 1, 2, 3, 4;");
+
+	res = PSQLexec(buf.data);
+	termPQExpBuffer(&buf);
+	if (!res)
+		return false;
+
+	myopt.nullPrint = NULL;
+	myopt.title = _("List of assertions");
+	myopt.translate_header = true;
+
+	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
+
+	PQclear(res);
+	return true;
+}
+
+
 /*
  * \dA
  * Takes an optional regexp to select particular access methods
@@ -2518,6 +2578,38 @@ describeOneTableDetails(const char *schemaname,
 			PQclear(result);
 		}
 
+		/* print assertions referencing this table (none if no triggers) */
+		if (tableinfo.hastriggers)
+		{
+			printfPQExpBuffer(&buf,
+							  "SELECT conname,\n"
+									  "  pg_catalog.pg_get_constraintdef(c.oid, true) AS condef\n"
+									  "FROM pg_catalog.pg_constraint c\n"
+									  "WHERE c.oid IN (SELECT objid FROM pg_depend WHERE classid = 'pg_constraint'::pg_catalog.regclass AND refclassid = 'pg_class'::pg_catalog.regclass AND refobjid = '%s')\n"
+									  "  AND c.conrelid = 0 AND c.contypid = 0 AND c.contype = 'c'\n"
+									  "ORDER BY 1",
+							  oid);
+			result = PSQLexec(buf.data);
+			if (!result)
+				goto error_return;
+			else
+				tuples = PQntuples(result);
+
+			if (tuples > 0)
+			{
+				printTableAddFooter(&cont, _("Assertions:"));
+				for (i = 0; i < tuples; i++)
+				{
+					printfPQExpBuffer(&buf, "    \"%s\" %s",
+									  PQgetvalue(result, i, 0),
+									  PQgetvalue(result, i, 1));
+
+					printTableAddFooter(&cont, buf.data);
+				}
+			}
+			PQclear(result);
+		}
+
 		/* print rules */
 		if (tableinfo.hasrules && tableinfo.relkind != RELKIND_MATVIEW)
 		{
diff --git a/src/bin/psql/describe.h b/src/bin/psql/describe.h
index a4cc5efae0..7b405b65da 100644
--- a/src/bin/psql/describe.h
+++ b/src/bin/psql/describe.h
@@ -15,6 +15,9 @@ extern bool describeAggregates(const char *pattern, bool verbose, bool showSyste
 /* \dA */
 extern bool describeAccessMethods(const char *pattern, bool verbose);
 
+/* \dQ */
+extern bool describeAssertions(const char *pattern);
+
 /* \db */
 extern bool describeTablespaces(const char *pattern, bool verbose);
 
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index 702e742af4..9deabd195f 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -223,6 +223,7 @@ slashUsage(unsigned short int pager)
 	fprintf(output, _("  \\d[S+]                 list tables, views, and sequences\n"));
 	fprintf(output, _("  \\d[S+]  NAME           describe table, view, sequence, or index\n"));
 	fprintf(output, _("  \\da[S]  [PATTERN]      list aggregates\n"));
+	fprintf(output, _("  \\dQ     [PATTERN]      list assertions\n"));
 	fprintf(output, _("  \\dA[+]  [PATTERN]      list access methods\n"));
 	fprintf(output, _("  \\db[+]  [PATTERN]      list tablespaces\n"));
 	fprintf(output, _("  \\dc[S+] [PATTERN]      list conversions\n"));
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index b431efc983..b44632d5b8 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -424,6 +424,24 @@ static const SchemaQuery Query_for_list_of_aggregates[] = {
 	}
 };
 
+static const SchemaQuery Query_for_list_of_assertions = {
+	/* min_server_version */
+	0,
+	/* catname */
+	"pg_catalog.pg_constraint c",
+	/* selcondition */
+	"(c.conrelid = 0 AND c.contypid = 0)",
+	/* viscondition */
+	"TRUE", //TODO: "pg_catalog.pg_constraint_is_visible(c.oid)",
+	/* namespace */
+	"c.connamespace",
+	/* result */
+	"pg_catalog.quote_ident(c.conname)",
+	/* qualresult */
+	NULL
+};
+
+
 static const SchemaQuery Query_for_list_of_datatypes = {
 	/* min_server_version */
 	0,
@@ -1192,6 +1210,7 @@ typedef struct
 static const pgsql_thing_t words_after_create[] = {
 	{"ACCESS METHOD", NULL, NULL, NULL, THING_NO_ALTER},
 	{"AGGREGATE", NULL, NULL, Query_for_list_of_aggregates},
+	{"ASSERTION", NULL, NULL, &Query_for_list_of_assertions},
 	{"CAST", NULL, NULL, NULL}, /* Casts have complex structures for names, so
 								 * skip it */
 	{"COLLATION", "SELECT pg_catalog.quote_ident(collname) FROM pg_catalog.pg_collation WHERE collencoding IN (-1, pg_catalog.pg_char_to_encoding(pg_catalog.getdatabaseencoding())) AND substring(pg_catalog.quote_ident(collname),1,%d)='%s'"},
@@ -1618,7 +1637,7 @@ psql_completion(const char *text, int start, int end)
 		"\\a",
 		"\\connect", "\\conninfo", "\\C", "\\cd", "\\copy",
 		"\\copyright", "\\crosstabview",
-		"\\d", "\\da", "\\dA", "\\db", "\\dc", "\\dC", "\\dd", "\\ddp", "\\dD",
+		"\\d", "\\da", "\\dA", "\\dQ", "\\db", "\\dc", "\\dC", "\\dd", "\\ddp", "\\dD",
 		"\\des", "\\det", "\\deu", "\\dew", "\\dE", "\\df",
 		"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
 		"\\dm", "\\dn", "\\do", "\\dO", "\\dp",
@@ -1769,6 +1788,11 @@ psql_completion(const char *text, int start, int end)
 	else if (Matches3("ALTER", "SCHEMA", MatchAny))
 		COMPLETE_WITH_LIST2("OWNER TO", "RENAME TO");
 
+	// TODO ASSERTION
+
+	else if (Matches3("ALTER", "ASSERTION", MatchAny))
+		COMPLETE_WITH_CONST("RENAME TO ");
+
 	/* ALTER COLLATION <name> */
 	else if (Matches3("ALTER", "COLLATION", MatchAny))
 		COMPLETE_WITH_LIST3("OWNER TO", "RENAME TO", "SET SCHEMA");
@@ -2897,7 +2921,7 @@ psql_completion(const char *text, int start, int end)
 /* DROP */
 	/* Complete DROP object with CASCADE / RESTRICT */
 	else if (Matches3("DROP",
-					  "COLLATION|CONVERSION|DOMAIN|EXTENSION|LANGUAGE|PUBLICATION|SCHEMA|SEQUENCE|SERVER|SUBSCRIPTION|STATISTICS|TABLE|TYPE|VIEW",
+					  "ASSERTION|COLLATION|CONVERSION|DOMAIN|EXTENSION|LANGUAGE|PUBLICATION|SCHEMA|SEQUENCE|SERVER|SUBSCRIPTION|STATISTICS|TABLE|TYPE|VIEW",
 					  MatchAny) ||
 			 Matches4("DROP", "ACCESS", "METHOD", MatchAny) ||
 			 (Matches4("DROP", "AGGREGATE|FUNCTION|PROCEDURE|ROUTINE", MatchAny, MatchAny) &&
@@ -3633,6 +3657,8 @@ psql_completion(const char *text, int start, int end)
 	}
 	else if (TailMatchesCS1("\\da*"))
 		COMPLETE_WITH_VERSIONED_SCHEMA_QUERY(Query_for_list_of_aggregates, NULL);
+	else if (TailMatchesCS1("\\dQ*"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_assertions, NULL);
 	else if (TailMatchesCS1("\\dA*"))
 		COMPLETE_WITH_QUERY(Query_for_list_of_access_methods);
 	else if (TailMatchesCS1("\\db*"))
diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h
index 7991de5e21..5b5c21f976 100644
--- a/src/include/catalog/namespace.h
+++ b/src/include/catalog/namespace.h
@@ -151,6 +151,7 @@ extern bool OverrideSearchPathMatchesCurrent(OverrideSearchPath *path);
 extern void PushOverrideSearchPath(OverrideSearchPath *newpath);
 extern void PopOverrideSearchPath(void);
 
+extern Oid	get_assertion_oid(List *name, bool missing_ok);
 extern Oid	get_collation_oid(List *collname, bool missing_ok);
 extern Oid	get_conversion_oid(List *conname, bool missing_ok);
 extern Oid	FindDefaultConversionProc(int32 for_encoding, int32 to_encoding);
diff --git a/src/include/catalog/pg_constraint.h b/src/include/catalog/pg_constraint.h
index 7c1c0e1db8..038ff60116 100644
--- a/src/include/catalog/pg_constraint.h
+++ b/src/include/catalog/pg_constraint.h
@@ -58,8 +58,7 @@ CATALOG(pg_constraint,2606,ConstraintRelationId)
 	 * contypid links to the pg_type row for a domain if this is a domain
 	 * constraint.  Otherwise it's 0.
 	 *
-	 * For SQL-style global ASSERTIONs, both conrelid and contypid would be
-	 * zero. This is not presently supported, however.
+	 * For SQL-style global ASSERTIONs, both conrelid and contypid are zero.
 	 */
 	Oid			contypid;		/* domain this constraint constrains */
 
@@ -184,7 +183,7 @@ typedef enum ConstraintCategory
 {
 	CONSTRAINT_RELATION,
 	CONSTRAINT_DOMAIN,
-	CONSTRAINT_ASSERTION		/* for future expansion */
+	CONSTRAINT_ASSERTION
 } ConstraintCategory;
 
 /*
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 66c6c224a8..30c4d74421 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -3711,6 +3711,10 @@
   proname => 'unique_key_recheck', provolatile => 'v', prorettype => 'trigger',
   proargtypes => '', prosrc => 'unique_key_recheck' },
 
+{ oid => '3996', descr => 'assertion constraint check',
+  proname => 'assertion_check', provolatile => 'v', prorettype => 'trigger',
+  proargtypes => '', prosrc => 'assertion_check' },
+
 # Generic referential integrity constraint triggers
 { oid => '1644', descr => 'referential integrity FOREIGN KEY ... REFERENCES',
   proname => 'RI_FKey_check_ins', provolatile => 'v', prorettype => 'trigger',
diff --git a/src/include/commands/constraint.h b/src/include/commands/constraint.h
new file mode 100644
index 0000000000..98aa8f996e
--- /dev/null
+++ b/src/include/commands/constraint.h
@@ -0,0 +1,10 @@
+#ifndef CONSTRAINT_H
+#define CONSTRAINT_H
+
+#include "catalog/objectaddress.h"
+#include "nodes/parsenodes.h"
+
+extern ObjectAddress CreateAssertion(CreateAssertionStmt *stmt);
+extern ObjectAddress RenameAssertion(RenameStmt *stmt);
+
+#endif
\ No newline at end of file
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index adb159a6da..e5dd5d1618 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -418,6 +418,7 @@ typedef enum NodeTag
 	T_DropSubscriptionStmt,
 	T_CreateStatsStmt,
 	T_AlterCollationStmt,
+	T_CreateAssertionStmt,
 	T_CallStmt,
 
 	/*
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 6390f7e8c1..389cf6331c 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -1637,6 +1637,7 @@ typedef enum ObjectType
 	OBJECT_AGGREGATE,
 	OBJECT_AMOP,
 	OBJECT_AMPROC,
+	OBJECT_ASSERTION,
 	OBJECT_ATTRIBUTE,			/* type's attribute, when distinct from column */
 	OBJECT_CAST,
 	OBJECT_COLUMN,
@@ -3290,6 +3291,16 @@ typedef struct ReindexStmt
 	int			options;		/* Reindex options flags */
 } ReindexStmt;
 
+/* ----------------------
+ *      CREATE ASSERTION Statement
+ */
+typedef struct CreateAssertionStmt
+{
+	NodeTag		type;
+	List    	*assertion_name;
+	Constraint	*constraint;
+} CreateAssertionStmt;
+
 /* ----------------------
  *		CREATE CONVERSION Statement
  * ----------------------
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
index 0230543810..54a6b114ff 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -60,6 +60,7 @@ typedef enum ParseExprKind
 	EXPR_KIND_VALUES_SINGLE,	/* single-row VALUES (in INSERT only) */
 	EXPR_KIND_CHECK_CONSTRAINT, /* CHECK constraint for a table */
 	EXPR_KIND_DOMAIN_CHECK,		/* CHECK constraint for a domain */
+	EXPR_KIND_ASSERTION_CHECK,	/* CHECK constraint for an assertion */
 	EXPR_KIND_COLUMN_DEFAULT,	/* default value for a table column */
 	EXPR_KIND_FUNCTION_DEFAULT, /* default parameter value for function */
 	EXPR_KIND_INDEX_EXPRESSION, /* index expression */
diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h
index f4d4be8d0d..2c10dd082f 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -295,6 +295,7 @@ extern bool pg_opclass_ownercheck(Oid opc_oid, Oid roleid);
 extern bool pg_opfamily_ownercheck(Oid opf_oid, Oid roleid);
 extern bool pg_database_ownercheck(Oid db_oid, Oid roleid);
 extern bool pg_collation_ownercheck(Oid coll_oid, Oid roleid);
+extern bool pg_constraint_ownercheck(Oid constr_oid, Oid roleid);
 extern bool pg_conversion_ownercheck(Oid conv_oid, Oid roleid);
 extern bool pg_ts_dict_ownercheck(Oid dict_oid, Oid roleid);
 extern bool pg_ts_config_ownercheck(Oid cfg_oid, Oid roleid);
diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h
index e55ea4035b..f618270bc8 100644
--- a/src/include/utils/lsyscache.h
+++ b/src/include/utils/lsyscache.h
@@ -114,6 +114,7 @@ extern int	get_func_nargs(Oid funcid);
 extern Oid	get_func_signature(Oid funcid, Oid **argtypes, int *nargs);
 extern Oid	get_func_variadictype(Oid funcid);
 extern bool get_func_retset(Oid funcid);
+extern Oid  get_func_lang(Oid funcid);
 extern bool func_strict(Oid funcid);
 extern char func_volatile(Oid funcid);
 extern char func_parallel(Oid funcid);
diff --git a/src/test/regress/expected/assertions.out b/src/test/regress/expected/assertions.out
new file mode 100644
index 0000000000..7fc0d710a3
--- /dev/null
+++ b/src/test/regress/expected/assertions.out
@@ -0,0 +1,779 @@
+BEGIN TRANSACTION;
+--
+-- Create some helper views and functions to allow us to more easily
+-- assert which operations will cause an assertion to be checked.
+--
+CREATE OR REPLACE VIEW invalidating_operation
+  (assertion_name,
+   relation_name,
+   operation)
+AS
+SELECT c.conname,
+       cl.relname,
+       v.operation
+  FROM pg_trigger t
+  JOIN pg_constraint c ON (t.tgconstraint = c.oid)
+  JOIN pg_proc p ON (t.tgfoid = p.oid)
+  JOIN pg_class cl ON (t.tgrelid = cl.oid)
+  JOIN (VALUES ('INSERT', 1<<2),
+               ('DELETE', 1<<3|1<<5),
+               ('UPDATE', 1<<4))
+    AS v(operation, mask) ON ((v.mask & tgtype) > 0)
+ WHERE t.tgisinternal
+   AND p.proname = 'assertion_check';
+CREATE OR REPLACE VIEW invalidating_by_update_of_column
+  (assertion_name,
+   relation_name,
+   column_name)
+AS
+SELECT c.conname,
+       cl.relname,
+       a.attname
+  FROM pg_trigger t INNER JOIN pg_constraint c ON (t.tgconstraint = c.oid)
+                    INNER JOIN pg_proc p ON (t.tgfoid = p.oid)
+                    INNER JOIN pg_class cl ON (t.tgrelid = cl.oid)
+                    CROSS JOIN LATERAL UNNEST(t.tgattr) AS co(n)
+                    INNER JOIN pg_attribute a ON (a.attrelid = cl.oid AND attnum = co.n)
+ WHERE t.tgisinternal
+   AND p.proname = 'assertion_check'
+   AND (tgtype & (1 << 4)) > 0;
+CREATE OR REPLACE VIEW invalidating_summary
+  (assertion_name,
+   operations)
+AS
+SELECT assertion_name,
+       string_agg(operation, ' ' ORDER BY operation)
+  FROM (SELECT assertion_name,
+               operation ||'('|| string_agg(relation_name, ', ' ORDER BY relation_name) ||')'
+          FROM invalidating_operation
+         WHERE operation <> 'UPDATE'
+         GROUP BY assertion_name,
+                  operation
+         UNION
+        SELECT assertion_name,
+               operation ||'('|| string_agg(relation_name ||'.'|| column_name, ', ' ORDER BY relation_name, column_name) ||')'
+          FROM invalidating_by_update_of_column JOIN invalidating_operation USING (assertion_name, relation_name)
+         WHERE operation = 'UPDATE'
+         GROUP BY assertion_name,
+                  operation)
+    AS v(assertion_name, operation)
+ GROUP BY assertion_name;
+CREATE OR REPLACE FUNCTION have_identical_invalidating_operations
+  (a text, b text)
+RETURNS TABLE (predicate text, correct text)
+AS $$
+  SELECT 'assertion "'|| a ||'" has identical invalidating operations to assertion "'|| b ||'"',
+         CASE WHEN
+           NOT EXISTS (SELECT relation_name, operation
+                         FROM invalidating_operation
+                        WHERE assertion_name = a
+                       EXCEPT 
+                       SELECT relation_name, operation
+                         FROM invalidating_operation
+                        WHERE assertion_name = b) AND
+           NOT EXISTS (SELECT relation_name, column_name
+                         FROM invalidating_by_update_of_column
+                        WHERE assertion_name = a
+                       EXCEPT
+                       SELECT relation_name, column_name
+                         FROM invalidating_by_update_of_column
+                        WHERE assertion_name = b) THEN 'YES'
+                                                  ELSE 'NO' END
+$$ LANGUAGE SQL;
+COMMIT;
+BEGIN TRANSACTION;
+--
+-- Perform some basic sanity checking on creating assertions
+--
+CREATE TABLE test1 (a int, b text);
+CREATE ASSERTION foo CHECK (1 < 2);
+CREATE ASSERTION a2 CHECK ((SELECT count(*) FROM test1) < 5);
+DELETE FROM test1;
+INSERT INTO test1 VALUES (1, 'one');
+INSERT INTO test1 VALUES (2, 'two');
+INSERT INTO test1 VALUES (3, 'three');
+INSERT INTO test1 VALUES (4, 'four');
+SAVEPOINT pre_insert_too_many;
+INSERT INTO test1 VALUES (5, 'five');
+ERROR:  assertion "a2" violated
+ROLLBACK TO SAVEPOINT pre_insert_too_many;
+SELECT constraint_schema,
+       constraint_name
+  FROM information_schema.assertions
+ ORDER BY 1, 2;
+ constraint_schema | constraint_name 
+-------------------+-----------------
+ public            | a2
+ public            | foo
+(2 rows)
+
+\dQ
+                        List of assertions
+ Schema | Name |             Definition             | Description 
+--------+------+------------------------------------+-------------
+ public | a2   | CHECK ((( SELECT count(*) AS count+| 
+        |      |    FROM test1)) < 5)               | 
+ public | foo  | CHECK (1 < 2)                      | 
+(2 rows)
+
+ALTER ASSERTION a2 RENAME TO a3;
+SAVEPOINT pre_rename_foo;
+ALTER ASSERTION foo RENAME TO a3; -- fails
+ERROR:  assertion "a3" already exists
+ROLLBACK TO SAVEPOINT pre_rename_foo;
+DROP ASSERTION foo;
+SAVEPOINT pre_drop_test1;
+DROP TABLE test1; -- fails
+ERROR:  cannot drop table test1 because other objects depend on it
+DETAIL:  constraint a3 depends on table test1
+HINT:  Use DROP ... CASCADE to drop the dependent objects too.
+ROLLBACK TO SAVEPOINT pre_drop_test1;
+DROP TABLE test1 CASCADE;
+NOTICE:  drop cascades to constraint a3
+ROLLBACK;
+BEGIN TRANSACTION;
+--
+-- Expressions involving various operators, functions and casts
+-- 
+CREATE TABLE t (n INTEGER NOT NULL PRIMARY KEY, p BOOLEAN NOT NULL);
+INSERT INTO t (n, p) VALUES (20, TRUE);
+CREATE ASSERTION is_distinct_from CHECK (10 IS DISTINCT FROM (SELECT MIN(n) FROM t));
+CREATE ASSERTION is_not_distinct_from CHECK (20 IS NOT DISTINCT FROM (SELECT MIN(n) FROM t));
+CREATE ASSERTION null_if CHECK (NULLIF((SELECT BOOL_AND(p) FROM t), FALSE));
+CREATE ASSERTION coerce_to_boolean_from_min CHECK (CAST((SELECT MIN(n) FROM t) AS BOOLEAN));
+CREATE ASSERTION coerce_to_boolean_from_bool_and CHECK (CAST((SELECT BOOL_AND(p) FROM t) AS BOOLEAN));
+CREATE ASSERTION coerce_to_boolean_from_boolean CHECK (CAST((SELECT TRUE FROM t) AS BOOLEAN));
+ROLLBACK;
+BEGIN TRANSACTION;
+--
+-- Expressions involving function calls
+--
+CREATE TABLE t (n INTEGER NOT NULL PRIMARY KEY, m INTEGER NOT NULL, d DATE NOT NULL);
+CREATE FUNCTION f(n INTEGER) RETURNS INTEGER
+AS $$
+  SELECT n * 2
+$$ LANGUAGE SQL;
+CREATE FUNCTION r(n INTEGER) RETURNS TABLE (m INTEGER )
+AS $$
+  SELECT m
+    FROM t
+   WHERE n > r.n
+$$ LANGUAGE SQL;
+CREATE ASSERTION age_of_d_in_t CHECK (
+  NOT EXISTS (
+    SELECT FROM t
+     WHERE AGE(DATE '01-01-2018') > INTERVAL '10 days' -- AGE(DATE) is built-in SQL
+  )
+);
+CREATE ASSERTION use_f CHECK (f(0) = 0);
+CREATE ASSERTION use_f_in_predicate CHECK (NOT EXISTS (SELECT FROM t WHERE f(n) = 20));
+CREATE ASSERTION use_f_in_target CHECK (NOT EXISTS (SELECT f(n) FROM t));
+CREATE ASSERTION use_r_in_from CHECK (NOT EXISTS (SELECT FROM r(100)));
+CREATE ASSERTION use_r_in_target CHECK (NOT EXISTS (SELECT r(100)));
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (COALESCE(operations, 'NONE') = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('age_of_d_in_t',      'INSERT(t)'),
+       ('use_f',              'NONE'),
+       ('use_f_in_predicate', 'INSERT(t) UPDATE(t.n)'),
+       ('use_f_in_target',    'INSERT(t)'),
+       ('use_r_in_from',      'INSERT(t) UPDATE(t.m, t.n)'), -- TODO should _from and _target be the same?
+       ('use_r_in_target',    'INSERT(t) UPDATE(t.n)'))
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+   assertion_name   |           actual           |          expected          | correct 
+--------------------+----------------------------+----------------------------+---------
+ age_of_d_in_t      | INSERT(t)                  | INSERT(t)                  | YES
+ use_f              | NONE                       | NONE                       | YES
+ use_f_in_predicate | INSERT(t) UPDATE(t.n)      | INSERT(t) UPDATE(t.n)      | YES
+ use_f_in_target    | INSERT(t)                  | INSERT(t)                  | YES
+ use_r_in_from      | INSERT(t) UPDATE(t.m, t.n) | INSERT(t) UPDATE(t.m, t.n) | YES
+ use_r_in_target    | INSERT(t) UPDATE(t.n)      | INSERT(t) UPDATE(t.n)      | YES
+(6 rows)
+
+CREATE FUNCTION g(n INTEGER) RETURNS INTEGER
+AS $$
+BEGIN
+  RETURN n * 2;
+END;
+$$ LANGUAGE PLPGSQL;
+-- Use of functions other than internal or those implemented in SQL are illegal
+CREATE ASSERTION use_g_in_from CHECK (NOT EXISTS (SELECT FROM g(100))); -- fails
+ERROR:  function "g" uses unsupported language "plpgsql"
+ROLLBACK;
+BEGIN TRANSACTION;
+--
+-- Expressions involving EXISTS and NOT EXISTS
+--
+CREATE TABLE r (n INTEGER NOT NULL PRIMARY KEY);
+CREATE TABLE s (n INTEGER NOT NULL PRIMARY KEY);
+CREATE TABLE t (n INTEGER NOT NULL PRIMARY KEY, m INTEGER NOT NULL);
+CREATE TABLE t1 (n INTEGER NOT NULL PRIMARY KEY, m INTEGER NOT NULL);
+CREATE TABLE t2 (n INTEGER NOT NULL PRIMARY KEY, m INTEGER NOT NULL);
+INSERT INTO r (n) VALUES (1);
+INSERT INTO t (n, m) VALUES (0, 0);
+INSERT INTO t1 (n, m) VALUES (0, 0);
+INSERT INTO t2 (n, m) VALUES (0, 0);
+CREATE ASSERTION exists_no_predicate CHECK (
+  EXISTS (SELECT FROM r)
+);
+CREATE ASSERTION exists_with_predicate CHECK (
+  EXISTS (SELECT FROM r WHERE n > 0)
+);
+CREATE ASSERTION not_exists_no_predicate CHECK (
+  NOT EXISTS (SELECT FROM s)
+);
+CREATE ASSERTION not_exists_with_predicate CHECK (
+  NOT EXISTS (SELECT FROM r WHERE n < 1)
+);
+CREATE ASSERTION direct_subject_of_an_exists CHECK (
+  EXISTS (SELECT n FROM t WHERE m = 0)
+);
+-- TODO These can be optimised, at least if the set operation is UNION.
+CREATE ASSERTION except_subject_of_an_exists CHECK (
+  EXISTS (SELECT n FROM t1 WHERE m = 0 EXCEPT SELECT n FROM t2 WHERE m = 1)
+);
+CREATE ASSERTION intersect_subject_of_an_exists CHECK (
+  EXISTS (SELECT n FROM t1 WHERE m = 0 INTERSECT SELECT n FROM t2 WHERE m = 0)
+);
+CREATE ASSERTION union_subject_of_an_exists CHECK (
+  EXISTS (SELECT n FROM t1 WHERE m = 0 UNION SELECT n FROM t2 WHERE m = 0)
+);
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (operations = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('exists_no_predicate',            'DELETE(r)'),
+       ('exists_with_predicate',          'DELETE(r) UPDATE(r.n)'),
+       ('not_exists_no_predicate',        'INSERT(s)'),
+       ('not_exists_with_predicate',      'INSERT(r) UPDATE(r.n)'),
+       ('direct_subject_of_an_exists',    'DELETE(t) UPDATE(t.m)'),
+       ('except_subject_of_an_exists',    'DELETE(t1) INSERT(t2) UPDATE(t1.m, t1.n, t2.m, t2.n)'),
+       ('intersect_subject_of_an_exists', 'DELETE(t1, t2) UPDATE(t1.m, t1.n, t2.m, t2.n)'),
+       ('union_subject_of_an_exists',     'DELETE(t1, t2) UPDATE(t1.m, t1.n, t2.m, t2.n)'))
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+         assertion_name         |                        actual                        |                       expected                       | correct 
+--------------------------------+------------------------------------------------------+------------------------------------------------------+---------
+ direct_subject_of_an_exists    | DELETE(t) UPDATE(t.m)                                | DELETE(t) UPDATE(t.m)                                | YES
+ except_subject_of_an_exists    | DELETE(t1) INSERT(t2) UPDATE(t1.m, t1.n, t2.m, t2.n) | DELETE(t1) INSERT(t2) UPDATE(t1.m, t1.n, t2.m, t2.n) | YES
+ exists_no_predicate            | DELETE(r)                                            | DELETE(r)                                            | YES
+ exists_with_predicate          | DELETE(r) UPDATE(r.n)                                | DELETE(r) UPDATE(r.n)                                | YES
+ intersect_subject_of_an_exists | DELETE(t1, t2) UPDATE(t1.m, t1.n, t2.m, t2.n)        | DELETE(t1, t2) UPDATE(t1.m, t1.n, t2.m, t2.n)        | YES
+ not_exists_no_predicate        | INSERT(s)                                            | INSERT(s)                                            | YES
+ not_exists_with_predicate      | INSERT(r) UPDATE(r.n)                                | INSERT(r) UPDATE(r.n)                                | YES
+ union_subject_of_an_exists     | DELETE(t1, t2) UPDATE(t1.m, t1.n, t2.m, t2.n)        | DELETE(t1, t2) UPDATE(t1.m, t1.n, t2.m, t2.n)        | YES
+(8 rows)
+
+ROLLBACK;
+BEGIN TRANSACTION;
+--
+-- Expressions involving NOT
+--
+CREATE TABLE r (p BOOLEAN NOT NULL PRIMARY KEY);
+INSERT INTO r (p) VALUES (TRUE);
+CREATE ASSERTION a CHECK (EXISTS (SELECT FROM r WHERE p));
+CREATE ASSERTION b CHECK (NOT EXISTS (SELECT FROM r WHERE NOT p));
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (operations = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('a', 'DELETE(r) UPDATE(r.p)'),
+       ('b', 'INSERT(r) UPDATE(r.p)')) -- NOT(x) and x have opposite invalidating operations
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+ assertion_name |        actual         |       expected        | correct 
+----------------+-----------------------+-----------------------+---------
+ a              | DELETE(r) UPDATE(r.p) | DELETE(r) UPDATE(r.p) | YES
+ b              | INSERT(r) UPDATE(r.p) | INSERT(r) UPDATE(r.p) | YES
+(2 rows)
+
+ROLLBACK;
+BEGIN TRANSACTION;
+--
+-- Expressions involving set operations INTERSECT, UNION, and EXCEPT
+--
+CREATE TABLE r (n INTEGER NOT NULL PRIMARY KEY);
+CREATE TABLE s (n INTEGER NOT NULL PRIMARY KEY);
+CREATE TABLE t (n INTEGER NOT NULL PRIMARY KEY);
+INSERT INTO t (n) VALUES (0);
+CREATE ASSERTION except_operands CHECK (NOT EXISTS (SELECT FROM r EXCEPT SELECT FROM s));
+CREATE ASSERTION intersect_operands CHECK (NOT EXISTS (SELECT FROM r INTERSECT SELECT FROM s));
+CREATE ASSERTION union_operands CHECK (EXISTS (SELECT FROM s UNION SELECT FROM t));
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (operations = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('except_operands',    'DELETE(s) INSERT(r)'),
+       ('intersect_operands', 'INSERT(r, s)'),
+       ('union_operands',     'DELETE(s, t)'))
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+   assertion_name   |       actual        |      expected       | correct 
+--------------------+---------------------+---------------------+---------
+ except_operands    | DELETE(s) INSERT(r) | DELETE(s) INSERT(r) | YES
+ intersect_operands | INSERT(r, s)        | INSERT(r, s)        | YES
+ union_operands     | DELETE(s, t)        | DELETE(s, t)        | YES
+(3 rows)
+
+ROLLBACK;
+BEGIN TRANSACTION;
+--
+-- Expressions involving COUNT aggregations
+--
+CREATE TABLE s (n INTEGER NOT NULL PRIMARY KEY, m INTEGER NOT NULL);
+INSERT INTO s VALUES (1, 2);
+CREATE ASSERTION ge_all_count CHECK (1 >= ALL (SELECT COUNT(*) FROM s));
+CREATE ASSERTION ge_any_count CHECK (1 >= ANY (SELECT COUNT(*) FROM s));
+CREATE ASSERTION ge_count     CHECK (1 >=     (SELECT COUNT(*) FROM s));
+CREATE ASSERTION gt_all_count CHECK (2 >  ALL (SELECT COUNT(*) FROM s));
+CREATE ASSERTION gt_any_count CHECK (2 >  ANY (SELECT COUNT(*) FROM s));
+CREATE ASSERTION gt_count     CHECK (2 >      (SELECT COUNT(*) FROM s));
+CREATE ASSERTION le_all_count CHECK (1 <= ALL (SELECT COUNT(*) FROM s));
+CREATE ASSERTION le_any_count CHECK (0 <= ANY (SELECT COUNT(*) FROM s));
+CREATE ASSERTION le_count     CHECK (0 <=     (SELECT COUNT(*) FROM s));
+CREATE ASSERTION lt_all_count CHECK (0 <  ALL (SELECT COUNT(*) FROM s));
+CREATE ASSERTION lt_any_count CHECK (0 <  ANY (SELECT COUNT(*) FROM s));
+CREATE ASSERTION lt_count     CHECK (0 <      (SELECT COUNT(*) FROM s));
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (operations = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('ge_all_count', 'INSERT(s)'),
+       ('ge_any_count', 'INSERT(s)'),
+       ('ge_count',     'INSERT(s)'),
+       ('gt_all_count', 'INSERT(s)'),
+       ('gt_any_count', 'INSERT(s)'),
+       ('gt_count',     'INSERT(s)'),
+       ('le_all_count', 'DELETE(s)'),
+       ('le_any_count', 'DELETE(s)'),
+       ('le_count',     'DELETE(s)'),
+       ('lt_all_count', 'DELETE(s)'),
+       ('lt_any_count', 'DELETE(s)'),
+       ('lt_count',     'DELETE(s)'))
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+ assertion_name |  actual   | expected  | correct 
+----------------+-----------+-----------+---------
+ ge_all_count   | INSERT(s) | INSERT(s) | YES
+ ge_any_count   | INSERT(s) | INSERT(s) | YES
+ ge_count       | INSERT(s) | INSERT(s) | YES
+ gt_all_count   | INSERT(s) | INSERT(s) | YES
+ gt_any_count   | INSERT(s) | INSERT(s) | YES
+ gt_count       | INSERT(s) | INSERT(s) | YES
+ le_all_count   | DELETE(s) | DELETE(s) | YES
+ le_any_count   | DELETE(s) | DELETE(s) | YES
+ le_count       | DELETE(s) | DELETE(s) | YES
+ lt_all_count   | DELETE(s) | DELETE(s) | YES
+ lt_any_count   | DELETE(s) | DELETE(s) | YES
+ lt_count       | DELETE(s) | DELETE(s) | YES
+(12 rows)
+
+ROLLBACK;
+BEGIN TRANSACTION;
+--
+-- Expressions involving MIN aggregations
+--
+CREATE TABLE s (n INTEGER NOT NULL PRIMARY KEY, m INTEGER NOT NULL);
+INSERT INTO s VALUES (1, 2);
+CREATE ASSERTION ge_any_min CHECK (1 >= ANY (SELECT MIN(n) FROM s));
+CREATE ASSERTION ge_all_min CHECK (1 >= ALL (SELECT MIN(n) FROM s));
+CREATE ASSERTION ge_min     CHECK (1 >=     (SELECT MIN(n) FROM s));
+CREATE ASSERTION gt_any_min CHECK (2 >  ANY (SELECT MIN(n) FROM s));
+CREATE ASSERTION gt_all_min CHECK (2 >  ALL (SELECT MIN(n) FROM s));
+CREATE ASSERTION gt_min     CHECK (2 >      (SELECT MIN(n) FROM s));
+CREATE ASSERTION le_any_min CHECK (0 <= ANY (SELECT MIN(n) FROM s));
+CREATE ASSERTION le_all_min CHECK (0 <= ALL (SELECT MIN(n) FROM s));
+CREATE ASSERTION le_min     CHECK (0 <=     (SELECT MIN(n) FROM s));
+CREATE ASSERTION lt_any_min CHECK (0 <  ANY (SELECT MIN(n) FROM s));
+CREATE ASSERTION lt_all_min CHECK (0 <  ALL (SELECT MIN(n) FROM s));
+CREATE ASSERTION lt_min     CHECK (0 <      (SELECT MIN(n) FROM s));
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (operations = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('ge_all_min', 'DELETE(s) UPDATE(s.n)'),
+       ('ge_any_min', 'DELETE(s) UPDATE(s.n)'),
+       ('ge_min',     'DELETE(s) UPDATE(s.n)'),
+       ('gt_all_min', 'DELETE(s) UPDATE(s.n)'),
+       ('gt_any_min', 'DELETE(s) UPDATE(s.n)'),
+       ('gt_min',     'DELETE(s) UPDATE(s.n)'),
+       ('le_all_min', 'INSERT(s) UPDATE(s.n)'),
+       ('le_any_min', 'INSERT(s) UPDATE(s.n)'),
+       ('le_min',     'INSERT(s) UPDATE(s.n)'),
+       ('lt_all_min', 'INSERT(s) UPDATE(s.n)'),
+       ('lt_any_min', 'INSERT(s) UPDATE(s.n)'),
+       ('lt_min',     'INSERT(s) UPDATE(s.n)'))
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+ assertion_name |        actual         |       expected        | correct 
+----------------+-----------------------+-----------------------+---------
+ ge_all_min     | DELETE(s) UPDATE(s.n) | DELETE(s) UPDATE(s.n) | YES
+ ge_any_min     | DELETE(s) UPDATE(s.n) | DELETE(s) UPDATE(s.n) | YES
+ ge_min         | DELETE(s) UPDATE(s.n) | DELETE(s) UPDATE(s.n) | YES
+ gt_all_min     | DELETE(s) UPDATE(s.n) | DELETE(s) UPDATE(s.n) | YES
+ gt_any_min     | DELETE(s) UPDATE(s.n) | DELETE(s) UPDATE(s.n) | YES
+ gt_min         | DELETE(s) UPDATE(s.n) | DELETE(s) UPDATE(s.n) | YES
+ le_all_min     | INSERT(s) UPDATE(s.n) | INSERT(s) UPDATE(s.n) | YES
+ le_any_min     | INSERT(s) UPDATE(s.n) | INSERT(s) UPDATE(s.n) | YES
+ le_min         | INSERT(s) UPDATE(s.n) | INSERT(s) UPDATE(s.n) | YES
+ lt_all_min     | INSERT(s) UPDATE(s.n) | INSERT(s) UPDATE(s.n) | YES
+ lt_any_min     | INSERT(s) UPDATE(s.n) | INSERT(s) UPDATE(s.n) | YES
+ lt_min         | INSERT(s) UPDATE(s.n) | INSERT(s) UPDATE(s.n) | YES
+(12 rows)
+
+ROLLBACK;
+BEGIN TRANSACTION;
+--
+-- Expressions involving MAX aggregations
+--
+CREATE TABLE s (n INTEGER NOT NULL PRIMARY KEY, m INTEGER NOT NULL);
+INSERT INTO s VALUES (1, 2);
+CREATE ASSERTION ge_any_max CHECK (1 >= ANY (SELECT MAX(n) FROM s));
+CREATE ASSERTION ge_all_max CHECK (1 >= ALL (SELECT MAX(n) FROM s));
+CREATE ASSERTION ge_max     CHECK (1 >=     (SELECT MAX(n) FROM s));
+CREATE ASSERTION gt_all_max CHECK (2 >  ALL (SELECT MAX(n) FROM s));
+CREATE ASSERTION gt_any_max CHECK (2 >  ANY (SELECT MAX(n) FROM s));
+CREATE ASSERTION gt_max     CHECK (2 >      (SELECT MAX(n) FROM s));
+CREATE ASSERTION le_all_max CHECK (0 <= ALL (SELECT MAX(n) FROM s));
+CREATE ASSERTION le_any_max CHECK (0 <= ANY (SELECT MAX(n) FROM s));
+CREATE ASSERTION le_max     CHECK (0 <=     (SELECT MAX(n) FROM s));
+CREATE ASSERTION lt_all_max CHECK (0 <  ALL (SELECT MAX(n) FROM s));
+CREATE ASSERTION lt_any_max CHECK (0 <  ANY (SELECT MAX(n) FROM s));
+CREATE ASSERTION lt_max     CHECK (0 <      (SELECT MAX(n) FROM s));
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (operations = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('ge_all_max', 'INSERT(s) UPDATE(s.n)'),
+       ('ge_any_max', 'INSERT(s) UPDATE(s.n)'),
+       ('ge_max',     'INSERT(s) UPDATE(s.n)'),
+       ('gt_all_max', 'INSERT(s) UPDATE(s.n)'),
+       ('gt_any_max', 'INSERT(s) UPDATE(s.n)'),
+       ('gt_max',     'INSERT(s) UPDATE(s.n)'),
+       ('le_all_max', 'DELETE(s) UPDATE(s.n)'),
+       ('le_any_max', 'DELETE(s) UPDATE(s.n)'),
+       ('le_max',     'DELETE(s) UPDATE(s.n)'),
+       ('lt_all_max', 'DELETE(s) UPDATE(s.n)'),
+       ('lt_any_max', 'DELETE(s) UPDATE(s.n)'),
+       ('lt_max',     'DELETE(s) UPDATE(s.n)'))
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+ assertion_name |        actual         |       expected        | correct 
+----------------+-----------------------+-----------------------+---------
+ ge_all_max     | INSERT(s) UPDATE(s.n) | INSERT(s) UPDATE(s.n) | YES
+ ge_any_max     | INSERT(s) UPDATE(s.n) | INSERT(s) UPDATE(s.n) | YES
+ ge_max         | INSERT(s) UPDATE(s.n) | INSERT(s) UPDATE(s.n) | YES
+ gt_all_max     | INSERT(s) UPDATE(s.n) | INSERT(s) UPDATE(s.n) | YES
+ gt_any_max     | INSERT(s) UPDATE(s.n) | INSERT(s) UPDATE(s.n) | YES
+ gt_max         | INSERT(s) UPDATE(s.n) | INSERT(s) UPDATE(s.n) | YES
+ le_all_max     | DELETE(s) UPDATE(s.n) | DELETE(s) UPDATE(s.n) | YES
+ le_any_max     | DELETE(s) UPDATE(s.n) | DELETE(s) UPDATE(s.n) | YES
+ le_max         | DELETE(s) UPDATE(s.n) | DELETE(s) UPDATE(s.n) | YES
+ lt_all_max     | DELETE(s) UPDATE(s.n) | DELETE(s) UPDATE(s.n) | YES
+ lt_any_max     | DELETE(s) UPDATE(s.n) | DELETE(s) UPDATE(s.n) | YES
+ lt_max         | DELETE(s) UPDATE(s.n) | DELETE(s) UPDATE(s.n) | YES
+(12 rows)
+
+ROLLBACK;
+BEGIN TRANSACTION;
+--
+-- Expressions involving BOOL_AND aggregations
+--
+CREATE TABLE t (n INTEGER NOT NULL PRIMARY KEY, p BOOLEAN NOT NULL);
+INSERT INTO t (n, p) VALUES (0, TRUE);
+CREATE ASSERTION eq_bool_and     CHECK (TRUE  =      (SELECT BOOL_AND(p) FROM t));
+CREATE ASSERTION eq_any_bool_and CHECK (TRUE  =  ANY (SELECT BOOL_AND(p) FROM t));
+CREATE ASSERTION eq_all_bool_and CHECK (TRUE  =  ALL (SELECT BOOL_AND(p) FROM t));
+CREATE ASSERTION ne_bool_and     CHECK (FALSE <>     (SELECT BOOL_AND(p) FROM t));
+CREATE ASSERTION ne_any_bool_and CHECK (FALSE <> ANY (SELECT BOOL_AND(p) FROM t));
+CREATE ASSERTION ne_all_bool_and CHECK (FALSE <> ALL (SELECT BOOL_AND(p) FROM t));
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (operations = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('eq_all_bool_and', 'INSERT(t) UPDATE(t.p)'),
+       ('eq_any_bool_and', 'INSERT(t) UPDATE(t.p)'),
+       ('eq_bool_and',     'INSERT(t) UPDATE(t.p)'),
+       ('ne_all_bool_and', 'INSERT(t) UPDATE(t.p)'),
+       ('ne_any_bool_and', 'INSERT(t) UPDATE(t.p)'),
+       ('ne_bool_and',     'INSERT(t) UPDATE(t.p)'))
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+ assertion_name  |        actual         |       expected        | correct 
+-----------------+-----------------------+-----------------------+---------
+ eq_all_bool_and | INSERT(t) UPDATE(t.p) | INSERT(t) UPDATE(t.p) | YES
+ eq_any_bool_and | INSERT(t) UPDATE(t.p) | INSERT(t) UPDATE(t.p) | YES
+ eq_bool_and     | INSERT(t) UPDATE(t.p) | INSERT(t) UPDATE(t.p) | YES
+ ne_all_bool_and | INSERT(t) UPDATE(t.p) | INSERT(t) UPDATE(t.p) | YES
+ ne_any_bool_and | INSERT(t) UPDATE(t.p) | INSERT(t) UPDATE(t.p) | YES
+ ne_bool_and     | INSERT(t) UPDATE(t.p) | INSERT(t) UPDATE(t.p) | YES
+(6 rows)
+
+ROLLBACK;
+BEGIN TRANSACTION;
+--
+-- Expressions involving BOOL_OR aggregations
+--
+CREATE TABLE t (n INTEGER NOT NULL PRIMARY KEY, p BOOLEAN NOT NULL);
+INSERT INTO t (n, p) VALUES (0, TRUE);
+CREATE ASSERTION eq_bool_or     CHECK (TRUE  =      (SELECT BOOL_OR(p) FROM t));
+CREATE ASSERTION eq_any_bool_or CHECK (TRUE  =  ANY (SELECT BOOL_OR(p) FROM t));
+CREATE ASSERTION eq_all_bool_or CHECK (TRUE  =  ALL (SELECT BOOL_OR(p) FROM t));
+CREATE ASSERTION ne_bool_or     CHECK (FALSE <>     (SELECT BOOL_OR(p) FROM t));
+CREATE ASSERTION ne_any_bool_or CHECK (FALSE <> ANY (SELECT BOOL_OR(p) FROM t));
+CREATE ASSERTION ne_all_bool_or CHECK (FALSE <> ALL (SELECT BOOL_OR(p) FROM t));
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (operations = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('eq_all_bool_or', 'DELETE(t) UPDATE(t.p)'),
+       ('eq_any_bool_or', 'DELETE(t) UPDATE(t.p)'),
+       ('eq_bool_or',     'DELETE(t) UPDATE(t.p)'),
+       ('ne_all_bool_or', 'DELETE(t) UPDATE(t.p)'),
+       ('ne_any_bool_or', 'DELETE(t) UPDATE(t.p)'),
+       ('ne_bool_or',     'DELETE(t) UPDATE(t.p)'))
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+ assertion_name |        actual         |       expected        | correct 
+----------------+-----------------------+-----------------------+---------
+ eq_all_bool_or | DELETE(t) UPDATE(t.p) | DELETE(t) UPDATE(t.p) | YES
+ eq_any_bool_or | DELETE(t) UPDATE(t.p) | DELETE(t) UPDATE(t.p) | YES
+ eq_bool_or     | DELETE(t) UPDATE(t.p) | DELETE(t) UPDATE(t.p) | YES
+ ne_all_bool_or | DELETE(t) UPDATE(t.p) | DELETE(t) UPDATE(t.p) | YES
+ ne_any_bool_or | DELETE(t) UPDATE(t.p) | DELETE(t) UPDATE(t.p) | YES
+ ne_bool_or     | DELETE(t) UPDATE(t.p) | DELETE(t) UPDATE(t.p) | YES
+(6 rows)
+
+ROLLBACK;
+BEGIN TRANSACTION;
+--
+-- Expressions involving window functions
+--
+CREATE TABLE t (n INTEGER NOT NULL PRIMARY KEY, p BOOLEAN NOT NULL);
+-- Regular window function
+CREATE ASSERTION alternate_p CHECK (
+  NOT EXISTS (
+    SELECT FROM (
+      SELECT LAG(p, 1, NOT p) OVER n <> p
+         AND LEAD(p, 1, NOT p) OVER n <> p
+        FROM t
+      WINDOW n AS (ORDER BY n)
+    ) AS v(q)
+    WHERE NOT q
+  )
+);
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (operations = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('alternate_p', 'DELETE(t) INSERT(t) UPDATE(t.n, t.p)'))
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+ assertion_name |                actual                |               expected               | correct 
+----------------+--------------------------------------+--------------------------------------+---------
+ alternate_p    | DELETE(t) INSERT(t) UPDATE(t.n, t.p) | DELETE(t) INSERT(t) UPDATE(t.n, t.p) | YES
+(1 row)
+
+INSERT INTO t (n, p) VALUES (10, TRUE);
+INSERT INTO t (n, p) VALUES (20, FALSE);
+INSERT INTO t (n, p) VALUES (30, TRUE);
+SAVEPOINT pre_insert;
+INSERT INTO t (n, p) VALUES (40, TRUE);
+ERROR:  assertion "alternate_p" violated
+ROLLBACK TO SAVEPOINT pre_insert;
+SAVEPOINT pre_delete;
+DELETE FROM t WHERE n = 20;
+ERROR:  assertion "alternate_p" violated
+ROLLBACK TO SAVEPOINT pre_delete;
+SAVEPOINT pre_update;
+UPDATE t SET p = NOT p WHERE n = 20;
+ERROR:  assertion "alternate_p" violated
+ROLLBACK TO SAVEPOINT pre_update;
+DROP ASSERTION alternate_p;
+-- Aggregate function over a window
+CREATE TABLE s (n INTEGER NOT NULL PRIMARY KEY, m INTEGER NOT NULL);
+INSERT INTO s (n, m)
+SELECT n, (2 * n)
+  FROM GENERATE_SERIES(0, 9) AS ns(n);
+CREATE ASSERTION max_over_window CHECK ((10, 20) > ALL (SELECT n, MAX(m) OVER (ORDER BY n) FROM s));
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (operations = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('max_over_window', 'INSERT(s) UPDATE(s.m, s.n)'))
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+ assertion_name  |           actual           |          expected          | correct 
+-----------------+----------------------------+----------------------------+---------
+ max_over_window | INSERT(s) UPDATE(s.m, s.n) | INSERT(s) UPDATE(s.m, s.n) | YES
+(1 row)
+
+ROLLBACK;
+BEGIN TRANSACTION;
+--
+-- Expressions containing a comparison should have the same invalidating
+-- operations as an expression containing the inverse comparison operation
+-- and where the operands have been switched
+--
+CREATE TABLE r (n INTEGER NOT NULL PRIMARY KEY);
+INSERT INTO r (n) VALUES (9);
+-- ">" and "<" are opposites
+CREATE ASSERTION a CHECK (10 > (SELECT MIN(n) FROM r));
+CREATE ASSERTION b CHECK ((SELECT MIN(n) FROM r) < 10);
+-- "<" and ">" are opposites
+CREATE ASSERTION c CHECK (8 < (SELECT MIN(n) FROM r));
+CREATE ASSERTION d CHECK ((SELECT MIN(n) FROM r) > 8);
+-- ">=" and "<=" are opposites
+CREATE ASSERTION e CHECK (9 >= (SELECT MIN(n) FROM r));
+CREATE ASSERTION f CHECK ((SELECT MIN(n) FROM r) <= 9);
+-- "<=" and ">=" are opposites
+CREATE ASSERTION g CHECK (9 <= (SELECT MIN(n) FROM r));
+CREATE ASSERTION h CHECK ((SELECT MIN(n) FROM r) >= 9);
+SELECT (test).predicate, (test).correct
+  FROM (
+VALUES ('a', 'b'),
+       ('c', 'd'),
+       ('e', 'f'),
+       ('g', 'h'))
+    AS v(a, b)
+ CROSS JOIN LATERAL (SELECT have_identical_invalidating_operations(v.a, v.b))
+    AS w(test)
+ ORDER BY 1, 2;
+                              predicate                               | correct 
+----------------------------------------------------------------------+---------
+ assertion "a" has identical invalidating operations to assertion "b" | YES
+ assertion "c" has identical invalidating operations to assertion "d" | YES
+ assertion "e" has identical invalidating operations to assertion "f" | YES
+ assertion "g" has identical invalidating operations to assertion "h" | YES
+(4 rows)
+
+ROLLBACK;
+BEGIN TRANSACTION;
+--
+-- The INFORMATION_SCHEMA should contain the correct information
+--
+CREATE TABLE t (n INTEGER NOT NULL PRIMARY KEY, p BOOLEAN NOT NULL);
+CREATE ASSERTION a CHECK (NOT EXISTS (SELECT FROM t WHERE p));
+SELECT predicate,
+       CASE WHEN
+         truth THEN 'YES'
+               ELSE 'NO'
+       END AS correct
+  FROM ( 
+VALUES ('INFORMATION_SCHEMA.CONSTRAINT_TABLE_USAGE contains only the correct tables',
+        EXISTS (
+          SELECT FROM information_schema.constraint_table_usage
+           WHERE (constraint_name, table_name) = ('a', 't'))
+        AND NOT EXISTS (
+          SELECT FROM information_schema.constraint_table_usage
+           WHERE constraint_name = 'a'
+             AND table_name <> 't')),
+       ('INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE contains only the correct columns (t.p)',
+        EXISTS (
+          SELECT FROM information_schema.constraint_column_usage
+           WHERE (constraint_name, table_name, column_name) = ('a', 't', 'p'))
+        AND NOT EXISTS (
+          SELECT FROM information_schema.constraint_column_usage
+           WHERE constraint_name ='a'
+             AND (table_name, column_name) <> ('t', 'p'))),
+       ('INFORMATION_SCHEMA.ASSERTIONS contains only the correct assertions',
+        EXISTS (
+          SELECT FROM information_schema.assertions
+           WHERE constraint_name = 'a')
+        AND NOT EXISTS (
+          SELECT FROM information_schema.assertions
+           WHERE constraint_name <> 'a')))
+    AS v(predicate, truth);
+                                     predicate                                      | correct 
+------------------------------------------------------------------------------------+---------
+ INFORMATION_SCHEMA.CONSTRAINT_TABLE_USAGE contains only the correct tables         | YES
+ INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE contains only the correct columns (t.p) | YES
+ INFORMATION_SCHEMA.ASSERTIONS contains only the correct assertions                 | YES
+(3 rows)
+
+ROLLBACK;
+BEGIN TRANSACTION;
+-- TODO This needs rethinking
+/*
+CREATE TABLE s (n INTEGER NOT NULL PRIMARY KEY, m INTEGER NOT NULL);
+CREATE TABLE t (n INTEGER NOT NULL PRIMARY KEY, p BOOLEAN NOT NULL);
+CREATE VIEW v AS SELECT * FROM s INNER JOIN t USING (n);
+
+CREATE ASSERTION a CHECK (NOT EXISTS (SELECT FROM v));
+
+-- we should depend on the view not the tables
+SELECT OK(depends_on('a', 'v'),
+          'Assertion depends upon the view v it references');
+SELECT OK(NOT depends_on('a', 's'),
+          'Assertion does not depend upon the table s referenced in the view v');
+SELECT OK(NOT depends_on('a', 't'),
+          'Assertion does not depend upon the table t referenced in the view v');
+
+-- we should trigger on the tables and not the view
+SELECT OK(EXISTS(SELECT FROM assertion_check_operation
+                  WHERE (assertion_name, relation_name) = ('a', 's')),
+          'Assertion is checked on modifications to table s');
+SELECT OK(EXISTS(SELECT FROM assertion_check_operation
+                  WHERE (assertion_name, relation_name) = ('a', 't')),
+          'Assertion is checked on modifications to table t');
+SELECT OK(NOT EXISTS(SELECT FROM assertion_check_operation
+                      WHERE (assertion_name, relation_name) = ('a', 'v')),
+          'Assertion is not checked on modifications to view v');
+*/
+ROLLBACK;
+-- TODO test commonalities between count, min, max, etc, for optimisations
+-- TODO ensure window function aggregates are treated the same as regular aggregates
+-- TODO ensure that conflicting aggregate functions are not incorrectly optimised
+-- TODO ensure that recorded dependencies are correct (traversal into functions and views)
+BEGIN TRANSACTION;
+DROP FUNCTION have_identical_invalidating_operations;
+DROP VIEW invalidating_summary;
+DROP VIEW invalidating_by_update_of_column;
+DROP VIEW invalidating_operation;
+COMMIT;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 16f979c8d9..084e76c1d0 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -60,7 +60,7 @@ test: create_index create_view index_including
 # ----------
 # Another group of parallel tests
 # ----------
-test: create_aggregate create_function_3 create_cast constraints triggers inherit create_table_like typed_table vacuum drop_if_exists updatable_views rolenames roleattributes create_am hash_func
+test: create_aggregate create_function_3 create_cast constraints triggers inherit create_table_like typed_table vacuum drop_if_exists updatable_views rolenames roleattributes create_am assertions hash_func
 
 # ----------
 # sanity_check does a vacuum, affecting the sort order of SELECT *
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 42632be675..f413969b6d 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -58,6 +58,7 @@ test: create_function_1
 test: create_type
 test: create_table
 test: create_function_2
+test: assertions
 test: copy
 test: copyselect
 test: copydml
diff --git a/src/test/regress/sql/assertions.sql b/src/test/regress/sql/assertions.sql
new file mode 100644
index 0000000000..f270698da1
--- /dev/null
+++ b/src/test/regress/sql/assertions.sql
@@ -0,0 +1,775 @@
+
+BEGIN TRANSACTION;
+
+--
+-- Create some helper views and functions to allow us to more easily
+-- assert which operations will cause an assertion to be checked.
+--
+
+CREATE OR REPLACE VIEW invalidating_operation
+  (assertion_name,
+   relation_name,
+   operation)
+AS
+SELECT c.conname,
+       cl.relname,
+       v.operation
+  FROM pg_trigger t
+  JOIN pg_constraint c ON (t.tgconstraint = c.oid)
+  JOIN pg_proc p ON (t.tgfoid = p.oid)
+  JOIN pg_class cl ON (t.tgrelid = cl.oid)
+  JOIN (VALUES ('INSERT', 1<<2),
+               ('DELETE', 1<<3|1<<5),
+               ('UPDATE', 1<<4))
+    AS v(operation, mask) ON ((v.mask & tgtype) > 0)
+ WHERE t.tgisinternal
+   AND p.proname = 'assertion_check';
+
+CREATE OR REPLACE VIEW invalidating_by_update_of_column
+  (assertion_name,
+   relation_name,
+   column_name)
+AS
+SELECT c.conname,
+       cl.relname,
+       a.attname
+  FROM pg_trigger t INNER JOIN pg_constraint c ON (t.tgconstraint = c.oid)
+                    INNER JOIN pg_proc p ON (t.tgfoid = p.oid)
+                    INNER JOIN pg_class cl ON (t.tgrelid = cl.oid)
+                    CROSS JOIN LATERAL UNNEST(t.tgattr) AS co(n)
+                    INNER JOIN pg_attribute a ON (a.attrelid = cl.oid AND attnum = co.n)
+ WHERE t.tgisinternal
+   AND p.proname = 'assertion_check'
+   AND (tgtype & (1 << 4)) > 0;
+
+CREATE OR REPLACE VIEW invalidating_summary
+  (assertion_name,
+   operations)
+AS
+SELECT assertion_name,
+       string_agg(operation, ' ' ORDER BY operation)
+  FROM (SELECT assertion_name,
+               operation ||'('|| string_agg(relation_name, ', ' ORDER BY relation_name) ||')'
+          FROM invalidating_operation
+         WHERE operation <> 'UPDATE'
+         GROUP BY assertion_name,
+                  operation
+         UNION
+        SELECT assertion_name,
+               operation ||'('|| string_agg(relation_name ||'.'|| column_name, ', ' ORDER BY relation_name, column_name) ||')'
+          FROM invalidating_by_update_of_column JOIN invalidating_operation USING (assertion_name, relation_name)
+         WHERE operation = 'UPDATE'
+         GROUP BY assertion_name,
+                  operation)
+    AS v(assertion_name, operation)
+ GROUP BY assertion_name;
+
+CREATE OR REPLACE FUNCTION have_identical_invalidating_operations
+  (a text, b text)
+RETURNS TABLE (predicate text, correct text)
+AS $$
+  SELECT 'assertion "'|| a ||'" has identical invalidating operations to assertion "'|| b ||'"',
+         CASE WHEN
+           NOT EXISTS (SELECT relation_name, operation
+                         FROM invalidating_operation
+                        WHERE assertion_name = a
+                       EXCEPT 
+                       SELECT relation_name, operation
+                         FROM invalidating_operation
+                        WHERE assertion_name = b) AND
+           NOT EXISTS (SELECT relation_name, column_name
+                         FROM invalidating_by_update_of_column
+                        WHERE assertion_name = a
+                       EXCEPT
+                       SELECT relation_name, column_name
+                         FROM invalidating_by_update_of_column
+                        WHERE assertion_name = b) THEN 'YES'
+                                                  ELSE 'NO' END
+$$ LANGUAGE SQL;
+
+COMMIT;
+
+
+BEGIN TRANSACTION;
+
+--
+-- Perform some basic sanity checking on creating assertions
+--
+
+CREATE TABLE test1 (a int, b text);
+
+CREATE ASSERTION foo CHECK (1 < 2);
+CREATE ASSERTION a2 CHECK ((SELECT count(*) FROM test1) < 5);
+
+DELETE FROM test1;
+INSERT INTO test1 VALUES (1, 'one');
+INSERT INTO test1 VALUES (2, 'two');
+INSERT INTO test1 VALUES (3, 'three');
+INSERT INTO test1 VALUES (4, 'four');
+
+SAVEPOINT pre_insert_too_many;
+INSERT INTO test1 VALUES (5, 'five');
+ROLLBACK TO SAVEPOINT pre_insert_too_many;
+
+SELECT constraint_schema,
+       constraint_name
+  FROM information_schema.assertions
+ ORDER BY 1, 2;
+
+\dQ
+
+ALTER ASSERTION a2 RENAME TO a3;
+
+SAVEPOINT pre_rename_foo;
+ALTER ASSERTION foo RENAME TO a3; -- fails
+ROLLBACK TO SAVEPOINT pre_rename_foo;
+
+DROP ASSERTION foo;
+
+SAVEPOINT pre_drop_test1;
+DROP TABLE test1; -- fails
+ROLLBACK TO SAVEPOINT pre_drop_test1;
+
+DROP TABLE test1 CASCADE;
+
+ROLLBACK;
+
+
+
+BEGIN TRANSACTION;
+
+--
+-- Expressions involving various operators, functions and casts
+-- 
+
+CREATE TABLE t (n INTEGER NOT NULL PRIMARY KEY, p BOOLEAN NOT NULL);
+
+INSERT INTO t (n, p) VALUES (20, TRUE);
+
+CREATE ASSERTION is_distinct_from CHECK (10 IS DISTINCT FROM (SELECT MIN(n) FROM t));
+CREATE ASSERTION is_not_distinct_from CHECK (20 IS NOT DISTINCT FROM (SELECT MIN(n) FROM t));
+CREATE ASSERTION null_if CHECK (NULLIF((SELECT BOOL_AND(p) FROM t), FALSE));
+CREATE ASSERTION coerce_to_boolean_from_min CHECK (CAST((SELECT MIN(n) FROM t) AS BOOLEAN));
+CREATE ASSERTION coerce_to_boolean_from_bool_and CHECK (CAST((SELECT BOOL_AND(p) FROM t) AS BOOLEAN));
+CREATE ASSERTION coerce_to_boolean_from_boolean CHECK (CAST((SELECT TRUE FROM t) AS BOOLEAN));
+
+ROLLBACK;
+
+
+BEGIN TRANSACTION;
+
+--
+-- Expressions involving function calls
+--
+
+CREATE TABLE t (n INTEGER NOT NULL PRIMARY KEY, m INTEGER NOT NULL, d DATE NOT NULL);
+
+CREATE FUNCTION f(n INTEGER) RETURNS INTEGER
+AS $$
+  SELECT n * 2
+$$ LANGUAGE SQL;
+
+CREATE FUNCTION r(n INTEGER) RETURNS TABLE (m INTEGER )
+AS $$
+  SELECT m
+    FROM t
+   WHERE n > r.n
+$$ LANGUAGE SQL;
+
+CREATE ASSERTION age_of_d_in_t CHECK (
+  NOT EXISTS (
+    SELECT FROM t
+     WHERE AGE(DATE '01-01-2018') > INTERVAL '10 days' -- AGE(DATE) is built-in SQL
+  )
+);
+CREATE ASSERTION use_f CHECK (f(0) = 0);
+CREATE ASSERTION use_f_in_predicate CHECK (NOT EXISTS (SELECT FROM t WHERE f(n) = 20));
+CREATE ASSERTION use_f_in_target CHECK (NOT EXISTS (SELECT f(n) FROM t));
+CREATE ASSERTION use_r_in_from CHECK (NOT EXISTS (SELECT FROM r(100)));
+CREATE ASSERTION use_r_in_target CHECK (NOT EXISTS (SELECT r(100)));
+
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (COALESCE(operations, 'NONE') = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('age_of_d_in_t',      'INSERT(t)'),
+       ('use_f',              'NONE'),
+       ('use_f_in_predicate', 'INSERT(t) UPDATE(t.n)'),
+       ('use_f_in_target',    'INSERT(t)'),
+       ('use_r_in_from',      'INSERT(t) UPDATE(t.m, t.n)'), -- TODO should _from and _target be the same?
+       ('use_r_in_target',    'INSERT(t) UPDATE(t.n)'))
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+
+CREATE FUNCTION g(n INTEGER) RETURNS INTEGER
+AS $$
+BEGIN
+  RETURN n * 2;
+END;
+$$ LANGUAGE PLPGSQL;
+
+-- Use of functions other than internal or those implemented in SQL are illegal
+CREATE ASSERTION use_g_in_from CHECK (NOT EXISTS (SELECT FROM g(100))); -- fails
+
+ROLLBACK;
+
+
+BEGIN TRANSACTION;
+
+--
+-- Expressions involving EXISTS and NOT EXISTS
+--
+
+CREATE TABLE r (n INTEGER NOT NULL PRIMARY KEY);
+CREATE TABLE s (n INTEGER NOT NULL PRIMARY KEY);
+CREATE TABLE t (n INTEGER NOT NULL PRIMARY KEY, m INTEGER NOT NULL);
+CREATE TABLE t1 (n INTEGER NOT NULL PRIMARY KEY, m INTEGER NOT NULL);
+CREATE TABLE t2 (n INTEGER NOT NULL PRIMARY KEY, m INTEGER NOT NULL);
+
+INSERT INTO r (n) VALUES (1);
+INSERT INTO t (n, m) VALUES (0, 0);
+INSERT INTO t1 (n, m) VALUES (0, 0);
+INSERT INTO t2 (n, m) VALUES (0, 0);
+
+CREATE ASSERTION exists_no_predicate CHECK (
+  EXISTS (SELECT FROM r)
+);
+
+CREATE ASSERTION exists_with_predicate CHECK (
+  EXISTS (SELECT FROM r WHERE n > 0)
+);
+
+CREATE ASSERTION not_exists_no_predicate CHECK (
+  NOT EXISTS (SELECT FROM s)
+);
+
+CREATE ASSERTION not_exists_with_predicate CHECK (
+  NOT EXISTS (SELECT FROM r WHERE n < 1)
+);
+
+CREATE ASSERTION direct_subject_of_an_exists CHECK (
+  EXISTS (SELECT n FROM t WHERE m = 0)
+);
+
+-- TODO These can be optimised, at least if the set operation is UNION.
+CREATE ASSERTION except_subject_of_an_exists CHECK (
+  EXISTS (SELECT n FROM t1 WHERE m = 0 EXCEPT SELECT n FROM t2 WHERE m = 1)
+);
+
+CREATE ASSERTION intersect_subject_of_an_exists CHECK (
+  EXISTS (SELECT n FROM t1 WHERE m = 0 INTERSECT SELECT n FROM t2 WHERE m = 0)
+);
+
+CREATE ASSERTION union_subject_of_an_exists CHECK (
+  EXISTS (SELECT n FROM t1 WHERE m = 0 UNION SELECT n FROM t2 WHERE m = 0)
+);
+
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (operations = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('exists_no_predicate',            'DELETE(r)'),
+       ('exists_with_predicate',          'DELETE(r) UPDATE(r.n)'),
+       ('not_exists_no_predicate',        'INSERT(s)'),
+       ('not_exists_with_predicate',      'INSERT(r) UPDATE(r.n)'),
+       ('direct_subject_of_an_exists',    'DELETE(t) UPDATE(t.m)'),
+       ('except_subject_of_an_exists',    'DELETE(t1) INSERT(t2) UPDATE(t1.m, t1.n, t2.m, t2.n)'),
+       ('intersect_subject_of_an_exists', 'DELETE(t1, t2) UPDATE(t1.m, t1.n, t2.m, t2.n)'),
+       ('union_subject_of_an_exists',     'DELETE(t1, t2) UPDATE(t1.m, t1.n, t2.m, t2.n)'))
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+
+ROLLBACK;
+
+
+BEGIN TRANSACTION;
+
+--
+-- Expressions involving NOT
+--
+
+CREATE TABLE r (p BOOLEAN NOT NULL PRIMARY KEY);
+
+INSERT INTO r (p) VALUES (TRUE);
+
+CREATE ASSERTION a CHECK (EXISTS (SELECT FROM r WHERE p));
+CREATE ASSERTION b CHECK (NOT EXISTS (SELECT FROM r WHERE NOT p));
+
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (operations = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('a', 'DELETE(r) UPDATE(r.p)'),
+       ('b', 'INSERT(r) UPDATE(r.p)')) -- NOT(x) and x have opposite invalidating operations
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+
+ROLLBACK;
+
+
+BEGIN TRANSACTION;
+
+--
+-- Expressions involving set operations INTERSECT, UNION, and EXCEPT
+--
+
+CREATE TABLE r (n INTEGER NOT NULL PRIMARY KEY);
+CREATE TABLE s (n INTEGER NOT NULL PRIMARY KEY);
+CREATE TABLE t (n INTEGER NOT NULL PRIMARY KEY);
+
+INSERT INTO t (n) VALUES (0);
+
+CREATE ASSERTION except_operands CHECK (NOT EXISTS (SELECT FROM r EXCEPT SELECT FROM s));
+CREATE ASSERTION intersect_operands CHECK (NOT EXISTS (SELECT FROM r INTERSECT SELECT FROM s));
+CREATE ASSERTION union_operands CHECK (EXISTS (SELECT FROM s UNION SELECT FROM t));
+
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (operations = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('except_operands',    'DELETE(s) INSERT(r)'),
+       ('intersect_operands', 'INSERT(r, s)'),
+       ('union_operands',     'DELETE(s, t)'))
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+
+ROLLBACK;
+
+
+BEGIN TRANSACTION;
+
+--
+-- Expressions involving COUNT aggregations
+--
+
+CREATE TABLE s (n INTEGER NOT NULL PRIMARY KEY, m INTEGER NOT NULL);
+
+INSERT INTO s VALUES (1, 2);
+
+CREATE ASSERTION ge_all_count CHECK (1 >= ALL (SELECT COUNT(*) FROM s));
+CREATE ASSERTION ge_any_count CHECK (1 >= ANY (SELECT COUNT(*) FROM s));
+CREATE ASSERTION ge_count     CHECK (1 >=     (SELECT COUNT(*) FROM s));
+CREATE ASSERTION gt_all_count CHECK (2 >  ALL (SELECT COUNT(*) FROM s));
+CREATE ASSERTION gt_any_count CHECK (2 >  ANY (SELECT COUNT(*) FROM s));
+CREATE ASSERTION gt_count     CHECK (2 >      (SELECT COUNT(*) FROM s));
+CREATE ASSERTION le_all_count CHECK (1 <= ALL (SELECT COUNT(*) FROM s));
+CREATE ASSERTION le_any_count CHECK (0 <= ANY (SELECT COUNT(*) FROM s));
+CREATE ASSERTION le_count     CHECK (0 <=     (SELECT COUNT(*) FROM s));
+CREATE ASSERTION lt_all_count CHECK (0 <  ALL (SELECT COUNT(*) FROM s));
+CREATE ASSERTION lt_any_count CHECK (0 <  ANY (SELECT COUNT(*) FROM s));
+CREATE ASSERTION lt_count     CHECK (0 <      (SELECT COUNT(*) FROM s));
+
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (operations = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('ge_all_count', 'INSERT(s)'),
+       ('ge_any_count', 'INSERT(s)'),
+       ('ge_count',     'INSERT(s)'),
+       ('gt_all_count', 'INSERT(s)'),
+       ('gt_any_count', 'INSERT(s)'),
+       ('gt_count',     'INSERT(s)'),
+       ('le_all_count', 'DELETE(s)'),
+       ('le_any_count', 'DELETE(s)'),
+       ('le_count',     'DELETE(s)'),
+       ('lt_all_count', 'DELETE(s)'),
+       ('lt_any_count', 'DELETE(s)'),
+       ('lt_count',     'DELETE(s)'))
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+
+ROLLBACK;
+
+
+BEGIN TRANSACTION;
+
+--
+-- Expressions involving MIN aggregations
+--
+
+CREATE TABLE s (n INTEGER NOT NULL PRIMARY KEY, m INTEGER NOT NULL);
+
+INSERT INTO s VALUES (1, 2);
+
+CREATE ASSERTION ge_any_min CHECK (1 >= ANY (SELECT MIN(n) FROM s));
+CREATE ASSERTION ge_all_min CHECK (1 >= ALL (SELECT MIN(n) FROM s));
+CREATE ASSERTION ge_min     CHECK (1 >=     (SELECT MIN(n) FROM s));
+CREATE ASSERTION gt_any_min CHECK (2 >  ANY (SELECT MIN(n) FROM s));
+CREATE ASSERTION gt_all_min CHECK (2 >  ALL (SELECT MIN(n) FROM s));
+CREATE ASSERTION gt_min     CHECK (2 >      (SELECT MIN(n) FROM s));
+CREATE ASSERTION le_any_min CHECK (0 <= ANY (SELECT MIN(n) FROM s));
+CREATE ASSERTION le_all_min CHECK (0 <= ALL (SELECT MIN(n) FROM s));
+CREATE ASSERTION le_min     CHECK (0 <=     (SELECT MIN(n) FROM s));
+CREATE ASSERTION lt_any_min CHECK (0 <  ANY (SELECT MIN(n) FROM s));
+CREATE ASSERTION lt_all_min CHECK (0 <  ALL (SELECT MIN(n) FROM s));
+CREATE ASSERTION lt_min     CHECK (0 <      (SELECT MIN(n) FROM s));
+
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (operations = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('ge_all_min', 'DELETE(s) UPDATE(s.n)'),
+       ('ge_any_min', 'DELETE(s) UPDATE(s.n)'),
+       ('ge_min',     'DELETE(s) UPDATE(s.n)'),
+       ('gt_all_min', 'DELETE(s) UPDATE(s.n)'),
+       ('gt_any_min', 'DELETE(s) UPDATE(s.n)'),
+       ('gt_min',     'DELETE(s) UPDATE(s.n)'),
+       ('le_all_min', 'INSERT(s) UPDATE(s.n)'),
+       ('le_any_min', 'INSERT(s) UPDATE(s.n)'),
+       ('le_min',     'INSERT(s) UPDATE(s.n)'),
+       ('lt_all_min', 'INSERT(s) UPDATE(s.n)'),
+       ('lt_any_min', 'INSERT(s) UPDATE(s.n)'),
+       ('lt_min',     'INSERT(s) UPDATE(s.n)'))
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+
+ROLLBACK;
+
+
+BEGIN TRANSACTION;
+
+--
+-- Expressions involving MAX aggregations
+--
+
+CREATE TABLE s (n INTEGER NOT NULL PRIMARY KEY, m INTEGER NOT NULL);
+
+INSERT INTO s VALUES (1, 2);
+
+CREATE ASSERTION ge_any_max CHECK (1 >= ANY (SELECT MAX(n) FROM s));
+CREATE ASSERTION ge_all_max CHECK (1 >= ALL (SELECT MAX(n) FROM s));
+CREATE ASSERTION ge_max     CHECK (1 >=     (SELECT MAX(n) FROM s));
+CREATE ASSERTION gt_all_max CHECK (2 >  ALL (SELECT MAX(n) FROM s));
+CREATE ASSERTION gt_any_max CHECK (2 >  ANY (SELECT MAX(n) FROM s));
+CREATE ASSERTION gt_max     CHECK (2 >      (SELECT MAX(n) FROM s));
+CREATE ASSERTION le_all_max CHECK (0 <= ALL (SELECT MAX(n) FROM s));
+CREATE ASSERTION le_any_max CHECK (0 <= ANY (SELECT MAX(n) FROM s));
+CREATE ASSERTION le_max     CHECK (0 <=     (SELECT MAX(n) FROM s));
+CREATE ASSERTION lt_all_max CHECK (0 <  ALL (SELECT MAX(n) FROM s));
+CREATE ASSERTION lt_any_max CHECK (0 <  ANY (SELECT MAX(n) FROM s));
+CREATE ASSERTION lt_max     CHECK (0 <      (SELECT MAX(n) FROM s));
+
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (operations = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('ge_all_max', 'INSERT(s) UPDATE(s.n)'),
+       ('ge_any_max', 'INSERT(s) UPDATE(s.n)'),
+       ('ge_max',     'INSERT(s) UPDATE(s.n)'),
+       ('gt_all_max', 'INSERT(s) UPDATE(s.n)'),
+       ('gt_any_max', 'INSERT(s) UPDATE(s.n)'),
+       ('gt_max',     'INSERT(s) UPDATE(s.n)'),
+       ('le_all_max', 'DELETE(s) UPDATE(s.n)'),
+       ('le_any_max', 'DELETE(s) UPDATE(s.n)'),
+       ('le_max',     'DELETE(s) UPDATE(s.n)'),
+       ('lt_all_max', 'DELETE(s) UPDATE(s.n)'),
+       ('lt_any_max', 'DELETE(s) UPDATE(s.n)'),
+       ('lt_max',     'DELETE(s) UPDATE(s.n)'))
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+
+ROLLBACK;
+
+
+BEGIN TRANSACTION;
+
+--
+-- Expressions involving BOOL_AND aggregations
+--
+
+CREATE TABLE t (n INTEGER NOT NULL PRIMARY KEY, p BOOLEAN NOT NULL);
+INSERT INTO t (n, p) VALUES (0, TRUE);
+
+CREATE ASSERTION eq_bool_and     CHECK (TRUE  =      (SELECT BOOL_AND(p) FROM t));
+CREATE ASSERTION eq_any_bool_and CHECK (TRUE  =  ANY (SELECT BOOL_AND(p) FROM t));
+CREATE ASSERTION eq_all_bool_and CHECK (TRUE  =  ALL (SELECT BOOL_AND(p) FROM t));
+CREATE ASSERTION ne_bool_and     CHECK (FALSE <>     (SELECT BOOL_AND(p) FROM t));
+CREATE ASSERTION ne_any_bool_and CHECK (FALSE <> ANY (SELECT BOOL_AND(p) FROM t));
+CREATE ASSERTION ne_all_bool_and CHECK (FALSE <> ALL (SELECT BOOL_AND(p) FROM t));
+
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (operations = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('eq_all_bool_and', 'INSERT(t) UPDATE(t.p)'),
+       ('eq_any_bool_and', 'INSERT(t) UPDATE(t.p)'),
+       ('eq_bool_and',     'INSERT(t) UPDATE(t.p)'),
+       ('ne_all_bool_and', 'INSERT(t) UPDATE(t.p)'),
+       ('ne_any_bool_and', 'INSERT(t) UPDATE(t.p)'),
+       ('ne_bool_and',     'INSERT(t) UPDATE(t.p)'))
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+
+ROLLBACK;
+
+
+BEGIN TRANSACTION;
+
+--
+-- Expressions involving BOOL_OR aggregations
+--
+
+CREATE TABLE t (n INTEGER NOT NULL PRIMARY KEY, p BOOLEAN NOT NULL);
+INSERT INTO t (n, p) VALUES (0, TRUE);
+
+CREATE ASSERTION eq_bool_or     CHECK (TRUE  =      (SELECT BOOL_OR(p) FROM t));
+CREATE ASSERTION eq_any_bool_or CHECK (TRUE  =  ANY (SELECT BOOL_OR(p) FROM t));
+CREATE ASSERTION eq_all_bool_or CHECK (TRUE  =  ALL (SELECT BOOL_OR(p) FROM t));
+CREATE ASSERTION ne_bool_or     CHECK (FALSE <>     (SELECT BOOL_OR(p) FROM t));
+CREATE ASSERTION ne_any_bool_or CHECK (FALSE <> ANY (SELECT BOOL_OR(p) FROM t));
+CREATE ASSERTION ne_all_bool_or CHECK (FALSE <> ALL (SELECT BOOL_OR(p) FROM t));
+
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (operations = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('eq_all_bool_or', 'DELETE(t) UPDATE(t.p)'),
+       ('eq_any_bool_or', 'DELETE(t) UPDATE(t.p)'),
+       ('eq_bool_or',     'DELETE(t) UPDATE(t.p)'),
+       ('ne_all_bool_or', 'DELETE(t) UPDATE(t.p)'),
+       ('ne_any_bool_or', 'DELETE(t) UPDATE(t.p)'),
+       ('ne_bool_or',     'DELETE(t) UPDATE(t.p)'))
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+
+ROLLBACK;
+
+
+BEGIN TRANSACTION;
+
+--
+-- Expressions involving window functions
+--
+
+CREATE TABLE t (n INTEGER NOT NULL PRIMARY KEY, p BOOLEAN NOT NULL);
+
+-- Regular window function
+CREATE ASSERTION alternate_p CHECK (
+  NOT EXISTS (
+    SELECT FROM (
+      SELECT LAG(p, 1, NOT p) OVER n <> p
+         AND LEAD(p, 1, NOT p) OVER n <> p
+        FROM t
+      WINDOW n AS (ORDER BY n)
+    ) AS v(q)
+    WHERE NOT q
+  )
+);
+
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (operations = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('alternate_p', 'DELETE(t) INSERT(t) UPDATE(t.n, t.p)'))
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+
+INSERT INTO t (n, p) VALUES (10, TRUE);
+INSERT INTO t (n, p) VALUES (20, FALSE);
+INSERT INTO t (n, p) VALUES (30, TRUE);
+
+SAVEPOINT pre_insert;
+INSERT INTO t (n, p) VALUES (40, TRUE);
+ROLLBACK TO SAVEPOINT pre_insert;
+
+SAVEPOINT pre_delete;
+DELETE FROM t WHERE n = 20;
+ROLLBACK TO SAVEPOINT pre_delete;
+
+SAVEPOINT pre_update;
+UPDATE t SET p = NOT p WHERE n = 20;
+ROLLBACK TO SAVEPOINT pre_update;
+
+DROP ASSERTION alternate_p;
+
+-- Aggregate function over a window
+CREATE TABLE s (n INTEGER NOT NULL PRIMARY KEY, m INTEGER NOT NULL);
+
+INSERT INTO s (n, m)
+SELECT n, (2 * n)
+  FROM GENERATE_SERIES(0, 9) AS ns(n);
+
+CREATE ASSERTION max_over_window CHECK ((10, 20) > ALL (SELECT n, MAX(m) OVER (ORDER BY n) FROM s));
+
+SELECT assertion_name,
+       COALESCE(operations, 'NONE') AS actual,
+       expected,
+       CASE WHEN (operations = expected)
+         THEN 'YES' ELSE 'NO'
+       END AS correct
+  FROM invalidating_summary FULL OUTER JOIN (
+VALUES ('max_over_window', 'INSERT(s) UPDATE(s.m, s.n)'))
+    AS v(assertion_name, expected)
+ USING (assertion_name)
+ ORDER BY assertion_name, operations;
+
+ROLLBACK;
+
+
+BEGIN TRANSACTION;
+
+--
+-- Expressions containing a comparison should have the same invalidating
+-- operations as an expression containing the inverse comparison operation
+-- and where the operands have been switched
+--
+
+CREATE TABLE r (n INTEGER NOT NULL PRIMARY KEY);
+
+INSERT INTO r (n) VALUES (9);
+
+-- ">" and "<" are opposites
+CREATE ASSERTION a CHECK (10 > (SELECT MIN(n) FROM r));
+CREATE ASSERTION b CHECK ((SELECT MIN(n) FROM r) < 10);
+
+-- "<" and ">" are opposites
+CREATE ASSERTION c CHECK (8 < (SELECT MIN(n) FROM r));
+CREATE ASSERTION d CHECK ((SELECT MIN(n) FROM r) > 8);
+
+-- ">=" and "<=" are opposites
+CREATE ASSERTION e CHECK (9 >= (SELECT MIN(n) FROM r));
+CREATE ASSERTION f CHECK ((SELECT MIN(n) FROM r) <= 9);
+
+-- "<=" and ">=" are opposites
+CREATE ASSERTION g CHECK (9 <= (SELECT MIN(n) FROM r));
+CREATE ASSERTION h CHECK ((SELECT MIN(n) FROM r) >= 9);
+
+SELECT (test).predicate, (test).correct
+  FROM (
+VALUES ('a', 'b'),
+       ('c', 'd'),
+       ('e', 'f'),
+       ('g', 'h'))
+    AS v(a, b)
+ CROSS JOIN LATERAL (SELECT have_identical_invalidating_operations(v.a, v.b))
+    AS w(test)
+ ORDER BY 1, 2;
+
+ROLLBACK;
+
+
+BEGIN TRANSACTION;
+
+--
+-- The INFORMATION_SCHEMA should contain the correct information
+--
+
+CREATE TABLE t (n INTEGER NOT NULL PRIMARY KEY, p BOOLEAN NOT NULL);
+CREATE ASSERTION a CHECK (NOT EXISTS (SELECT FROM t WHERE p));
+
+SELECT predicate,
+       CASE WHEN
+         truth THEN 'YES'
+               ELSE 'NO'
+       END AS correct
+  FROM ( 
+VALUES ('INFORMATION_SCHEMA.CONSTRAINT_TABLE_USAGE contains only the correct tables',
+        EXISTS (
+          SELECT FROM information_schema.constraint_table_usage
+           WHERE (constraint_name, table_name) = ('a', 't'))
+        AND NOT EXISTS (
+          SELECT FROM information_schema.constraint_table_usage
+           WHERE constraint_name = 'a'
+             AND table_name <> 't')),
+       ('INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE contains only the correct columns (t.p)',
+        EXISTS (
+          SELECT FROM information_schema.constraint_column_usage
+           WHERE (constraint_name, table_name, column_name) = ('a', 't', 'p'))
+        AND NOT EXISTS (
+          SELECT FROM information_schema.constraint_column_usage
+           WHERE constraint_name ='a'
+             AND (table_name, column_name) <> ('t', 'p'))),
+       ('INFORMATION_SCHEMA.ASSERTIONS contains only the correct assertions',
+        EXISTS (
+          SELECT FROM information_schema.assertions
+           WHERE constraint_name = 'a')
+        AND NOT EXISTS (
+          SELECT FROM information_schema.assertions
+           WHERE constraint_name <> 'a')))
+    AS v(predicate, truth);
+
+ROLLBACK;
+
+BEGIN TRANSACTION;
+-- TODO This needs rethinking
+/*
+CREATE TABLE s (n INTEGER NOT NULL PRIMARY KEY, m INTEGER NOT NULL);
+CREATE TABLE t (n INTEGER NOT NULL PRIMARY KEY, p BOOLEAN NOT NULL);
+CREATE VIEW v AS SELECT * FROM s INNER JOIN t USING (n);
+
+CREATE ASSERTION a CHECK (NOT EXISTS (SELECT FROM v));
+
+-- we should depend on the view not the tables
+SELECT OK(depends_on('a', 'v'),
+          'Assertion depends upon the view v it references');
+SELECT OK(NOT depends_on('a', 's'),
+          'Assertion does not depend upon the table s referenced in the view v');
+SELECT OK(NOT depends_on('a', 't'),
+          'Assertion does not depend upon the table t referenced in the view v');
+
+-- we should trigger on the tables and not the view
+SELECT OK(EXISTS(SELECT FROM assertion_check_operation
+                  WHERE (assertion_name, relation_name) = ('a', 's')),
+          'Assertion is checked on modifications to table s');
+SELECT OK(EXISTS(SELECT FROM assertion_check_operation
+                  WHERE (assertion_name, relation_name) = ('a', 't')),
+          'Assertion is checked on modifications to table t');
+SELECT OK(NOT EXISTS(SELECT FROM assertion_check_operation
+                      WHERE (assertion_name, relation_name) = ('a', 'v')),
+          'Assertion is not checked on modifications to view v');
+*/
+ROLLBACK;
+
+-- TODO test commonalities between count, min, max, etc, for optimisations
+-- TODO ensure window function aggregates are treated the same as regular aggregates
+-- TODO ensure that conflicting aggregate functions are not incorrectly optimised
+-- TODO ensure that recorded dependencies are correct (traversal into functions and views)
+
+BEGIN TRANSACTION;
+
+DROP FUNCTION have_identical_invalidating_operations;
+DROP VIEW invalidating_summary;
+DROP VIEW invalidating_by_update_of_column;
+DROP VIEW invalidating_operation;
+
+COMMIT;
-- 
2.15.0

#22David Fetter
david@fetter.org
In reply to: Joe Wildish (#21)
Re: Implementing SQL ASSERTION

On Sun, Apr 29, 2018 at 07:18:00PM +0100, Joe Wildish wrote:

On 28 Mar 2018, at 16:13, David Fetter <david@fetter.org> wrote:

Sorry to bother you again, but this now doesn't compile atop master.

Attached is a rebased patch for the prototype.

Thanks!

This is great timing for the 12 cycle :)

Best,
David.
--
David Fetter <david(at)fetter(dot)org> http://fetter.org/
Phone: +1 415 235 3778

Remember to vote!
Consider donating to Postgres: http://www.postgresql.org/about/donate

#23Peter Eisentraut
peter.eisentraut@2ndquadrant.com
In reply to: Joe Wildish (#21)
Re: Implementing SQL ASSERTION

On 29/04/2018 20:18, Joe Wildish wrote:

On 28 Mar 2018, at 16:13, David Fetter <david@fetter.org> wrote:

Sorry to bother you again, but this now doesn't compile atop master.

Attached is a rebased patch for the prototype.

I took a look at this.

This has been lying around for a few months, so it will need to be
rebased again. I applied this patch on top of
68e7e973d22274a089ce95200b3782f514f6d2f8, which was the HEAD around the
time this patch was created, and it applies cleanly there.

Please check you patch for whitespace errors:

warning: squelched 13 whitespace errors
warning: 18 lines add whitespace errors.

Also, reduce the amount of useless whitespace changes in the patch.

There are some compiler warnings:

constraint.c: In function 'CreateAssertion':
constraint.c:1211:2: error: ISO C90 forbids mixed declarations and code
[-Werror=declaration-after-statement]

constraint.c: In function 'oppositeDmlOp':
constraint.c:458:1: error: control reaches end of non-void function
[-Werror=return-type]

The version check in psql's describeAssertions() needs to be updated.
Also, you should use formatPGVersionNumber() to cope with two-part and
one-part version numbers.

All this new code in constraint.c that checks the assertion expression
needs more comments and documentation.

Stuff like this isn't going to work:

static int
funcMaskForFuncOid(Oid funcOid)
{
char *name = get_func_name(funcOid);

if (name == NULL)
return OTHER_FUNC;
else if (strncmp(name, "min", strlen("min")) == 0)
return MIN_AGG_FUNC;
else if (strncmp(name, "max", strlen("max")) == 0)
return MAX_AGG_FUNC;

You can assume from the name of a function what it's going to do.
Solving this properly might be hard.

The regression test crashes for me around

frame #4: 0x000000010d3a4cdc postgres`castNodeImpl(type=T_SubLink,
ptr=0x00007ff27006d230) at nodes.h:582
frame #5: 0x000000010d3a61c6
postgres`visitSubLink(node=0x00007ff270034040, info=0x00007ffee2a23930)
at constraint.c:843

This ought to be reproducible for you if you build with assertions.

My feeling is that if we want to move forward on this topic, we need to
solve the concurrency question first. All these optimizations for when
we don't need to check the assertion are cool, but they are just
optimizations that we can apply later on, once we have solved the
critical problems.

--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#24Joe Wildish
joe-postgresql.org@elusive.cx
In reply to: Peter Eisentraut (#23)
Re: Implementing SQL ASSERTION

Hi Peter,

On 24 Sep 2018, at 15:06, Peter Eisentraut
<peter.eisentraut@2ndquadrant.com> wrote:

On 29/04/2018 20:18, Joe Wildish wrote:

Attached is a rebased patch for the prototype.

I took a look at this.

Thank you for reviewing.

This has been lying around for a few months, so it will need to be
rebased again.

8< - - - snipped for brevity - - - 8<

All this new code in constraint.c that checks the assertion expression
needs more comments and documentation.

All agreed. I’ll give the patch some TLC and get a new version that
addresses the above.

Stuff like this isn't going to work:

static int
funcMaskForFuncOid(Oid funcOid)
{
char *name = get_func_name(funcOid);

if (name == NULL)
return OTHER_FUNC;
else if (strncmp(name, "min", strlen("min")) == 0)
return MIN_AGG_FUNC;
else if (strncmp(name, "max", strlen("max")) == 0)
return MAX_AGG_FUNC;

You can assume from the name of a function what it's going to do.
Solving this properly might be hard.

Agreed. My assumption was that we would record in the data dictionary the
behaviour (or “polarity") of each aggregate function with respect to the
various operators. Column in pg_aggregate? I don’t know how we’d record it
exactly. A bitmask would be a possibility. Also, I don’t know what we’d do
with custom aggregate functions (or indeed custom operators). Allowing end
users to determine the value would potentially lead to assertion checks
being incorrectly skipped. Maybe we’d say that custom aggregates always
have a neutral polarity and are therefore not subject to this
optimisation.

This ought to be reproducible for you if you build with assertions.

Yes. I shall correct this when I do the aforementioned rebase and
application of TLC.

My feeling is that if we want to move forward on this topic, we need to
solve the concurrency question first. All these optimizations for when
we don't need to check the assertion are cool, but they are just
optimizations that we can apply later on, once we have solved the
critical problems.

I obviously agree that the concurrency issue needs solving. But I don’t
see that at all as a separate matter from the algos. Far from being merely
optimisations, the research indicates we can go a lot further toward
reducing the need for rechecks and, therefore, reducing the chance of
concurrency conflicts from occurring in the first place. This is true
regardless of whatever mechanism we use to enforce correct behaviour under
concurrent modifications -- e.g. a lock on the ASSERTION object itself,
enforced use of SERIALIZABLE, etc.

By way of example (lifted directly from the AM4DP book):

CREATE TABLE employee (
id INTEGER PRIMARY KEY,
dept INTEGER NOT NULL,
job TEXT NOT NULL
);

CREATE ASSERTION department_managers_need_administrators CHECK
(NOT EXISTS
(SELECT dept
FROM employee a
WHERE EXISTS (SELECT * FROM employee b
WHERE a.dept = b.dept
AND b.job IN ('Manager', 'Senior Manager'))
AND NOT EXISTS (SELECT * FROM employee b
WHERE a.dept = b.dept
AND b.job = 'Administrator')));

The current implementation derives "DELETE(employee), INSERT(employee) and
UPDATE(employee.dept, employee.job)" as the set of invalidating operations
and triggers accordingly. However, in this case, we can supplement the
triggers by having them inspect the transition tables to see if the actual
data from the triggering DML statement could in fact affect the truth of
the expression: specifically, only do the recheck on DELETE of an
"Administrator", INSERT of a "Manager" or "Senior Manager", or UPDATE when
the new job is a "Manager" or "Senior Manager" or the old job was an
"Administrator".

Now, if this is a company with 10,000 employees, and would therefore
presumably only require a handful of managers, right? ;-), then the
potential for a concurrency conflict is massively reduced when compared to
rechecking every time the employee table is touched.

(This optimisation has some caveats and is reliant upon being able to
derive the key of an expression from the underlying base tables plus some
stuff about functional dependencies. I have started work on it but sadly
not had time to progress it in recent months).

Having said all that: there are obviously going to be some expressions
that cannot be proven to have no potential for invalidating the assertion
truth. I guess this is the prime concern from a concurrency PoV? Example:

CREATE TABLE t (
b BOOLEAN NOT NULL,
n INTEGER NOT NULL,
PRIMARY KEY (b, n)
);

CREATE ASSERTION sum_per_b_less_than_10 CHECK
(NOT EXISTS
(SELECT FROM (SELECT b, SUM(n)
FROM t
GROUP BY b) AS v(b, sum_n)
WHERE sum_n > 10));

Invalidating operations are "INSERT(t) and UPDATE(t.b, t.n)". I guess the
interesting case, from a concurrency perspective, is how do we avoid an
INSERT WHERE b IS TRUE from blocking an INSERT WHERE B IS FALSE? I don’t
have an answer to that unfortunately. Although my understanding was that
SSI could help in these sorts of cases, but I really haven't read or
looked into the detail (yet). Thoughts?

-Joe

#25Andrew Gierth
andrew@tao11.riddles.org.uk
In reply to: Joe Wildish (#24)
Re: Implementing SQL ASSERTION

"Joe" == Joe Wildish <joe-postgresql.org@elusive.cx> writes:

Joe> Agreed. My assumption was that we would record in the data
Joe> dictionary the behaviour (or “polarity") of each aggregate
Joe> function with respect to the various operators. Column in
Joe> pg_aggregate? I don’t know how we’d record it exactly.

I haven't looked at the background of this, but if what you want to know
is whether the aggregate function has the semantics of min() or max()
(and if so, which) then the place to look is pg_aggregate.aggsortop.

(For a given aggregate foo(x), the presence of an operator oid in
aggsortop means something like "foo(x) is equivalent to (select x from
... order by x using OP limit 1)", and the planner will replace the
aggregate by the applicable subquery if it thinks it'd be faster.)

As for operators, you can only make assumptions about their meaning if
the operator is a member of some opfamily that assigns it some
semantics. For example, the planner can assume that WHERE x=y AND x=1
implies that y=1 (assuming x and y are of appropriate types) not because
it assumes that "=" is the name of a transitive operator, but because
the operators actually selected for (x=1) and (x=y) are both "equality"
members of the same btree operator family. Likewise proving that (a>2)
implies (a>1) requires knowing that > is a btree comparison op.

--
Andrew (irc:RhodiumToad)

#26Peter Eisentraut
peter.eisentraut@2ndquadrant.com
In reply to: Joe Wildish (#24)
Re: Implementing SQL ASSERTION

On 25/09/2018 01:04, Joe Wildish wrote:

Having said all that: there are obviously going to be some expressions
that cannot be proven to have no potential for invalidating the assertion
truth. I guess this is the prime concern from a concurrency PoV?

Before we spend more time on this, I think we need to have at least a
plan for that. Perhaps we could should disallow cases that we can't
handle otherwise. But even that would need some analysis of which
practical cases we can and cannot handle, how we could extend support in
the future, etc.

In the meantime, I have committed parts of your gram.y changes that seem
to come up every time someone dusts off an assertions patch. Keep that
in mind when you rebase.

--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#27David Fetter
david@fetter.org
In reply to: Joe Wildish (#24)
Re: Implementing SQL ASSERTION

On Tue, Sep 25, 2018 at 12:04:12AM +0100, Joe Wildish wrote:

Hi Peter,

My feeling is that if we want to move forward on this topic, we need to
solve the concurrency question first. All these optimizations for when
we don't need to check the assertion are cool, but they are just
optimizations that we can apply later on, once we have solved the
critical problems.

Having said all that: there are obviously going to be some expressions
that cannot be proven to have no potential for invalidating the assertion
truth. I guess this is the prime concern from a concurrency PoV? Example:

CREATE TABLE t (
b BOOLEAN NOT NULL,
n INTEGER NOT NULL,
PRIMARY KEY (b, n)
);

CREATE ASSERTION sum_per_b_less_than_10 CHECK
(NOT EXISTS
(SELECT FROM (SELECT b, SUM(n)
FROM t
GROUP BY b) AS v(b, sum_n)
WHERE sum_n > 10));

Invalidating operations are "INSERT(t) and UPDATE(t.b, t.n)".

So would DELETE(t), assuming n can be negative.

Is there some interesting and fairly easily documented subset of
ASSERTIONs that wouldn't have the "can't prove" property?

Best,
David.
--
David Fetter <david(at)fetter(dot)org> http://fetter.org/
Phone: +1 415 235 3778

Remember to vote!
Consider donating to Postgres: http://www.postgresql.org/about/donate

#28Joe Wildish
joe-postgresql.org@elusive.cx
In reply to: Peter Eisentraut (#26)
Re: Implementing SQL ASSERTION

On 26 Sep 2018, at 12:36, Peter Eisentraut <peter.eisentraut@2ndquadrant.com> wrote:

On 25/09/2018 01:04, Joe Wildish wrote:

Having said all that: there are obviously going to be some expressions
that cannot be proven to have no potential for invalidating the assertion
truth. I guess this is the prime concern from a concurrency PoV?

Before we spend more time on this, I think we need to have at least a
plan for that.

Having thought about this some more: the answer could lie in using predicate
locks, and enforcing that the transaction be SERIALIZABLE whenever an ASSERTION
is triggered.

To make use of the predicate locks we'd do a transformation on the ASSERTION
expression. I believe that there is derivation, similar to the one mentioned
up-thread re: "managers and administrators", that would essentially push
predicates into the expression on the basis of the changed data. The semantics
of the expression would remain unchanged, but it would mean that when the
expression is rechecked, the minimal set of data is read and would therefore not
conflict with other DML statements that had triggered the same ASSERTION but had
modified unrelated data. Example:

CREATE TABLE t
(n INTEGER NOT NULL,
m INTEGER NOT NULL,
k INTEGER NOT NULL,
PRIMARY KEY (n, m));

CREATE ASSERTION sum_k_at_most_10 CHECK
(NOT EXISTS
(SELECT * FROM
(SELECT n, sum(k)
FROM t
GROUP BY n)
AS r(n, ks)
WHERE ks > 10));

On an INSERT/DELETE/UPDATE of "t", we would transform the inner-most expression
of the ASSERTION to have a predicate of "WHERE n = NEW.n". In my experiments I
can see that doing so allows concurrent transactions to COMMIT that have
modified unrelated segments of "t" (assuming the planner uses Index Scan). The
efficacy of this would be dictated by the granularity of the SIREAD locks; my
understanding is that this can be as low as tuple-level in the case where Index
Scans are used (and this is borne out in my experiments - ie. you don't want a
SeqScan).

Perhaps we could should disallow cases that we can't
handle otherwise. But even that would need some analysis of which
practical cases we can and cannot handle, how we could extend support in
the future, etc.

The optimisation I mentioned up-thread, plus the one hypothesised here, both
rely on being able to derive the key of an expression from the underlying base
tables/other expressions. We could perhaps disallow ASSERTIONS that don't have
such properties?

Beyond that I think it starts to get difficult (impossible?) to know which
expressions are likely to be costly on the basis of static analysis. It could be
legitimate to have an ASSERTION defined over what turns out to be a small subset
of a very large table, for example.

-Joe

#29Joe Wildish
joe-postgresql.org@elusive.cx
In reply to: Andrew Gierth (#25)
Re: Implementing SQL ASSERTION

Hi Andrew,

On 25 Sep 2018, at 01:51, Andrew Gierth <andrew@tao11.riddles.org.uk> wrote:

I haven't looked at the background of this, but if what you want to know
is whether the aggregate function has the semantics of min() or max()
(and if so, which) then the place to look is pg_aggregate.aggsortop.

Thanks for the pointer. I've had a quick look at pg_aggregate, and back
at my code, but I think there is more to it than just the sorting property.
Specifically we need to know about the aggregate function when combined with
connectors <, <=, < ANY, <= ANY, < ALL and <= ALL (and their equivalents
with ">" and ">="). Also, it looks like COUNT and SUM don't have a sortop
(the other aggregates I've catered for do though).

When I come to do the rework of the patch I'll take a more in-depth look
though, and see if this can be utilised.

As for operators, you can only make assumptions about their meaning if
the operator is a member of some opfamily that assigns it some
semantics.

I had clocked the BT semantics stuff when doing the PoC patch. I have used
the "get_op_btree_interpretation" function for determining operator meaning.

-Joe

#30Joe Wildish
joe-postgresql.org@elusive.cx
In reply to: David Fetter (#27)
Re: Implementing SQL ASSERTION

Hi David,

On 26 Sep 2018, at 19:47, David Fetter <david@fetter.org> wrote:

Invalidating operations are "INSERT(t) and UPDATE(t.b, t.n)".

So would DELETE(t), assuming n can be negative.

Oops, right you are. Bug in my implementation :-)

Is there some interesting and fairly easily documented subset of
ASSERTIONs that wouldn't have the "can't prove" property?

We can certainly know at the time the ASSERTION is created if we
can use the transition table optimisation, as that relies upon
the expression being written in such a way that a key can be
derived for each expression.

We could warn or disallow the creation on that basis. Ceri & Widom
mention this actually in their papers, and their view is that most
real-world use cases do indeed allow themselves to be optimised
using the transition tables.

-Joe

#31Andrew Gierth
andrew@tao11.riddles.org.uk
In reply to: Joe Wildish (#29)
Re: Implementing SQL ASSERTION

"Joe" == Joe Wildish <joe-postgresql.org@elusive.cx> writes:

I haven't looked at the background of this, but if what you want to
know is whether the aggregate function has the semantics of min() or
max() (and if so, which) then the place to look is
pg_aggregate.aggsortop.

Joe> Thanks for the pointer. I've had a quick look at pg_aggregate, and
Joe> back at my code, but I think there is more to it than just the
Joe> sorting property. Specifically we need to know about the aggregate
Joe> function when combined with connectors <, <=, < ANY, <= ANY, < ALL
Joe> and <= ALL (and their equivalents with ">" and ">=").

The presence of an aggsortop means "this aggregate function is
interchangeable with (select x from ... order by x using OP limit 1)",
with all of the semantic consequences that implies. Since OP must be the
"<" or ">" member of a btree index opclass, the semantics of its
relationships with other members of the same opfamily can be deduced
from that.

Joe> Also, it looks like COUNT and SUM don't have a sortop

Right, because those currently have no semantics that PG needs to know
about or describe.

--
Andrew (irc:RhodiumToad)

#32Dmitry Dolgov
9erthalion6@gmail.com
In reply to: Andrew Gierth (#31)
Re: Implementing SQL ASSERTION

On Tue, Sep 25, 2018 at 1:04 AM Joe Wildish <joe-postgresql.org@elusive.cx>
wrote:

All agreed. I’ll give the patch some TLC and get a new version that
addresses the above.

Hi,

Just a reminder, that the patch still needs to be rebased, could you please do
this? I'm moving the item to the next CF.

#33Andres Freund
andres@anarazel.de
In reply to: Dmitry Dolgov (#32)
Re: Implementing SQL ASSERTION

Hi,

On 2018-11-29 16:54:14 +0100, Dmitry Dolgov wrote:

On Tue, Sep 25, 2018 at 1:04 AM Joe Wildish <joe-postgresql.org@elusive.cx>
wrote:

All agreed. I’ll give the patch some TLC and get a new version that
addresses the above.

Hi,

Just a reminder, that the patch still needs to be rebased, could you please do
this? I'm moving the item to the next CF.

As nothing has happened, I'm marking this patch as returned with feedback.

Greetings,

Andres Freund