]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Add compiler support for PostgreSQL "NOT VALID" constraints.
authorGilbert Gilb's <gilbsgilbert@gmail.com>
Sat, 22 Jan 2022 00:45:31 +0000 (01:45 +0100)
committerGilbert Gilb's <gilbsgilbert@gmail.com>
Sun, 23 Jan 2022 17:53:51 +0000 (18:53 +0100)
PostgreSQL supports declaring CHECK and FOREIGN KEY constraints as "NOT
VALID" which prevents the constraint from being validated on already
inserted rows, but still validates against any newly inserted or updated
rows.

This commit adds compiler support for this dialect option on
`CheckConstraint` and `ForeignKeyConstraint` classes. Although this
option doesn't make much sense on table creation, it doesn't seem to
cause any issue with Postgres and it is a preliminary step to have this
supported in Alembic during `ALTER TABLE`. Note that other constraints
types do not support this option.

See https://www.postgresql.org/docs/current/sql-altertable.html

See also #4825 which added supported for "NOT VALID" constraints
introsecpection.

Fixes: #7600
Signed-off-by: Gilbert Gilb's <gilbsgilbert@gmail.com>
doc/build/changelog/unreleased_14/7601.rst [new file with mode: 0644]
lib/sqlalchemy/dialects/postgresql/base.py
test/dialect/postgresql/test_compiler.py

diff --git a/doc/build/changelog/unreleased_14/7601.rst b/doc/build/changelog/unreleased_14/7601.rst
new file mode 100644 (file)
index 0000000..41481c9
--- /dev/null
@@ -0,0 +1,10 @@
+.. change::
+    :tags: postgresql, dialect
+    :tickets: 7600
+
+    Added compiler support for ``NOT VALID`` CHECK and FOREIGN KEY constraints
+    in PostgreSQL dialect.
+
+    .. seealso::
+
+        :ref:`_postgresql_constraint_options`
index e4ebbcb8070a21ead77d24dcc4f4fe07ce31260d..5f905d617aa5e7cfbd7ca73061e9d9a10dc43133 100644 (file)
@@ -1060,6 +1060,24 @@ dialect in conjunction with the :class:`_schema.Table` construct:
     `PostgreSQL CREATE TABLE options
     <https://www.postgresql.org/docs/current/static/sql-createtable.html>`_
 
+.. _postgresql_constraint_options:
+
+PostgreSQL Constraint Options
+-----------------------------
+
+* ``NOT VALID``::
+
+    CheckConstraint("some_field IS NOT NULL", postgresql_not_valid=True)
+
+    ForeignKeyConstraint(["some_id"], ["some_table.some_id"], postgresql_not_valid=True)
+
+  The above option is only available on check and foreign key constraints.
+
+  .. seealso::
+
+      `PostgreSQL ALTER TABLE options
+      <https://www.postgresql.org/docs/current/static/sql-altertable.html>`_
+
 .. _postgresql_table_valued_overview:
 
 Table values, Table and Column valued functions, Row and Tuple objects
@@ -2580,6 +2598,14 @@ class PGDDLCompiler(compiler.DDLCompiler):
             colspec += " NULL"
         return colspec
 
+    def _define_constraint_validity(self, constraint):
+        if self.dialect._supports_not_valid_constraints:
+            not_valid = constraint.dialect_options["postgresql"]["not_valid"]
+            if not_valid:
+                return " NOT VALID"
+
+        return ""
+
     def visit_check_constraint(self, constraint):
         if constraint._type_bound:
             typ = list(constraint.columns)[0].type
@@ -2594,7 +2620,16 @@ class PGDDLCompiler(compiler.DDLCompiler):
                     "create_constraint=False on this Enum datatype."
                 )
 
-        return super(PGDDLCompiler, self).visit_check_constraint(constraint)
+        text = super(PGDDLCompiler, self).visit_check_constraint(constraint)
+        text += self._define_constraint_validity(constraint)
+        return text
+
+    def visit_foreign_key_constraint(self, constraint):
+        text = super(PGDDLCompiler, self).visit_foreign_key_constraint(
+            constraint
+        )
+        text += self._define_constraint_validity(constraint)
+        return text
 
     def visit_drop_table_comment(self, drop):
         return "COMMENT ON TABLE %s IS NULL" % self.preparer.format_table(
@@ -3210,6 +3245,18 @@ class PGDialect(default.DefaultDialect):
                 "inherits": None,
             },
         ),
+        (
+            schema.CheckConstraint,
+            {
+                "not_valid": False,
+            },
+        ),
+        (
+            schema.ForeignKeyConstraint,
+            {
+                "not_valid": False,
+            },
+        ),
     ]
 
     reflection_options = ("postgresql_ignore_search_path",)
@@ -3217,6 +3264,7 @@ class PGDialect(default.DefaultDialect):
     _backslash_escapes = True
     _supports_create_index_concurrently = True
     _supports_drop_index_concurrently = True
+    _supports_not_valid_constraints = True
 
     def __init__(self, json_serializer=None, json_deserializer=None, **kwargs):
         default.DefaultDialect.__init__(self, **kwargs)
@@ -3259,6 +3307,10 @@ class PGDialect(default.DefaultDialect):
             2,
         )
         self.supports_identity_columns = self.server_version_info >= (10,)
+        self._supports_not_valid_constraints = self.server_version_info >= (
+            9,
+            1,
+        )
 
     def get_isolation_level_values(self, dbapi_conn):
         # note the generic dialect doesn't have AUTOCOMMIT, however
index 0e04ccb955623f4ea3755f40aacc45f56b34478a..ae4d73c7aa088f57a9ab412eabb67b511e809883 100644 (file)
@@ -3,6 +3,7 @@ from sqlalchemy import and_
 from sqlalchemy import BigInteger
 from sqlalchemy import bindparam
 from sqlalchemy import cast
+from sqlalchemy import CheckConstraint
 from sqlalchemy import Column
 from sqlalchemy import Computed
 from sqlalchemy import Date
@@ -10,6 +11,7 @@ from sqlalchemy import delete
 from sqlalchemy import Enum
 from sqlalchemy import exc
 from sqlalchemy import Float
+from sqlalchemy import ForeignKeyConstraint
 from sqlalchemy import func
 from sqlalchemy import Identity
 from sqlalchemy import Index
@@ -828,6 +830,63 @@ class CompileTest(fixtures.TestBase, AssertsCompiledSQL):
             schema.DropIndex(idx1), "DROP INDEX test_idx1", dialect=dialect_9_1
         )
 
+    def test_create_check_constraint_not_valid(self):
+        m = MetaData()
+
+        tbl = Table(
+            "testtbl",
+            m,
+            Column("data", Integer),
+            CheckConstraint("data = 0", postgresql_not_valid=True),
+        )
+
+        self.assert_compile(
+            schema.CreateTable(tbl),
+            "CREATE TABLE testtbl (data INTEGER, CHECK (data = 0) NOT VALID)",
+        )
+
+        dialect_9_0 = postgresql.dialect()
+        dialect_9_0._supports_not_valid_constraints = False
+        self.assert_compile(
+            schema.CreateTable(tbl),
+            "CREATE TABLE testtbl (data INTEGER, CHECK (data = 0))",
+            dialect=dialect_9_0,
+        )
+
+    def test_create_foreign_key_constraint_not_valid(self):
+        m = MetaData()
+
+        tbl = Table(
+            "testtbl",
+            m,
+            Column("a", Integer),
+            Column("b", Integer),
+            ForeignKeyConstraint(
+                "b", ["testtbl.a"], postgresql_not_valid=True
+            ),
+        )
+
+        self.assert_compile(
+            schema.CreateTable(tbl),
+            "CREATE TABLE testtbl ("
+            "a INTEGER, "
+            "b INTEGER, "
+            "FOREIGN KEY(b) REFERENCES testtbl (a) NOT VALID"
+            ")",
+        )
+
+        dialect_9_0 = postgresql.dialect()
+        dialect_9_0._supports_not_valid_constraints = False
+        self.assert_compile(
+            schema.CreateTable(tbl),
+            "CREATE TABLE testtbl ("
+            "a INTEGER, "
+            "b INTEGER, "
+            "FOREIGN KEY(b) REFERENCES testtbl (a)"
+            ")",
+            dialect=dialect_9_0,
+        )
+
     def test_exclude_constraint_min(self):
         m = MetaData()
         tbl = Table("testtbl", m, Column("room", Integer, primary_key=True))