From: Gilbert Gilb's Date: Sat, 22 Jan 2022 00:45:31 +0000 (+0100) Subject: Add compiler support for PostgreSQL "NOT VALID" constraints. X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=21a1e9e402d8c1b4fd3739dd4c93df3e0b5b4b08;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git Add compiler support for PostgreSQL "NOT VALID" constraints. 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 --- diff --git a/doc/build/changelog/unreleased_14/7601.rst b/doc/build/changelog/unreleased_14/7601.rst new file mode 100644 index 0000000000..41481c9f8c --- /dev/null +++ b/doc/build/changelog/unreleased_14/7601.rst @@ -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` diff --git a/lib/sqlalchemy/dialects/postgresql/base.py b/lib/sqlalchemy/dialects/postgresql/base.py index e4ebbcb807..5f905d617a 100644 --- a/lib/sqlalchemy/dialects/postgresql/base.py +++ b/lib/sqlalchemy/dialects/postgresql/base.py @@ -1060,6 +1060,24 @@ dialect in conjunction with the :class:`_schema.Table` construct: `PostgreSQL CREATE TABLE options `_ +.. _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 + `_ + .. _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 diff --git a/test/dialect/postgresql/test_compiler.py b/test/dialect/postgresql/test_compiler.py index 0e04ccb955..ae4d73c7aa 100644 --- a/test/dialect/postgresql/test_compiler.py +++ b/test/dialect/postgresql/test_compiler.py @@ -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))