rewrite of schemadiff internals
This commit is contained in:
@@ -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], [])
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'\
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user