]> git.ipfire.org Git - thirdparty/sqlalchemy/alembic.git/commitdiff
Generalize uq -> uix dedupe logic
authorMike Bayer <mike_mp@zzzcomputing.com>
Tue, 3 Jan 2017 21:24:42 +0000 (16:24 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Mon, 9 Jan 2017 19:26:53 +0000 (14:26 -0500)
Adjusted the logic originally added for :ticket:`276` that detects MySQL
unique constraints which are actually unique indexes to be generalized
for any dialect that has this behavior, for SQLAlchemy version 1.0 and
greater.  This is to allow for upcoming SQLAlchemy support for unique
constraint reflection for Oracle, which also has no dedicated concept of
"unique constraint" and instead establishes a unique index.

Change-Id: Ie5770aba36005ec8618bdc18bc4633413d37fc16

alembic/autogenerate/compare.py
alembic/ddl/mysql.py
docs/build/changelog.rst
tests/test_autogen_indexes.py

index c3d945282c7814fa67b55834c9442ffe5668b85f..267b017ee7c27126f4d9d337136593e4b3161b2f 100644 (file)
@@ -195,11 +195,14 @@ def _make_index(params, conn_table):
 
 
 def _make_unique_constraint(params, conn_table):
-    # TODO: add .info such as 'duplicates_index'
-    return sa_schema.UniqueConstraint(
+    uq = sa_schema.UniqueConstraint(
         *[conn_table.c[cname] for cname in params['column_names']],
         name=params['name']
     )
+    if 'duplicates_index' in params:
+        uq.info['duplicates_index'] = params['duplicates_index']
+
+    return uq
 
 
 def _make_foreign_key(params, conn_table):
@@ -364,6 +367,8 @@ def _compare_indexes_and_uniques(
 
     supports_unique_constraints = False
 
+    unique_constraints_duplicate_unique_indexes = False
+
     if conn_table is not None:
         # 1b. ... and from connection, if the table exists
         if hasattr(inspector, "get_unique_constraints"):
@@ -378,6 +383,10 @@ def _compare_indexes_and_uniques(
                 # method in SQLAlchemy due to the cache decorator
                 # not being present
                 pass
+            else:
+                for uq in conn_uniques:
+                    if uq.get('duplicates_index'):
+                        unique_constraints_duplicate_unique_indexes = True
         try:
             conn_indexes = inspector.get_indexes(tname, schema=schema)
         except NotImplementedError:
@@ -389,6 +398,16 @@ def _compare_indexes_and_uniques(
                            for uq_def in conn_uniques)
         conn_indexes = set(_make_index(ix, conn_table) for ix in conn_indexes)
 
+    # 2a. if the dialect dupes unique indexes as unique constraints
+    # (mysql and oracle), correct for that
+
+    if unique_constraints_duplicate_unique_indexes:
+        _correct_for_uq_duplicates_uix(
+            conn_uniques, conn_indexes,
+            metadata_unique_constraints,
+            metadata_indexes
+        )
+
     # 3. give the dialect a chance to omit indexes and constraints that
     # we know are either added implicitly by the DB or that the DB
     # can't accurately report on
@@ -586,6 +605,48 @@ def _compare_indexes_and_uniques(
             obj_added(unnamed_metadata_uniques[uq_sig])
 
 
+def _correct_for_uq_duplicates_uix(
+    conn_unique_constraints,
+        conn_indexes,
+        metadata_unique_constraints,
+        metadata_indexes):
+
+    # dedupe unique indexes vs. constraints, since MySQL / Oracle
+    # doesn't really have unique constraints as a separate construct.
+    # but look in the metadata and try to maintain constructs
+    # that already seem to be defined one way or the other
+    # on that side.  This logic was formerly local to MySQL dialect,
+    # generalized to Oracle and others. See #276
+    metadata_uq_names = set([
+        cons.name for cons in metadata_unique_constraints
+        if cons.name is not None])
+
+    unnamed_metadata_uqs = set([
+        _uq_constraint_sig(cons).sig
+        for cons in metadata_unique_constraints
+        if cons.name is None
+    ])
+
+    metadata_ix_names = set([
+        cons.name for cons in metadata_indexes if cons.unique])
+    conn_ix_names = dict(
+        (cons.name, cons) for cons in conn_indexes if cons.unique
+    )
+
+    uqs_dupe_indexes = dict(
+        (cons.name, cons) for cons in conn_unique_constraints
+        if cons.info['duplicates_index']
+    )
+    for overlap in uqs_dupe_indexes:
+        if overlap not in metadata_uq_names:
+            if _uq_constraint_sig(uqs_dupe_indexes[overlap]).sig \
+                    not in unnamed_metadata_uqs:
+
+                conn_unique_constraints.discard(uqs_dupe_indexes[overlap])
+        elif overlap not in metadata_ix_names:
+            conn_indexes.discard(conn_ix_names[overlap])
+
+
 @comparators.dispatch_for("column")
 def _compare_nullable(
     autogen_context, alter_column_op, schema, tname, cname, conn_col,
index 7ed742fef7813f46e7642a5771f3d613485407ae..3af4fa65ea2a1f9542e72ab0ceb30fcb98096cc5 100644 (file)
@@ -10,7 +10,7 @@ from .base import ColumnNullable, ColumnName, ColumnDefault, \
     format_server_default
 from .base import alter_table
 from ..autogenerate import compare
-from ..util.sqla_compat import _is_type_bound
+from ..util.sqla_compat import _is_type_bound, sqla_100
 
 
 class MySQLImpl(DefaultImpl):
@@ -132,6 +132,19 @@ class MySQLImpl(DefaultImpl):
             if idx.name in removed:
                 metadata_indexes.remove(idx)
 
+        if not sqla_100:
+            self._legacy_correct_for_dupe_uq_uix(
+                conn_unique_constraints,
+                conn_indexes,
+                metadata_unique_constraints,
+                metadata_indexes
+            )
+
+    def _legacy_correct_for_dupe_uq_uix(self, conn_unique_constraints,
+                                        conn_indexes,
+                                        metadata_unique_constraints,
+                                        metadata_indexes):
+
         # then dedupe unique indexes vs. constraints, since MySQL
         # doesn't really have unique constraints as a separate construct.
         # but look in the metadata and try to maintain constructs
index 0baaa5380a378f0472090c7cdd6810f365368a5c..b09579699f885aa7e68425f27b43e8d7c10566df 100644 (file)
@@ -14,6 +14,16 @@ Changelog
       primary key constraint would fail to remove the "primary_key" flag
       from the column, resulting in the constraint being recreated.
 
+    .. change:: update_uq_dedupe
+      :tags: bug, autogenerate, oracle
+
+      Adjusted the logic originally added for :ticket:`276` that detects MySQL
+      unique constraints which are actually unique indexes to be generalized
+      for any dialect that has this behavior, for SQLAlchemy version 1.0 and
+      greater.  This is to allow for upcoming SQLAlchemy support for unique
+      constraint reflection for Oracle, which also has no dedicated concept of
+      "unique constraint" and instead establishes a unique index.
+
     .. change:: 356
       :tags: bug, versioning
       :tickets: 356
index d5f2e7ccb226a60142d93cb033cc55a8faf42547..bec90a7d4cd2846bd35b750e9b2db1c696afc507 100644 (file)
@@ -712,6 +712,12 @@ class MySQLUniqueIndexTest(AutogenerateUniqueIndexTest):
             assert False, "unexpected success"
 
 
+class OracleUniqueIndexTest(AutogenerateUniqueIndexTest):
+    reports_unnamed_constraints = True
+    reports_unique_constraints_as_indexes = True
+    __only_on__ = "oracle"
+
+
 class NoUqReflectionIndexTest(NoUqReflection, AutogenerateUniqueIndexTest):
     reports_unique_constraints = False
     __only_on__ = 'sqlite'