From 82c5fcd58c280a09acc29031f93b3282d18b9199 Mon Sep 17 00:00:00 2001 From: chrisw Date: Wed, 15 Sep 2010 23:36:11 +0100 Subject: [PATCH] implement column type diff'ing --- migrate/tests/versioning/test_schemadiff.py | 87 +++++++++++++-- migrate/versioning/schemadiff.py | 118 +++++++++++++++++--- 2 files changed, 180 insertions(+), 25 deletions(-) diff --git a/migrate/tests/versioning/test_schemadiff.py b/migrate/tests/versioning/test_schemadiff.py index 0cf60d3..51aaf80 100644 --- a/migrate/tests/versioning/test_schemadiff.py +++ b/migrate/tests/versioning/test_schemadiff.py @@ -10,7 +10,7 @@ from migrate.changeset import SQLA_06 from migrate.tests import fixture -class Test_getDiffOfModelAgainstDatabase(fixture.DB): +class SchemaDiffBase(fixture.DB): level = fixture.DB.CONNECT @@ -22,12 +22,35 @@ class Test_getDiffOfModelAgainstDatabase(fixture.DB): if kw.get('create',True): self.table.create() + def _assert_diff(self,col_A,col_B): + self._make_table(col_A) + self.meta.clear() + self._make_table(col_B,create=False) + diff = self._run_diff() + # print diff + self.assertTrue(diff) + eq_(1,len(diff.tables_different)) + td = diff.tables_different.values()[0] + eq_(1,len(td.columns_different)) + cd = td.columns_different.values()[0] + eq_(('Schema diffs:\n' + ' table with differences: xtable\n' + ' column with differences: data\n' + ' model: %r\n' + ' database: %r')%( + cd.col_A, + cd.col_B + ),str(diff)) + +class Test_getDiffOfModelAgainstDatabase(SchemaDiffBase): + def _run_diff(self,**kw): return schemadiff.getDiffOfModelAgainstDatabase( self.meta, self.engine, **kw ) + @fixture.usedb() - def test_getDiffOfModelAgainstDatabase_table_missing_in_db(self): + def test_table_missing_in_db(self): self._make_table(create=False) diff = self._run_diff() self.assertTrue(diff) @@ -35,7 +58,7 @@ class Test_getDiffOfModelAgainstDatabase(fixture.DB): str(diff)) @fixture.usedb() - def test_getDiffOfModelAgainstDatabase_table_missing_in_model(self): + def test_table_missing_in_model(self): self._make_table() self.meta.clear() diff = self._run_diff() @@ -44,7 +67,7 @@ class Test_getDiffOfModelAgainstDatabase(fixture.DB): str(diff)) @fixture.usedb() - def test_getDiffOfModelAgainstDatabase_column_missing_in_db(self): + def test_column_missing_in_db(self): # db Table('xtable', self.meta, Column('id',Integer(), primary_key=True), @@ -64,7 +87,7 @@ class Test_getDiffOfModelAgainstDatabase(fixture.DB): str(diff)) @fixture.usedb() - def test_getDiffOfModelAgainstDatabase_column_missing_in_model(self): + def test_column_missing_in_model(self): # db self._make_table( Column('xcol',Integer()), @@ -83,7 +106,7 @@ class Test_getDiffOfModelAgainstDatabase(fixture.DB): str(diff)) @fixture.usedb() - def test_getDiffOfModelAgainstDatabase_exclude_tables(self): + def test_exclude_tables(self): # db Table('ytable', self.meta, Column('id',Integer(), primary_key=True), @@ -109,14 +132,57 @@ class Test_getDiffOfModelAgainstDatabase(fixture.DB): eq_('No schema diffs',str(diff)) @fixture.usedb() - def test_getDiffOfModelAgainstDatabase_identical_just_pk(self): + def test_identical_just_pk(self): self._make_table() diff = self._run_diff() self.assertFalse(diff) eq_('No schema diffs',str(diff)) + + + @fixture.usedb() + def test_different_type(self): + self._assert_diff( + Column('data', String(10)), + Column('data', Integer()), + ) + + @fixture.usedb() + def test_int_vs_float(self): + self._assert_diff( + Column('data', Integer()), + Column('data', Float()), + ) + + @fixture.usedb() + def test_float_vs_numeric(self): + self._assert_diff( + Column('data', Float()), + Column('data', Numeric()), + ) + + @fixture.usedb() + def test_numeric_precision(self): + self._assert_diff( + Column('data', Numeric(precision=5)), + Column('data', Numeric(precision=6)), + ) + + @fixture.usedb() + def test_numeric_scale(self): + self._assert_diff( + Column('data', Numeric(precision=6,scale=0)), + Column('data', Numeric(precision=6,scale=1)), + ) + + @fixture.usedb() + def test_string_length(self): + self._assert_diff( + Column('data', String(10)), + Column('data', String(20)), + ) @fixture.usedb() - def test_getDiffOfModelAgainstDatabase_integer_identical(self): + def test_integer_identical(self): self._make_table( Column('data', Integer()), ) @@ -125,7 +191,7 @@ class Test_getDiffOfModelAgainstDatabase(fixture.DB): self.assertFalse(diff) @fixture.usedb() - def test_getDiffOfModelAgainstDatabase_string_identical(self): + def test_string_identical(self): self._make_table( Column('data', String(10)), ) @@ -134,10 +200,11 @@ class Test_getDiffOfModelAgainstDatabase(fixture.DB): self.assertFalse(diff) @fixture.usedb() - def test_getDiffOfModelAgainstDatabase_text_identical(self): + def test_text_identical(self): self._make_table( Column('data', Text(255)), ) diff = self._run_diff() eq_('No schema diffs',str(diff)) self.assertFalse(diff) + diff --git a/migrate/versioning/schemadiff.py b/migrate/versioning/schemadiff.py index fa08e7b..17c2d8e 100644 --- a/migrate/versioning/schemadiff.py +++ b/migrate/versioning/schemadiff.py @@ -1,11 +1,12 @@ """ Schema differencing support. """ + import logging - import sqlalchemy -from migrate.changeset import SQLA_06 +from migrate.changeset import SQLA_06 +from sqlalchemy.types import Float log = logging.getLogger(__name__) @@ -33,9 +34,66 @@ def getDiffOfModelAgainstModel(metadataA, metadataB, excludeTables=None): return SchemaDiff(metadataA, metadataB, excludeTables) +class ColDiff(object): + """ + Container for differences in one :class:`~sqlalchemy.schema.Column` + between two :class:`~sqlalchemy.schema.Table` instances, ``A`` + and ``B``. + + .. attribute:: col_A + + The :class:`~sqlalchemy.schema.Column` object for A. + + .. attribute:: col_B + + The :class:`~sqlalchemy.schema.Column` object for B. + + .. attribute:: type_A + + The most generic type of the :class:`~sqlalchemy.schema.Column` + object in A. + + .. attribute:: type_B + + The most generic type of the :class:`~sqlalchemy.schema.Column` + object in A. + + """ + + diff = False + + def __init__(self,col_A,col_B): + self.col_A = col_A + self.col_B = col_B + + self.type_A = col_A.type + self.type_B = col_B.type + + self.affinity_A = self.type_A._type_affinity + self.affinity_B = self.type_B._type_affinity + + if self.affinity_A is not self.affinity_B: + self.diff = True + return + + if isinstance(self.type_A,Float) or isinstance(self.type_B,Float): + if not (isinstance(self.type_A,Float) and isinstance(self.type_B,Float)): + self.diff=True + return + + for attr in ('precision','scale','length'): + A = getattr(self.type_A,attr,None) + B = getattr(self.type_B,attr,None) + if not (A is None or B is None) and A!=B: + self.diff=True + return + + def __nonzero__(self): + return self.diff + class TableDiff(object): """ - Container for differences in one :class:`~sqlalchemy.schema.Table + Container for differences in one :class:`~sqlalchemy.schema.Table` between two :class:`~sqlalchemy.schema.MetaData` instances, ``A`` and ``B``. @@ -51,7 +109,10 @@ class TableDiff(object): .. attribute:: columns_different - An empty dictionary, for future use... + A dictionary containing information about columns that were + found to be different. + It maps column names to a :class:`ColDiff` objects describing the + differences found. """ __slots__ = ( 'columns_missing_from_A', @@ -59,11 +120,11 @@ class TableDiff(object): 'columns_different', ) - def __len__(self): - return ( - len(self.columns_missing_from_A)+ - len(self.columns_missing_from_B)+ - len(self.columns_different) + def __nonzero__(self): + return bool( + self.columns_missing_from_A or + self.columns_missing_from_B or + self.columns_different ) class SchemaDiff(object): @@ -95,6 +156,23 @@ class SchemaDiff(object): :param excludeTables: A sequence of table names to exclude. + + .. attribute:: tables_missing_from_A + + A sequence of table names that were found in B but weren't in + A. + + .. attribute:: tables_missing_from_B + + A sequence of table names that were found in A but weren't in + B. + + .. attribute:: tables_different + + A dictionary containing information about tables that were found + to be different. + It maps table names to a :class:`TableDiff` objects describing the + differences found. """ def __init__(self, @@ -105,6 +183,7 @@ class SchemaDiff(object): self.metadataA, self.metadataB = metadataA, metadataB self.labelA, self.labelB = labelA, labelB + self.label_width = max(len(labelA),len(labelB)) excludeTables = set(excludeTables or []) A_table_names = set(metadataA.tables.keys()) @@ -138,12 +217,16 @@ class SchemaDiff(object): td.columns_different = {} - # XXX - should check columns differences - #for col_name in A_column_names.intersection(B_column_names): - # - # A_col = A_table.columns.get(col_name) - # B_col = B_table.columns.get(col_name) - + for col_name in A_column_names.intersection(B_column_names): + + cd = ColDiff( + A_table.columns.get(col_name), + B_table.columns.get(col_name) + ) + + if cd: + td.columns_different[col_name]=cd + # XXX - index and constraint differences should # be checked for here @@ -153,6 +236,7 @@ class SchemaDiff(object): def __str__(self): ''' Summarize differences. ''' out = [] + column_template =' %%%is: %%r' % self.label_width for names,label in ( (self.tables_missing_from_A,self.labelA), @@ -179,6 +263,10 @@ class SchemaDiff(object): label,', '.join(sorted(names)) ) ) + for name,cd in td.columns_different.items(): + out.append(' column with differences: %s' % name) + out.append(column_template % (self.labelA,cd.col_A)) + out.append(column_template % (self.labelB,cd.col_B)) if out: out.insert(0, 'Schema diffs:')