rewrite of schemadiff internals

This commit is contained in:
chrisw
2010-09-10 14:43:56 +01:00
parent 20a58b6e02
commit 91e887b081
7 changed files with 485 additions and 376 deletions

View File

@@ -1,13 +1,172 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
from migrate.exceptions import *
from migrate.versioning.genmodel import *
import sqlalchemy
from sqlalchemy import *
from nose.tools import eq_
from migrate.versioning import genmodel, schemadiff
from migrate.changeset import schema, SQLA_06
from migrate.tests import fixture
class TestModelGenerator(fixture.Pathed, fixture.DB):
level = fixture.DB.TXN
class TestSchemaDiff(fixture.DB):
table_name = 'tmp_schemadiff'
level = fixture.DB.CONNECT
def _setup(self, url):
super(TestSchemaDiff, self)._setup(url)
self.meta = MetaData(self.engine, reflect=True)
self.meta.drop_all() # in case junk tables are lying around in the test database
self.meta = MetaData(self.engine, reflect=True) # needed if we just deleted some tables
self.table = Table(self.table_name, self.meta,
Column('id',Integer(), primary_key=True),
Column('name', UnicodeText()),
Column('data', UnicodeText()),
)
def _teardown(self):
if self.table.exists():
self.meta = MetaData(self.engine, reflect=True)
self.meta.drop_all()
super(TestSchemaDiff, self)._teardown()
def _applyLatestModel(self):
diff = schemadiff.getDiffOfModelAgainstDatabase(self.meta, self.engine, excludeTables=['migrate_version'])
genmodel.ModelGenerator(diff,self.engine).applyModel()
@fixture.usedb()
def test_functional(self):
def assertDiff(isDiff, tablesMissingInDatabase, tablesMissingInModel, tablesWithDiff):
diff = schemadiff.getDiffOfModelAgainstDatabase(self.meta, self.engine, excludeTables=['migrate_version'])
eq_(bool(diff), isDiff)
eq_(
(diff.tables_missing_from_B,
diff.tables_missing_from_A,
diff.tables_different.keys()),
(tablesMissingInDatabase,
tablesMissingInModel,
tablesWithDiff)
)
# Model is defined but database is empty.
assertDiff(True, [self.table_name], [], [])
# Check Python upgrade and downgrade of database from updated model.
diff = schemadiff.getDiffOfModelAgainstDatabase(self.meta, self.engine, excludeTables=['migrate_version'])
decls, upgradeCommands, downgradeCommands = genmodel.ModelGenerator(diff,self.engine).toUpgradeDowngradePython()
self.assertEqualsIgnoreWhitespace(decls, '''
from migrate.changeset import schema
meta = MetaData()
tmp_schemadiff = Table('tmp_schemadiff', meta,
Column('id', Integer(), primary_key=True, nullable=False),
Column('name', UnicodeText(length=None)),
Column('data', UnicodeText(length=None)),
)
''')
self.assertEqualsIgnoreWhitespace(upgradeCommands,
'''meta.bind = migrate_engine
tmp_schemadiff.create()''')
self.assertEqualsIgnoreWhitespace(downgradeCommands,
'''meta.bind = migrate_engine
tmp_schemadiff.drop()''')
# Create table in database, now model should match database.
self._applyLatestModel()
assertDiff(False, [], [], [])
# Check Python code gen from database.
diff = schemadiff.getDiffOfModelAgainstDatabase(MetaData(), self.engine, excludeTables=['migrate_version'])
src = genmodel.ModelGenerator(diff,self.engine).toPython()
exec src in locals()
c1 = Table('tmp_schemadiff', self.meta, autoload=True).c
c2 = tmp_schemadiff.c
self.compare_columns_equal(c1, c2, ['type'])
# TODO: get rid of ignoring type
if not self.engine.name == 'oracle':
# Add data, later we'll make sure it's still present.
result = self.engine.execute(self.table.insert(), id=1, name=u'mydata')
if SQLA_06:
dataId = result.inserted_primary_key[0]
else:
dataId = result.last_inserted_ids()[0]
# Modify table in model (by removing it and adding it back to model) -- drop column data and add column data2.
self.meta.remove(self.table)
self.table = Table(self.table_name,self.meta,
Column('id',Integer(),primary_key=True),
Column('name',UnicodeText(length=None)),
Column('data2',Integer(),nullable=True),
)
assertDiff(True, [], [], [self.table_name])
# Apply latest model changes and find no more diffs.
self._applyLatestModel()
assertDiff(False, [], [], [])
if not self.engine.name == 'oracle':
# Make sure data is still present.
result = self.engine.execute(self.table.select(self.table.c.id==dataId))
rows = result.fetchall()
eq_(len(rows), 1)
eq_(rows[0].name, 'mydata')
# Add data, later we'll make sure it's still present.
result = self.engine.execute(self.table.insert(), id=2, name=u'mydata2', data2=123)
if SQLA_06:
dataId2 = result.inserted_primary_key[0]
else:
dataId2 = result.last_inserted_ids()[0]
# Change column type in model.
self.meta.remove(self.table)
self.table = Table(self.table_name,self.meta,
Column('id',Integer(),primary_key=True),
Column('name',UnicodeText(length=None)),
Column('data2',String(255),nullable=True),
)
# XXX test type diff
return
assertDiff(True, [], [], [self.table_name])
# Apply latest model changes and find no more diffs.
self._applyLatestModel()
assertDiff(False, [], [], [])
if not self.engine.name == 'oracle':
# Make sure data is still present.
result = self.engine.execute(self.table.select(self.table.c.id==dataId2))
rows = result.fetchall()
self.assertEquals(len(rows), 1)
self.assertEquals(rows[0].name, 'mydata2')
self.assertEquals(rows[0].data2, '123')
# Delete data, since we're about to make a required column.
# Not even using sqlalchemy.PassiveDefault helps because we're doing explicit column select.
self.engine.execute(self.table.delete(), id=dataId)
if not self.engine.name == 'firebird':
# Change column nullable in model.
self.meta.remove(self.table)
self.table = Table(self.table_name,self.meta,
Column('id',Integer(),primary_key=True),
Column('name',UnicodeText(length=None)),
Column('data2',String(255),nullable=False),
)
assertDiff(True, [], [], [self.table_name]) # TODO test nullable diff
# Apply latest model changes and find no more diffs.
self._applyLatestModel()
assertDiff(False, [], [], [])
# Remove table from model.
self.meta.remove(self.table)
assertDiff(True, [], [self.table_name], [])

View File

@@ -2,175 +2,138 @@
import os
import sqlalchemy
from sqlalchemy import *
from nose.tools import eq_
from migrate.versioning import genmodel, schemadiff
from migrate.changeset import schema, SQLA_06
from migrate.versioning import schemadiff
from migrate.changeset import SQLA_06
from migrate.tests import fixture
class Test_getDiffOfModelAgainstDatabase(fixture.DB):
class TestSchemaDiff(fixture.DB):
table_name = 'tmp_schemadiff'
level = fixture.DB.CONNECT
def _setup(self, url):
super(TestSchemaDiff, self)._setup(url)
self.meta = MetaData(self.engine, reflect=True)
self.meta.drop_all() # in case junk tables are lying around in the test database
self.meta = MetaData(self.engine, reflect=True) # needed if we just deleted some tables
self.table = Table(self.table_name, self.meta,
def _make_table(self,*cols,**kw):
self.table = Table('xtable', self.meta,
Column('id',Integer(), primary_key=True),
Column('name', UnicodeText()),
Column('data', UnicodeText()),
*cols
)
def _teardown(self):
if self.table.exists():
self.meta = MetaData(self.engine, reflect=True)
self.meta.drop_all()
super(TestSchemaDiff, self)._teardown()
def _applyLatestModel(self):
diff = schemadiff.getDiffOfModelAgainstDatabase(self.meta, self.engine, excludeTables=['migrate_version'])
genmodel.ModelGenerator(diff).applyModel()
# TODO: support for diff/generator to extract differences between columns
#@fixture.usedb()
#def test_type_diff(self):
#"""Basic test for correct type diff"""
#self.table.create()
#self.meta = MetaData(self.engine)
#self.table = Table(self.table_name, self.meta,
#Column('id', Integer(), primary_key=True),
#Column('name', Unicode(45)),
#Column('data', Integer),
#)
#diff = schemadiff.getDiffOfModelAgainstDatabase\
#(self.meta, self.engine, excludeTables=['migrate_version'])
if kw.get('create',True):
self.table.create()
def _run_diff(self,**kw):
return schemadiff.getDiffOfModelAgainstDatabase(
self.meta, self.engine, **kw
)
@fixture.usedb()
def test_getDiffOfModelAgainstDatabase_table_missing_in_db(self):
self._make_table(create=False)
diff = self._run_diff()
self.assertTrue(diff)
eq_('Schema diffs:\n tables missing from database: xtable',
str(diff))
@fixture.usedb()
def test_rundiffs(self):
def test_getDiffOfModelAgainstDatabase_table_missing_in_model(self):
self._make_table()
self.meta.clear()
diff = self._run_diff()
self.assertTrue(diff)
eq_('Schema diffs:\n tables missing from model: xtable',
str(diff))
def assertDiff(isDiff, tablesMissingInDatabase, tablesMissingInModel, tablesWithDiff):
diff = schemadiff.getDiffOfModelAgainstDatabase(self.meta, self.engine, excludeTables=['migrate_version'])
eq_(bool(diff), isDiff)
eq_( ([t.name for t in diff.tablesMissingInDatabase], [t.name for t in diff.tablesMissingInModel], [t.name for t in diff.tablesWithDiff]),
(tablesMissingInDatabase, tablesMissingInModel, tablesWithDiff) )
# Model is defined but database is empty.
assertDiff(True, [self.table_name], [], [])
# Check Python upgrade and downgrade of database from updated model.
diff = schemadiff.getDiffOfModelAgainstDatabase(self.meta, self.engine, excludeTables=['migrate_version'])
decls, upgradeCommands, downgradeCommands = genmodel.ModelGenerator(diff).toUpgradeDowngradePython()
self.assertEqualsIgnoreWhitespace(decls, '''
from migrate.changeset import schema
meta = MetaData()
tmp_schemadiff = Table('tmp_schemadiff', meta,
Column('id', Integer(), primary_key=True, nullable=False),
Column('name', UnicodeText(length=None)),
Column('data', UnicodeText(length=None)),
)
''')
self.assertEqualsIgnoreWhitespace(upgradeCommands,
'''meta.bind = migrate_engine
tmp_schemadiff.create()''')
self.assertEqualsIgnoreWhitespace(downgradeCommands,
'''meta.bind = migrate_engine
tmp_schemadiff.drop()''')
# Create table in database, now model should match database.
self._applyLatestModel()
assertDiff(False, [], [], [])
# Check Python code gen from database.
diff = schemadiff.getDiffOfModelAgainstDatabase(MetaData(), self.engine, excludeTables=['migrate_version'])
src = genmodel.ModelGenerator(diff).toPython()
exec src in locals()
c1 = Table('tmp_schemadiff', self.meta, autoload=True).c
c2 = tmp_schemadiff.c
self.compare_columns_equal(c1, c2, ['type'])
# TODO: get rid of ignoring type
if not self.engine.name == 'oracle':
# Add data, later we'll make sure it's still present.
result = self.engine.execute(self.table.insert(), id=1, name=u'mydata')
if SQLA_06:
dataId = result.inserted_primary_key[0]
else:
dataId = result.last_inserted_ids()[0]
# Modify table in model (by removing it and adding it back to model) -- drop column data and add column data2.
self.meta.remove(self.table)
self.table = Table(self.table_name,self.meta,
Column('id',Integer(),primary_key=True),
Column('name',UnicodeText(length=None)),
Column('data2',Integer(),nullable=True),
)
assertDiff(True, [], [], [self.table_name])
# Apply latest model changes and find no more diffs.
self._applyLatestModel()
assertDiff(False, [], [], [])
if not self.engine.name == 'oracle':
# Make sure data is still present.
result = self.engine.execute(self.table.select(self.table.c.id==dataId))
rows = result.fetchall()
eq_(len(rows), 1)
eq_(rows[0].name, 'mydata')
# Add data, later we'll make sure it's still present.
result = self.engine.execute(self.table.insert(), id=2, name=u'mydata2', data2=123)
if SQLA_06:
dataId2 = result.inserted_primary_key[0]
else:
dataId2 = result.last_inserted_ids()[0]
# Change column type in model.
self.meta.remove(self.table)
self.table = Table(self.table_name,self.meta,
Column('id',Integer(),primary_key=True),
Column('name',UnicodeText(length=None)),
Column('data2',String(255),nullable=True),
)
assertDiff(True, [], [], [self.table_name]) # TODO test type diff
# Apply latest model changes and find no more diffs.
self._applyLatestModel()
assertDiff(False, [], [], [])
if not self.engine.name == 'oracle':
# Make sure data is still present.
result = self.engine.execute(self.table.select(self.table.c.id==dataId2))
rows = result.fetchall()
self.assertEquals(len(rows), 1)
self.assertEquals(rows[0].name, 'mydata2')
self.assertEquals(rows[0].data2, '123')
# Delete data, since we're about to make a required column.
# Not even using sqlalchemy.PassiveDefault helps because we're doing explicit column select.
self.engine.execute(self.table.delete(), id=dataId)
if not self.engine.name == 'firebird':
# Change column nullable in model.
self.meta.remove(self.table)
self.table = Table(self.table_name,self.meta,
Column('id',Integer(),primary_key=True),
Column('name',UnicodeText(length=None)),
Column('data2',String(255),nullable=False),
@fixture.usedb()
def test_getDiffOfModelAgainstDatabase_column_missing_in_db(self):
# db
Table('xtable', self.meta,
Column('id',Integer(), primary_key=True),
).create()
self.meta.clear()
# model
self._make_table(
Column('xcol',Integer()),
create=False
)
assertDiff(True, [], [], [self.table_name]) # TODO test nullable diff
# Apply latest model changes and find no more diffs.
self._applyLatestModel()
assertDiff(False, [], [], [])
# Remove table from model.
self.meta.remove(self.table)
assertDiff(True, [], [self.table_name], [])
# run diff
diff = self._run_diff()
self.assertTrue(diff)
eq_('Schema diffs:\n xtable missing columns from database: xcol',
str(diff))
@fixture.usedb()
def test_getDiffOfModelAgainstDatabase_column_missing_in_model(self):
# db
self._make_table(
Column('xcol',Integer()),
)
self.meta.clear()
# model
self._make_table(
create=False
)
# run diff
diff = self._run_diff()
self.assertTrue(diff)
eq_('Schema diffs:\n xtable missing columns from model: xcol',
str(diff))
@fixture.usedb()
def test_getDiffOfModelAgainstDatabase_exclude_tables(self):
# db
Table('ytable', self.meta,
Column('id',Integer(), primary_key=True),
).create()
Table('ztable', self.meta,
Column('id',Integer(), primary_key=True),
).create()
self.meta.clear()
# model
self._make_table(
create=False
)
Table('ztable', self.meta,
Column('id',Integer(), primary_key=True),
)
# run diff
diff = self._run_diff(excludeTables=('xtable','ytable'))
# ytable only in database
# xtable only in model
# ztable identical on both
# ...so we expect no diff!
self.assertFalse(diff)
eq_('No schema diffs',str(diff))
@fixture.usedb()
def test_getDiffOfModelAgainstDatabase_identical_just_pk(self):
self._make_table()
diff = self._run_diff()
self.assertFalse(diff)
eq_('No schema diffs',str(diff))
@fixture.usedb()
def test_getDiffOfModelAgainstDatabase_integer_identical(self):
self._make_table(
Column('data', Integer()),
)
diff = self._run_diff()
eq_('No schema diffs',str(diff))
self.assertFalse(diff)
@fixture.usedb()
def test_getDiffOfModelAgainstDatabase_string_identical(self):
self._make_table(
Column('data', String(10)),
)
diff = self._run_diff()
eq_('No schema diffs',str(diff))
self.assertFalse(diff)
@fixture.usedb()
def test_getDiffOfModelAgainstDatabase_text_identical(self):
self._make_table(
Column('data', Text(255)),
)
diff = self._run_diff()
eq_('No schema diffs',str(diff))
self.assertFalse(diff)

View File

@@ -479,14 +479,14 @@ class TestShellDatabase(Shell, DB):
# Model is defined but database is empty.
result = self.env.run('migrate compare_model_to_db %s %s --model=%s' \
% (self.url, repos_path, model_module))
self.assert_("tables missing in database: tmp_account_rundiffs" in result.stdout)
self.assert_("tables missing from database: tmp_account_rundiffs" in result.stdout)
# Test Deprecation
result = self.env.run('migrate compare_model_to_db %s %s --model=%s' \
% (self.url, repos_path, model_module.replace(":", ".")), expect_error=True)
self.assertEqual(result.returncode, 0)
self.assertTrue("DeprecationWarning" in result.stderr)
self.assert_("tables missing in database: tmp_account_rundiffs" in result.stdout)
self.assert_("tables missing from database: tmp_account_rundiffs" in result.stdout)
# Update db to latest model.
result = self.env.run('migrate update_db_from_model %s %s %s'\

View File

@@ -35,8 +35,9 @@ Base = declarative.declarative_base()
class ModelGenerator(object):
def __init__(self, diff, declarative=False):
def __init__(self, diff, engine, declarative=False):
self.diff = diff
self.engine = engine
self.declarative = declarative
def column_repr(self, col):
@@ -111,6 +112,17 @@ class ModelGenerator(object):
out.append(")")
return out
def _get_tables(self,missingA=False,missingB=False,modified=False):
to_process = []
for bool_,names,metadata in (
(missingA,self.diff.tables_missing_from_A,self.diff.metadataB),
(missingB,self.diff.tables_missing_from_B,self.diff.metadataA),
(modified,self.diff.tables_different,self.diff.metadataA),
):
if bool_:
for name in names:
yield metadata.tables.get(name)
def toPython(self):
"""Assume database is current and model is empty."""
out = []
@@ -119,7 +131,7 @@ class ModelGenerator(object):
else:
out.append(HEADER)
out.append("")
for table in self.diff.tablesMissingInModel:
for table in self._get_tables(missingA=True):
out.extend(self.getTableDefn(table))
out.append("")
return '\n'.join(out)
@@ -128,25 +140,22 @@ class ModelGenerator(object):
''' Assume model is most current and database is out-of-date. '''
decls = ['from migrate.changeset import schema',
'meta = MetaData()']
for table in self.diff.tablesMissingInModel + \
self.diff.tablesMissingInDatabase + \
self.diff.tablesWithDiff:
for table in self._get_tables(
missingA=True,missingB=True,modified=True
):
decls.extend(self.getTableDefn(table))
upgradeCommands, downgradeCommands = [], []
for table in self.diff.tablesMissingInModel:
tableName = table.name
for tableName in self.diff.tables_missing_from_A:
upgradeCommands.append("%(table)s.drop()" % {'table': tableName})
downgradeCommands.append("%(table)s.create()" % \
{'table': tableName})
for table in self.diff.tablesMissingInDatabase:
tableName = table.name
for tableName in self.diff.tables_missing_from_B:
upgradeCommands.append("%(table)s.create()" % {'table': tableName})
downgradeCommands.append("%(table)s.drop()" % {'table': tableName})
for modelTable in self.diff.tablesWithDiff:
dbTable = self.diff.reflected_model.tables[modelTable.name]
tableName = modelTable.name
for tableName in self.diff.tables_different:
dbTable = self.diff.metadataB.tables[tableName]
missingInDatabase, missingInModel, diffDecl = \
self.diff.colDiffs[tableName]
for col in missingInDatabase:
@@ -173,38 +182,40 @@ class ModelGenerator(object):
'\n'.join([pre_command] + ['%s%s' % (indent, line) for line in upgradeCommands]),
'\n'.join([pre_command] + ['%s%s' % (indent, line) for line in downgradeCommands]))
def _db_can_handle_this_change(self,td):
if (td.columns_missing_from_B
and not td.columns_missing_from_A
and not td.columns_different):
# Even sqlite can handle this.
return True
else:
return not self.engine.url.drivername.startswith('sqlite')
def applyModel(self):
"""Apply model to current database."""
def dbCanHandleThisChange(missingInDatabase, missingInModel, diffDecl):
if missingInDatabase and not missingInModel and not diffDecl:
# Even sqlite can handle this.
return True
else:
return not self.diff.conn.url.drivername.startswith('sqlite')
meta = sqlalchemy.MetaData(self.engine)
meta = sqlalchemy.MetaData(self.diff.conn.engine)
for table in self.diff.tablesMissingInModel:
for table in self._get_tables(missingA=True):
table = table.tometadata(meta)
table.drop()
for table in self.diff.tablesMissingInDatabase:
for table in self._get_tables(missingB=True):
table = table.tometadata(meta)
table.create()
for modelTable in self.diff.tablesWithDiff:
modelTable = modelTable.tometadata(meta)
dbTable = self.diff.reflected_model.tables[modelTable.name]
for modelTable in self._get_tables(modified=True):
tableName = modelTable.name
missingInDatabase, missingInModel, diffDecl = \
self.diff.colDiffs[tableName]
if dbCanHandleThisChange(missingInDatabase, missingInModel,
diffDecl):
for col in missingInDatabase:
modelTable.columns[col.name].create()
for col in missingInModel:
dbTable.columns[col.name].drop()
for modelCol, databaseCol, modelDecl, databaseDecl in diffDecl:
databaseCol.alter(modelCol)
modelTable = modelTable.tometadata(meta)
dbTable = self.diff.metadataB.tables[tableName]
td = self.diff.tables_different[tableName]
if self._db_can_handle_this_change(td):
for col in td.columns_missing_from_B:
modelTable.columns[col].create()
for col in td.columns_missing_from_A:
dbTable.columns[col].drop()
# XXX handle column changes here.
else:
# Sqlite doesn't support drop column, so you have to
# do more: create temp table, copy data to it, drop
@@ -214,7 +225,7 @@ class ModelGenerator(object):
tempName = '_temp_%s' % modelTable.name
def getCopyStatement():
preparer = self.diff.conn.engine.dialect.preparer
preparer = self.engine.dialect.preparer
commonCols = []
for modelCol in modelTable.columns:
if modelCol.name in dbTable.columns:
@@ -225,7 +236,7 @@ class ModelGenerator(object):
# Move the data in one transaction, so that we don't
# leave the database in a nasty state.
connection = self.diff.conn.connect()
connection = self.engine.connect()
trans = connection.begin()
try:
connection.execute(

View File

@@ -108,8 +108,9 @@ class ControlledSchema(object):
model = load_model(model)
diff = schemadiff.getDiffOfModelAgainstDatabase(
model, self.engine, excludeTables=[self.repository.version_table])
genmodel.ModelGenerator(diff).applyModel()
model, self.engine, excludeTables=[self.repository.version_table]
)
genmodel.ModelGenerator(diff,self.engine).applyModel()
self.update_repository_table(self.version, int(self.repository.latest))
@@ -207,5 +208,6 @@ class ControlledSchema(object):
repository = Repository(repository)
diff = schemadiff.getDiffOfModelAgainstDatabase(
MetaData(), engine, excludeTables=[repository.version_table])
return genmodel.ModelGenerator(diff, declarative).toPython()
MetaData(), engine, excludeTables=[repository.version_table]
)
return genmodel.ModelGenerator(diff, engine, declarative).toPython()

View File

@@ -9,182 +9,176 @@ from migrate.changeset import SQLA_06
log = logging.getLogger(__name__)
def getDiffOfModelAgainstDatabase(model, conn, excludeTables=None):
def getDiffOfModelAgainstDatabase(metadata, engine, excludeTables=None):
"""
Return differences of model against database.
:return: object which will evaluate to :keyword:`True` if there \
are differences else :keyword:`False`.
"""
return SchemaDiff(model, conn, excludeTables)
return SchemaDiff(metadata,
sqlalchemy.MetaData(engine, reflect=True),
labelA='model',
labelB='database',
excludeTables=excludeTables)
def getDiffOfModelAgainstModel(oldmodel, model, conn, excludeTables=None):
def getDiffOfModelAgainstModel(metadataA, metadataB, excludeTables=None):
"""
Return differences of model against another model.
:return: object which will evaluate to :keyword:`True` if there \
are differences else :keyword:`False`.
"""
return SchemaDiff(model, conn, excludeTables, oldmodel=oldmodel)
return SchemaDiff(metadataA, metadataB, excludeTables)
class TableDiff(object):
"""
Container for differences in one :class:`~sqlalchemy.schema.Table
between two :class:`~sqlalchemy.schema.MetaData` instances, ``A``
and ``B``.
.. attribute:: columns_missing_from_A
A sequence of column names that were found in B but weren't in
A.
.. attribute:: columns_missing_from_B
A sequence of column names that were found in A but weren't in
B.
.. attribute:: columns_different
An empty dictionary, for future use...
"""
__slots__ = (
'columns_missing_from_A',
'columns_missing_from_B',
'columns_different',
)
def __len__(self):
return (
len(self.columns_missing_from_A)+
len(self.columns_missing_from_B)+
len(self.columns_different)
)
class SchemaDiff(object):
"""
Differences of model against database.
Compute the difference between two :class:`~sqlalchemy.schema.MetaData`
objects.
The string representation of a :class:`SchemaDiff` will summarise
the changes found between the two
:class:`~sqlalchemy.schema.MetaData` objects.
The length of a :class:`SchemaDiff` will give the number of
changes found, enabling it to be used much like a boolean in
expressions.
:param metadataA:
First :class:`~sqlalchemy.schema.MetaData` to compare.
:param metadataB:
Second :class:`~sqlalchemy.schema.MetaData` to compare.
:param labelA:
The label to use in messages about the first
:class:`~sqlalchemy.schema.MetaData`.
:param labelB:
The label to use in messages about the second
:class:`~sqlalchemy.schema.MetaData`.
:param excludeTables:
A sequence of table names to exclude.
"""
def __init__(self, model, conn, excludeTables=None, oldmodel=None):
"""
:param model: Python model's metadata
:param conn: active database connection.
"""
self.model = model
self.conn = conn
if not excludeTables:
# [] can't be default value in Python parameter
excludeTables = []
self.excludeTables = excludeTables
if oldmodel:
self.reflected_model = oldmodel
else:
self.reflected_model = sqlalchemy.MetaData(conn, reflect=True)
self.tablesMissingInDatabase, self.tablesMissingInModel, \
self.tablesWithDiff = [], [], []
self.colDiffs = {}
self.compareModelToDatabase()
def __init__(self,
metadataA, metadataB,
labelA='metadataA',
labelB='metadataB',
excludeTables=None):
def compareModelToDatabase(self):
"""
Do actual comparison.
"""
# Setup common variables.
cc = self.conn.contextual_connect()
if SQLA_06:
from sqlalchemy.ext import compiler
from sqlalchemy.schema import DDLElement
class DefineColumn(DDLElement):
def __init__(self, col):
self.col = col
self.metadataA, self.metadataB = metadataA, metadataB
self.labelA, self.labelB = labelA, labelB
excludeTables = set(excludeTables or [])
A_table_names = set(metadataA.tables.keys())
B_table_names = set(metadataB.tables.keys())
self.tables_missing_from_A = sorted(
B_table_names - A_table_names - excludeTables
)
self.tables_missing_from_B = sorted(
A_table_names - B_table_names - excludeTables
)
self.tables_different = {}
for table_name in A_table_names.intersection(B_table_names):
td = TableDiff()
@compiler.compiles(DefineColumn)
def compile(elem, compiler, **kw):
return compiler.get_column_specification(elem.col)
A_table = metadataA.tables[table_name]
B_table = metadataB.tables[table_name]
def get_column_specification(col):
return str(DefineColumn(col).compile(dialect=self.conn.dialect))
else:
schemagenerator = self.conn.dialect.schemagenerator(
self.conn.dialect, cc)
def get_column_specification(col):
return schemagenerator.get_column_specification(col)
# For each in model, find missing in database.
for modelName, modelTable in self.model.tables.items():
if modelName in self.excludeTables:
continue
reflectedTable = self.reflected_model.tables.get(modelName, None)
if reflectedTable is not None:
# Table exists.
pass
else:
self.tablesMissingInDatabase.append(modelTable)
A_column_names = set(A_table.columns.keys())
B_column_names = set(B_table.columns.keys())
# For each in database, find missing in model.
for reflectedName, reflectedTable in \
self.reflected_model.tables.items():
if reflectedName in self.excludeTables:
continue
modelTable = self.model.tables.get(reflectedName, None)
if modelTable is not None:
# Table exists.
td.columns_missing_from_A = sorted(
B_column_names - A_column_names
)
td.columns_missing_from_B = sorted(
A_column_names - B_column_names
)
td.columns_different = {}
# Find missing columns in database.
for modelCol in modelTable.columns:
databaseCol = reflectedTable.columns.get(modelCol.name,
None)
if databaseCol is not None:
pass
else:
self.storeColumnMissingInDatabase(modelTable, modelCol)
# 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)
# XXX - index and constraint differences should
# be checked for here
# Find missing columns in model.
for databaseCol in reflectedTable.columns:
# TODO: no test coverage here? (mrb)
modelCol = modelTable.columns.get(databaseCol.name, None)
if modelCol is not None:
# Compare attributes of column.
modelDecl = \
get_column_specification(modelCol)
databaseDecl = \
get_column_specification(databaseCol)
if modelDecl != databaseDecl:
# Unfortunately, sometimes the database
# decl won't quite match the model, even
# though they're the same.
mc, dc = modelCol.type.__class__, \
databaseCol.type.__class__
if (issubclass(mc, dc) \
or issubclass(dc, mc)) \
and modelCol.nullable == \
databaseCol.nullable:
# Types and nullable are the same.
pass
else:
self.storeColumnDiff(
modelTable, modelCol, databaseCol,
modelDecl, databaseDecl)
else:
self.storeColumnMissingInModel(modelTable, databaseCol)
else:
self.tablesMissingInModel.append(reflectedTable)
if td:
self.tables_different[table_name]=td
def __str__(self):
''' Summarize differences. '''
def colDiffDetails():
colout = []
for table in self.tablesWithDiff:
tableName = table.name
missingInDatabase, missingInModel, diffDecl = \
self.colDiffs[tableName]
if missingInDatabase:
colout.append(
' %s missing columns in database: %s' % \
(tableName, ', '.join(
[col.name for col in missingInDatabase])))
if missingInModel:
colout.append(
' %s missing columns in model: %s' % \
(tableName, ', '.join(
[col.name for col in missingInModel])))
if diffDecl:
colout.append(
' %s with different declaration of columns\
in database: %s' % (tableName, str(diffDecl)))
return colout
out = []
if self.tablesMissingInDatabase:
out.append(
' tables missing in database: %s' % \
', '.join(
[table.name for table in self.tablesMissingInDatabase]))
if self.tablesMissingInModel:
out.append(
' tables missing in model: %s' % \
', '.join(
[table.name for table in self.tablesMissingInModel]))
if self.tablesWithDiff:
out.append(
' tables with differences: %s' % \
', '.join([table.name for table in self.tablesWithDiff]))
for names,label in (
(self.tables_missing_from_A,self.labelA),
(self.tables_missing_from_B,self.labelB),
):
if names:
out.append(
' tables missing from %s: %s' % (
label,', '.join(sorted(names))
)
)
for name,td in sorted(self.tables_different.items()):
for names,label in (
(td.columns_missing_from_A,self.labelA),
(td.columns_missing_from_B,self.labelB),
):
if names:
out.append(
' %s missing columns from %s: %s' % (
name, label,', '.join(sorted(names))
)
)
if out:
out.insert(0, 'Schema diffs:')
out.extend(colDiffDetails())
return '\n'.join(out)
else:
return 'No schema diffs'
@@ -193,27 +187,8 @@ class SchemaDiff(object):
"""
Used in bool evaluation, return of 0 means no diffs.
"""
return len(self.tablesMissingInDatabase) + \
len(self.tablesMissingInModel) + len(self.tablesWithDiff)
def storeColumnMissingInDatabase(self, table, col):
if table not in self.tablesWithDiff:
self.tablesWithDiff.append(table)
missingInDatabase, missingInModel, diffDecl = \
self.colDiffs.setdefault(table.name, ([], [], []))
missingInDatabase.append(col)
def storeColumnMissingInModel(self, table, col):
if table not in self.tablesWithDiff:
self.tablesWithDiff.append(table)
missingInDatabase, missingInModel, diffDecl = \
self.colDiffs.setdefault(table.name, ([], [], []))
missingInModel.append(col)
def storeColumnDiff(self, table, modelCol, databaseCol, modelDecl,
databaseDecl):
if table not in self.tablesWithDiff:
self.tablesWithDiff.append(table)
missingInDatabase, missingInModel, diffDecl = \
self.colDiffs.setdefault(table.name, ([], [], []))
diffDecl.append((modelCol, databaseCol, modelDecl, databaseDecl))
return (
len(self.tables_missing_from_A) +
len(self.tables_missing_from_B) +
len(self.tables_different)
)

View File

@@ -62,11 +62,10 @@ class PythonScript(base.BaseScript):
diff = schemadiff.getDiffOfModelAgainstModel(
oldmodel,
model,
engine,
excludeTables=[repository.version_table])
# TODO: diff can be False (there is no difference?)
decls, upgradeCommands, downgradeCommands = \
genmodel.ModelGenerator(diff).toUpgradeDowngradePython()
genmodel.ModelGenerator(diff,engine).toUpgradeDowngradePython()
# Store differences into file.
src = Template(opts.pop('templates_path', None)).get_script(opts.pop('templates_theme', None))