]> git.ipfire.org Git - thirdparty/sqlalchemy/alembic.git/commitdiff
Resolve Postgresql implicit indexes via duplicates_constraint
authorMike Bayer <mike_mp@zzzcomputing.com>
Wed, 25 Oct 2017 19:24:29 +0000 (15:24 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Wed, 25 Oct 2017 20:52:05 +0000 (16:52 -0400)
Fixed bug where autogenerate would produce a DROP statement for the index
implicitly created by a Postgresql EXCLUDE constraint, rather than skipping
it as is the case for indexes implicitly generated by unique constraints.
Makes use of SQLAlchemy 1.0.x's improved "duplicates index" metadata and
requires at least SQLAlchemy version 1.0.x to function correctly.

Change-Id: I7362c8045f69553c6090dd3cb236569b0c9b1e67
Fixes: #461
alembic/autogenerate/compare.py
alembic/ddl/postgresql.py
alembic/testing/requirements.py
docs/build/unreleased/461.rst [new file with mode: 0644]
tests/requirements.py
tests/test_autogen_indexes.py

index f393c40b964f441eda5a1808a4b13c39ec173f8d..3abb32dfeb39322fb39be2ebef39a5cdcc63819f 100644 (file)
@@ -185,12 +185,14 @@ def _compare_tables(conn_table_names, metadata_table_names,
 
 
 def _make_index(params, conn_table):
-    # TODO: add .info such as 'duplicates_constraint'
-    return sa_schema.Index(
+    ix = sa_schema.Index(
         params['name'],
         *[conn_table.c[cname] for cname in params['column_names']],
         unique=params['unique']
     )
+    if 'duplicates_constraint' in params:
+        ix.info['duplicates_constraint'] = params['duplicates_constraint']
+    return ix
 
 
 def _make_unique_constraint(params, conn_table):
index 83471aa31318e2983f8b79f6db82f87ecabe3ce4..e42bc8105448ec73373f0614f8f6637da63b0d2f 100644 (file)
@@ -149,19 +149,25 @@ class PostgresqlImpl(DefaultImpl):
                                         conn_indexes,
                                         metadata_unique_constraints,
                                         metadata_indexes):
+
         conn_uniques_by_name = dict(
             (c.name, c) for c in conn_unique_constraints)
         conn_indexes_by_name = dict(
             (c.name, c) for c in conn_indexes)
 
-        # TODO: if SQLA 1.0, make use of "duplicates_constraint"
-        # metadata
-        doubled_constraints = dict(
-            (name, (conn_uniques_by_name[name], conn_indexes_by_name[name]))
-            for name in set(conn_uniques_by_name).intersection(
-                conn_indexes_by_name)
-        )
-        for name, (uq, ix) in doubled_constraints.items():
+        if not util.sqla_100:
+            doubled_constraints = set(
+                conn_indexes_by_name[name]
+                for name in set(conn_uniques_by_name).intersection(
+                    conn_indexes_by_name)
+            )
+        else:
+            doubled_constraints = set(
+                index for index in
+                conn_indexes if index.info.get('duplicates_constraint')
+            )
+
+        for ix in doubled_constraints:
             conn_indexes.remove(ix)
 
         for idx in list(metadata_indexes):
index 7564b06e8054db0281c3d8d01e5daf40f4cac0f3..32645ed8bfbb21edf4500aefb265664df978a5b9 100644 (file)
@@ -158,6 +158,13 @@ class SuiteRequirements(Requirements):
             "SQLAlchemy 0.9.4 or greater required"
         )
 
+    @property
+    def sqlalchemy_100(self):
+        return exclusions.skip_if(
+            lambda config: not util.sqla_100,
+            "SQLAlchemy 1.0.0 or greater required"
+        )
+
     @property
     def sqlalchemy_1014(self):
         return exclusions.skip_if(
diff --git a/docs/build/unreleased/461.rst b/docs/build/unreleased/461.rst
new file mode 100644 (file)
index 0000000..0435b59
--- /dev/null
@@ -0,0 +1,11 @@
+.. change::
+    :tags: bug, postgresql, autogenerate
+    :tickets: 461
+
+    Fixed bug where autogenerate would produce a DROP statement for the index
+    implicitly created by a Postgresql EXCLUDE constraint, rather than skipping
+    it as is the case for indexes implicitly generated by unique constraints.
+    Makes use of SQLAlchemy 1.0.x's improved "duplicates index" metadata and
+    requires at least SQLAlchemy version 1.0.x to function correctly.
+
+
index bdfa8a939640c681b46f2c82806e37947c282cad..7662987757f27254195132476317000676badefa 100644 (file)
@@ -116,6 +116,25 @@ class DefaultRequirements(SuiteRequirements):
 
         return exclusions.only_if(check_uuid_ossp)
 
+    def _has_pg_extension(self, name):
+        def check(config):
+            if not exclusions.against(config, "postgresql"):
+                return False
+            count = config.db.scalar(
+                "SELECT count(*) FROM pg_extension "
+                "WHERE extname='%s'" % name)
+            return bool(count)
+        return exclusions.only_if(check, "needs %s extension" % name)
+
+    @property
+    def hstore(self):
+        return self._has_pg_extension("hstore")
+
+    @property
+    def btree_gist(self):
+        return self._has_pg_extension("btree_gist")
+
+
     @property
     def autoincrement_on_composite_pk(self):
         return exclusions.skip_if(["sqlite"], "not supported by database")
index 97d0aa1ed5083a72ab0b399f2c3210e08d295190..e426a678aaa576d539e1fd0a76825cd6a4904a4a 100644 (file)
@@ -609,6 +609,31 @@ class PGUniqueIndexTest(AutogenerateUniqueIndexTest):
         diffs = self._fixture(m1, m2, include_schemas=True)
         eq_(diffs, [])
 
+    @config.requirements.sqlalchemy_100
+    @config.requirements.btree_gist
+    def test_exclude_const_unchanged(self):
+        from sqlalchemy.dialects.postgresql import TSRANGE, ExcludeConstraint
+
+        m1 = MetaData()
+        m2 = MetaData()
+
+        Table(
+            'add_excl', m1,
+            Column('id', Integer, primary_key=True),
+            Column('period', TSRANGE),
+            ExcludeConstraint(('period', '&&'), name='quarters_period_excl')
+        )
+
+        Table(
+            'add_excl', m2,
+            Column('id', Integer, primary_key=True),
+            Column('period', TSRANGE),
+            ExcludeConstraint(('period', '&&'), name='quarters_period_excl')
+        )
+
+        diffs = self._fixture(m1, m2)
+        eq_(diffs, [])
+
     def test_same_tname_two_schemas(self):
         m1 = MetaData()
         m2 = MetaData()