169 lines
7.3 KiB
Python
169 lines
7.3 KiB
Python
|
|
# Code to generate a Python model from a database or differences between a model and database.
|
|
# Some of this is borrowed heavily from the AutoCode project at: http://code.google.com/p/sqlautocode/
|
|
|
|
import sys
|
|
import migrate, sqlalchemy
|
|
|
|
|
|
HEADER = """
|
|
## File autogenerated by genmodel.py
|
|
|
|
from sqlalchemy import *
|
|
meta = MetaData()
|
|
"""
|
|
|
|
class ModelGenerator(object):
|
|
|
|
def __init__(self, diff):
|
|
self.diff = diff
|
|
dialectModule = sys.modules[self.diff.conn.dialect.__module__] # is there an easier way to get this?
|
|
self.colTypeMappings = dict( (v,k) for k,v in dialectModule.colspecs.items() )
|
|
|
|
def column_repr(self, col):
|
|
kwarg = []
|
|
if col.key != col.name: kwarg.append('key')
|
|
if col.primary_key:
|
|
col.primary_key = True # otherwise it dumps it as 1
|
|
kwarg.append('primary_key')
|
|
if not col.nullable: kwarg.append('nullable')
|
|
if col.onupdate: kwarg.append('onupdate')
|
|
if col.default:
|
|
if col.primary_key:
|
|
# I found that Postgres automatically creates a default value for the sequence, but let's not show that.
|
|
pass
|
|
else:
|
|
kwarg.append('default')
|
|
ks = ', '.join('%s=%r' % (k, getattr(col, k)) for k in kwarg )
|
|
|
|
name = col.name.encode('utf8') # crs: not sure if this is good idea, but it gets rid of extra u''
|
|
type = self.colTypeMappings.get(col.type.__class__, None)
|
|
if type:
|
|
# Make the column type be an instance of this type.
|
|
type = type()
|
|
else:
|
|
# We must already be a model type, no need to map from the database-specific types.
|
|
type = col.type
|
|
|
|
data = {'name' : name,
|
|
'type' : type,
|
|
'constraints' : ', '.join([repr(cn) for cn in col.constraints]),
|
|
'args' : ks and ks or ''
|
|
}
|
|
|
|
if data['constraints']:
|
|
if data['args']: data['args'] = ',' + data['args']
|
|
|
|
if data['constraints'] or data['args']:
|
|
data['maybeComma'] = ','
|
|
else:
|
|
data['maybeComma'] = ''
|
|
|
|
return """Column(%(name)r, %(type)r %(maybeComma)s %(constraints)s %(args)s)""" % data
|
|
|
|
def getTableDefn(self, table):
|
|
out = []
|
|
tableName = table.name
|
|
out.append("%(table)s = Table('%(table)s', meta," % {'table': tableName})
|
|
for col in table.columns:
|
|
out.append(" %s," % self.column_repr(col))
|
|
out.append(")")
|
|
return out
|
|
|
|
def toPython(self):
|
|
''' Assume database is current and model is empty. '''
|
|
out = []
|
|
out.append(HEADER)
|
|
out.append("")
|
|
for table in self.diff.tablesMissingInModel:
|
|
out.extend(self.getTableDefn(table))
|
|
out.append("")
|
|
return '\n'.join(out)
|
|
|
|
def toUpgradeDowngradePython(self, indent=' '):
|
|
''' Assume model is most current and database is out-of-date. '''
|
|
|
|
decls = ['meta = MetaData(migrate_engine)']
|
|
for table in self.diff.tablesMissingInModel + self.diff.tablesMissingInDatabase:
|
|
decls.extend(self.getTableDefn(table))
|
|
|
|
upgradeCommands, downgradeCommands = [], []
|
|
for table in self.diff.tablesMissingInModel:
|
|
tableName = table.name
|
|
upgradeCommands.append("%(table)s.drop()" % {'table': tableName})
|
|
downgradeCommands.append("%(table)s.create()" % {'table': tableName})
|
|
for table in self.diff.tablesMissingInDatabase:
|
|
tableName = table.name
|
|
upgradeCommands.append("%(table)s.create()" % {'table': tableName})
|
|
downgradeCommands.append("%(table)s.drop()" % {'table': tableName})
|
|
|
|
return ('\n'.join(decls),
|
|
'\n'.join(['%s%s' % (indent, line) for line in upgradeCommands]),
|
|
'\n'.join(['%s%s' % (indent, line) for line in downgradeCommands])
|
|
)
|
|
|
|
def applyModel(self):
|
|
''' Apply model to current database. '''
|
|
|
|
# Yuck! We have to import from changeset to apply the monkey-patch to allow column adding/dropping.
|
|
from migrate.changeset import schema
|
|
|
|
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.diff.conn.engine)
|
|
|
|
for table in self.diff.tablesMissingInModel:
|
|
table = table.tometadata(meta)
|
|
table.drop()
|
|
for table in self.diff.tablesMissingInDatabase:
|
|
table = table.tometadata(meta)
|
|
table.create()
|
|
for modelTable in self.diff.tablesWithDiff:
|
|
modelTable = modelTable.tometadata(meta)
|
|
dbTable = self.diff.reflected_model.tables[modelTable.name]
|
|
#print 'TODO DEBUG.cols1', [x.name for x in dbTable.columns]
|
|
#dbTable = dbTable.tometadata(meta)
|
|
#print 'TODO DEBUG.cols2', [x.name for x in dbTable.columns]
|
|
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)
|
|
else:
|
|
# Sqlite doesn't support drop column, so you have to do more:
|
|
# create temp table, copy data to it, drop old table, create new table, copy data back.
|
|
|
|
tempName = '_temp_%s' % modelTable.name # I wonder if this is guaranteed to be unique?
|
|
def getCopyStatement():
|
|
preparer = self.diff.conn.engine.dialect.preparer
|
|
commonCols = []
|
|
for modelCol in modelTable.columns:
|
|
if dbTable.columns.has_key(modelCol.name):
|
|
commonCols.append(modelCol.name)
|
|
commonColsStr = ', '.join(commonCols)
|
|
return 'INSERT INTO %s (%s) SELECT %s FROM %s' % (tableName, commonColsStr, commonColsStr, tempName)
|
|
|
|
# Move the data in one transaction, so that we don't leave the database in a nasty state.
|
|
connection = self.diff.conn.connect()
|
|
trans = connection.begin()
|
|
try:
|
|
connection.execute('CREATE TEMPORARY TABLE %s as SELECT * from %s' % (tempName, modelTable.name))
|
|
modelTable.drop(bind=connection) # make sure the drop takes place inside our transaction with the bind parameter
|
|
modelTable.create(bind=connection)
|
|
connection.execute(getCopyStatement())
|
|
connection.execute('DROP TABLE %s' % tempName)
|
|
trans.commit()
|
|
except:
|
|
trans.rollback()
|
|
raise
|
|
|