- Fixed bug where MySQL backend would report dropped unique indexes

and/or constraints as both at the same time.  This is because
MySQL doesn't actually have a "unique constraint" construct that
reports differently than a "unique index", so it is present in both
lists.  The net effect though is that the MySQL backend will report
a dropped unique index/constraint as an index in cases where the object
was first created as a unique constraint, if no other information
is available to make the decision.  This differs from other backends
like Postgresql which can report on unique constraints and
unique indexes separately.
fixes #276
This commit is contained in:
Mike Bayer 2015-02-20 19:20:53 -05:00
parent e8a0c740b1
commit 2108076362
4 changed files with 75 additions and 7 deletions

View File

@ -9,6 +9,7 @@ from .base import ColumnNullable, ColumnName, ColumnDefault, \
ColumnType, AlterColumn, format_column_name, \
format_server_default
from .base import alter_table
from ..autogenerate import compare
class MySQLImpl(DefaultImpl):
@ -123,6 +124,39 @@ class MySQLImpl(DefaultImpl):
if idx.name in removed:
metadata_indexes.remove(idx)
# 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
# that already seem to be defined one way or the other
# on that side. See #276
metadata_uq_names = set([
cons.name for cons in metadata_unique_constraints
if cons.name is not None])
unnamed_metadata_uqs = set([
compare._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_uq_names = dict(
(cons.name, cons) for cons in conn_unique_constraints
)
conn_ix_names = dict(
(cons.name, cons) for cons in conn_indexes if cons.unique
)
for overlap in set(conn_uq_names).intersection(conn_ix_names):
if overlap not in metadata_uq_names:
if compare._uq_constraint_sig(conn_uq_names[overlap]).sig \
not in unnamed_metadata_uqs:
conn_unique_constraints.discard(conn_uq_names[overlap])
elif overlap not in metadata_ix_names:
conn_indexes.discard(conn_ix_names[overlap])
class MySQLAlterDefault(AlterColumn):

View File

@ -6,6 +6,21 @@ Changelog
.. changelog::
:version: 0.7.5
.. change::
:tags: bug, autogenerate, mysql
:tickets: 276
Fixed bug where MySQL backend would report dropped unique indexes
and/or constraints as both at the same time. This is because
MySQL doesn't actually have a "unique constraint" construct that
reports differently than a "unique index", so it is present in both
lists. The net effect though is that the MySQL backend will report
a dropped unique index/constraint as an index in cases where the object
was first created as a unique constraint, if no other information
is available to make the decision. This differs from other backends
like Postgresql which can report on unique constraints and
unique indexes separately.
.. change::
:tags: bug, commands
:tickets: 269

View File

@ -49,3 +49,7 @@ class DefaultRequirements(SuiteRequirements):
def fk_names(self):
"""foreign key constraints always have names in the DB"""
return exclusions.fails_on('sqlite')
@property
def reflects_unique_constraints_unambiguously(self):
return exclusions.fails_on("mysql")

View File

@ -36,6 +36,8 @@ class NoUqReflection(object):
class AutogenerateUniqueIndexTest(AutogenFixtureTest, TestBase):
reports_unique_constraints = True
reports_unique_constraints_as_indexes = False
__requires__ = ('unique_constraint_reflection', )
__only_on__ = 'sqlite'
@ -441,8 +443,17 @@ class AutogenerateUniqueIndexTest(AutogenFixtureTest, TestBase):
('x' in obj.name) if obj.name is not None else False)
for cmd, obj in diffs)
if self.reports_unnamed_constraints:
assert ("remove_constraint", True) in diffs
assert ("add_constraint", False) in diffs
if self.reports_unique_constraints_as_indexes:
eq_(
diffs,
set([("remove_index", True), ("add_constraint", False)])
)
else:
eq_(
diffs,
set([("remove_constraint", True),
("add_constraint", False)])
)
def test_remove_named_unique_index(self):
m1 = MetaData()
@ -453,14 +464,14 @@ class AutogenerateUniqueIndexTest(AutogenFixtureTest, TestBase):
Index('xidx', 'x', unique=True)
)
Table('remove_idx', m2,
Column('x', Integer),
Column('x', Integer)
)
diffs = self._fixture(m1, m2)
if self.reports_unique_constraints:
diffs = set((cmd, obj.name) for cmd, obj in diffs)
assert ("remove_index", "xidx") in diffs
eq_(diffs, set([("remove_index", "xidx")]))
else:
eq_(diffs, [])
@ -479,8 +490,11 @@ class AutogenerateUniqueIndexTest(AutogenFixtureTest, TestBase):
diffs = self._fixture(m1, m2)
if self.reports_unique_constraints:
diffs = ((cmd, obj.name) for cmd, obj in diffs)
assert ("remove_constraint", "xidx") in diffs
diffs = set((cmd, obj.name) for cmd, obj in diffs)
if self.reports_unique_constraints_as_indexes:
eq_(diffs, set([("remove_index", "xidx")]))
else:
eq_(diffs, set([("remove_constraint", "xidx")]))
else:
eq_(diffs, [])
@ -628,6 +642,7 @@ class PGUniqueIndexTest(AutogenerateUniqueIndexTest):
class MySQLUniqueIndexTest(AutogenerateUniqueIndexTest):
reports_unnamed_constraints = True
reports_unique_constraints_as_indexes = True
__only_on__ = 'mysql'
def test_removed_idx_index_named_as_column(self):
@ -640,7 +655,6 @@ class MySQLUniqueIndexTest(AutogenerateUniqueIndexTest):
assert False, "unexpected success"
class NoUqReflectionIndexTest(NoUqReflection, AutogenerateUniqueIndexTest):
reports_unique_constraints = False
__only_on__ = 'sqlite'
@ -765,6 +779,7 @@ class IncludeHooksTest(AutogenFixtureTest, TestBase):
eq_(len(diffs), 1)
@config.requirements.unique_constraint_reflection
@config.requirements.reflects_unique_constraints_unambiguously
def test_remove_connection_uq(self):
m1 = MetaData()
m2 = MetaData()