Packages update

This commit is contained in:
Ruud
2012-02-11 16:28:06 +01:00
parent 3bbf1126c3
commit 02e01fb2d6
217 changed files with 26395 additions and 21194 deletions
+2
View File
@@ -7,3 +7,5 @@
from migrate.versioning import *
from migrate.changeset import *
__version__ = '0.7.2'
+1 -1
View File
@@ -13,7 +13,7 @@ from sqlalchemy import __version__ as _sa_version
warnings.simplefilter('always', DeprecationWarning)
_sa_version = tuple(int(re.match("\d+", x).group(0)) for x in _sa_version.split("."))
SQLA_06 = _sa_version >= (0, 6)
SQLA_07 = _sa_version >= (0, 7)
del re
del _sa_version
+21 -87
View File
@@ -17,23 +17,19 @@ from sqlalchemy.schema import (ForeignKeyConstraint,
Index)
from migrate import exceptions
from migrate.changeset import constraint, SQLA_06
from migrate.changeset import constraint
if not SQLA_06:
from sqlalchemy.sql.compiler import SchemaGenerator, SchemaDropper
else:
from sqlalchemy.schema import AddConstraint, DropConstraint
from sqlalchemy.sql.compiler import DDLCompiler
SchemaGenerator = SchemaDropper = DDLCompiler
from sqlalchemy.schema import AddConstraint, DropConstraint
from sqlalchemy.sql.compiler import DDLCompiler
SchemaGenerator = SchemaDropper = DDLCompiler
class AlterTableVisitor(SchemaVisitor):
"""Common operations for ``ALTER TABLE`` statements."""
if SQLA_06:
# engine.Compiler looks for .statement
# when it spawns off a new compiler
statement = ClauseElement()
# engine.Compiler looks for .statement
# when it spawns off a new compiler
statement = ClauseElement()
def append(self, s):
"""Append content to the SchemaIterator's query buffer."""
@@ -116,16 +112,15 @@ class ANSIColumnGenerator(AlterTableVisitor, SchemaGenerator):
# SA bounds FK constraints to table, add manually
for fk in column.foreign_keys:
self.add_foreignkey(fk.constraint)
# add primary key constraint if needed
if column.primary_key_name:
cons = constraint.PrimaryKeyConstraint(column,
name=column.primary_key_name)
cons.create()
if SQLA_06:
def add_foreignkey(self, fk):
self.connection.execute(AddConstraint(fk))
def add_foreignkey(self, fk):
self.connection.execute(AddConstraint(fk))
class ANSIColumnDropper(AlterTableVisitor, SchemaDropper):
"""Extends ANSI SQL dropper for column dropping (``ALTER TABLE
@@ -232,10 +227,7 @@ class ANSISchemaChanger(AlterTableVisitor, SchemaGenerator):
def _visit_column_type(self, table, column, delta):
type_ = delta['type']
if SQLA_06:
type_text = str(type_.compile(dialect=self.dialect))
else:
type_text = type_.dialect_impl(self.dialect).get_col_spec()
type_text = str(type_.compile(dialect=self.dialect))
self.append("TYPE %s" % type_text)
def _visit_column_name(self, table, column, delta):
@@ -279,75 +271,17 @@ class ANSIConstraintCommon(AlterTableVisitor):
def visit_migrate_unique_constraint(self, *p, **k):
self._visit_constraint(*p, **k)
if SQLA_06:
class ANSIConstraintGenerator(ANSIConstraintCommon, SchemaGenerator):
def _visit_constraint(self, constraint):
constraint.name = self.get_constraint_name(constraint)
self.append(self.process(AddConstraint(constraint)))
self.execute()
class ANSIConstraintGenerator(ANSIConstraintCommon, SchemaGenerator):
def _visit_constraint(self, constraint):
constraint.name = self.get_constraint_name(constraint)
self.append(self.process(AddConstraint(constraint)))
self.execute()
class ANSIConstraintDropper(ANSIConstraintCommon, SchemaDropper):
def _visit_constraint(self, constraint):
constraint.name = self.get_constraint_name(constraint)
self.append(self.process(DropConstraint(constraint, cascade=constraint.cascade)))
self.execute()
else:
class ANSIConstraintGenerator(ANSIConstraintCommon, SchemaGenerator):
def get_constraint_specification(self, cons, **kwargs):
"""Constaint SQL generators.
We cannot use SA visitors because they append comma.
"""
if isinstance(cons, PrimaryKeyConstraint):
if cons.name is not None:
self.append("CONSTRAINT %s " % self.preparer.format_constraint(cons))
self.append("PRIMARY KEY ")
self.append("(%s)" % ', '.join(self.preparer.quote(c.name, c.quote)
for c in cons))
self.define_constraint_deferrability(cons)
elif isinstance(cons, ForeignKeyConstraint):
self.define_foreign_key(cons)
elif isinstance(cons, CheckConstraint):
if cons.name is not None:
self.append("CONSTRAINT %s " %
self.preparer.format_constraint(cons))
self.append("CHECK (%s)" % cons.sqltext)
self.define_constraint_deferrability(cons)
elif isinstance(cons, UniqueConstraint):
if cons.name is not None:
self.append("CONSTRAINT %s " %
self.preparer.format_constraint(cons))
self.append("UNIQUE (%s)" % \
(', '.join(self.preparer.quote(c.name, c.quote) for c in cons)))
self.define_constraint_deferrability(cons)
else:
raise exceptions.InvalidConstraintError(cons)
def _visit_constraint(self, constraint):
table = self.start_alter_table(constraint)
constraint.name = self.get_constraint_name(constraint)
self.append("ADD ")
self.get_constraint_specification(constraint)
self.execute()
class ANSIConstraintDropper(ANSIConstraintCommon, SchemaDropper):
def _visit_constraint(self, constraint):
self.start_alter_table(constraint)
self.append("DROP CONSTRAINT ")
constraint.name = self.get_constraint_name(constraint)
self.append(self.preparer.format_constraint(constraint))
if constraint.cascade:
self.cascade_constraint(constraint)
self.execute()
def cascade_constraint(self, constraint):
self.append(" CASCADE")
class ANSIConstraintDropper(ANSIConstraintCommon, SchemaDropper):
def _visit_constraint(self, constraint):
constraint.name = self.get_constraint_name(constraint)
self.append(self.process(DropConstraint(constraint, cascade=constraint.cascade)))
self.execute()
class ANSIDialect(DefaultDialect):
-3
View File
@@ -4,7 +4,6 @@
from sqlalchemy import schema
from migrate.exceptions import *
from migrate.changeset import SQLA_06
class ConstraintChangeset(object):
"""Base class for Constraint classes."""
@@ -165,8 +164,6 @@ class CheckConstraint(ConstraintChangeset, schema.CheckConstraint):
table = kwargs.pop('table', table)
schema.CheckConstraint.__init__(self, sqltext, *args, **kwargs)
if table is not None:
if not SQLA_06:
self.table = table
self._set_parent(table)
self.colnames = colnames
+3 -9
View File
@@ -4,13 +4,10 @@
from sqlalchemy.databases import firebird as sa_base
from sqlalchemy.schema import PrimaryKeyConstraint
from migrate import exceptions
from migrate.changeset import ansisql, SQLA_06
from migrate.changeset import ansisql
if SQLA_06:
FBSchemaGenerator = sa_base.FBDDLCompiler
else:
FBSchemaGenerator = sa_base.FBSchemaGenerator
FBSchemaGenerator = sa_base.FBDDLCompiler
class FBColumnGenerator(FBSchemaGenerator, ansisql.ANSIColumnGenerator):
"""Firebird column generator implementation."""
@@ -41,10 +38,7 @@ class FBColumnDropper(ansisql.ANSIColumnDropper):
# is deleted!
continue
if SQLA_06:
should_drop = column.name in cons.columns
else:
should_drop = cons.contains_column(column) and cons.name
should_drop = column.name in cons.columns
if should_drop:
self.start_alter_table(column)
self.append("DROP CONSTRAINT ")
+6 -35
View File
@@ -6,13 +6,10 @@ from sqlalchemy.databases import mysql as sa_base
from sqlalchemy import types as sqltypes
from migrate import exceptions
from migrate.changeset import ansisql, SQLA_06
from migrate.changeset import ansisql
if not SQLA_06:
MySQLSchemaGenerator = sa_base.MySQLSchemaGenerator
else:
MySQLSchemaGenerator = sa_base.MySQLDDLCompiler
MySQLSchemaGenerator = sa_base.MySQLDDLCompiler
class MySQLColumnGenerator(MySQLSchemaGenerator, ansisql.ANSIColumnGenerator):
pass
@@ -53,37 +50,11 @@ class MySQLSchemaChanger(MySQLSchemaGenerator, ansisql.ANSISchemaChanger):
class MySQLConstraintGenerator(ansisql.ANSIConstraintGenerator):
pass
if SQLA_06:
class MySQLConstraintDropper(MySQLSchemaGenerator, ansisql.ANSIConstraintDropper):
def visit_migrate_check_constraint(self, *p, **k):
raise exceptions.NotSupportedError("MySQL does not support CHECK"
" constraints, use triggers instead.")
else:
class MySQLConstraintDropper(ansisql.ANSIConstraintDropper):
def visit_migrate_primary_key_constraint(self, constraint):
self.start_alter_table(constraint)
self.append("DROP PRIMARY KEY")
self.execute()
def visit_migrate_foreign_key_constraint(self, constraint):
self.start_alter_table(constraint)
self.append("DROP FOREIGN KEY ")
constraint.name = self.get_constraint_name(constraint)
self.append(self.preparer.format_constraint(constraint))
self.execute()
def visit_migrate_check_constraint(self, *p, **k):
raise exceptions.NotSupportedError("MySQL does not support CHECK"
" constraints, use triggers instead.")
def visit_migrate_unique_constraint(self, constraint, *p, **k):
self.start_alter_table(constraint)
self.append('DROP INDEX ')
constraint.name = self.get_constraint_name(constraint)
self.append(self.preparer.format_constraint(constraint))
self.execute()
class MySQLConstraintDropper(MySQLSchemaGenerator, ansisql.ANSIConstraintDropper):
def visit_migrate_check_constraint(self, *p, **k):
raise exceptions.NotSupportedError("MySQL does not support CHECK"
" constraints, use triggers instead.")
class MySQLDialect(ansisql.ANSIDialect):
+2 -5
View File
@@ -5,13 +5,10 @@ import sqlalchemy as sa
from sqlalchemy.databases import oracle as sa_base
from migrate import exceptions
from migrate.changeset import ansisql, SQLA_06
from migrate.changeset import ansisql
if not SQLA_06:
OracleSchemaGenerator = sa_base.OracleSchemaGenerator
else:
OracleSchemaGenerator = sa_base.OracleDDLCompiler
OracleSchemaGenerator = sa_base.OracleDDLCompiler
class OracleColumnGenerator(OracleSchemaGenerator, ansisql.ANSIColumnGenerator):
+3 -7
View File
@@ -3,14 +3,10 @@
.. _`PostgreSQL`: http://www.postgresql.org/
"""
from migrate.changeset import ansisql, SQLA_06
from migrate.changeset import ansisql
if not SQLA_06:
from sqlalchemy.databases import postgres as sa_base
PGSchemaGenerator = sa_base.PGSchemaGenerator
else:
from sqlalchemy.databases import postgresql as sa_base
PGSchemaGenerator = sa_base.PGDDLCompiler
from sqlalchemy.databases import postgresql as sa_base
PGSchemaGenerator = sa_base.PGDDLCompiler
class PGColumnGenerator(PGSchemaGenerator, ansisql.ANSIColumnGenerator):
+4 -6
View File
@@ -9,13 +9,11 @@ from copy import copy
from sqlalchemy.databases import sqlite as sa_base
from migrate import exceptions
from migrate.changeset import ansisql, SQLA_06
from migrate.changeset import ansisql
if not SQLA_06:
SQLiteSchemaGenerator = sa_base.SQLiteSchemaGenerator
else:
SQLiteSchemaGenerator = sa_base.SQLiteDDLCompiler
SQLiteSchemaGenerator = sa_base.SQLiteDDLCompiler
class SQLiteCommon(object):
@@ -39,7 +37,7 @@ class SQLiteHelper(SQLiteCommon):
insertion_string = self._modify_table(table, column, delta)
table.create()
table.create(bind=self.connection)
self.append(insertion_string % {'table_name': table_name})
self.execute()
self.append('DROP TABLE migration_tmp')
+18 -14
View File
@@ -11,7 +11,7 @@ from sqlalchemy.schema import ForeignKeyConstraint
from sqlalchemy.schema import UniqueConstraint
from migrate.exceptions import *
from migrate.changeset import SQLA_06
from migrate.changeset import SQLA_07
from migrate.changeset.databases.visitor import (get_engine_visitor,
run_single_visitor)
@@ -349,10 +349,7 @@ class ColumnDelta(DictMixin, sqlalchemy.schema.SchemaItem):
def process_column(self, column):
"""Processes default values for column"""
# XXX: this is a snippet from SA processing of positional parameters
if not SQLA_06 and column.args:
toinit = list(column.args)
else:
toinit = list()
toinit = list()
if column.server_default is not None:
if isinstance(column.server_default, sqlalchemy.FetchedValue):
@@ -367,9 +364,6 @@ class ColumnDelta(DictMixin, sqlalchemy.schema.SchemaItem):
for_update=True))
if toinit:
column._init_items(*toinit)
if not SQLA_06:
column.args = []
def _get_table(self):
return getattr(self, '_table', None)
@@ -469,14 +463,18 @@ class ChangesetTable(object):
self._set_parent(self.metadata)
def _meta_key(self):
"""Get the meta key for this table."""
return sqlalchemy.schema._get_table_key(self.name, self.schema)
def deregister(self):
"""Remove this table from its metadata"""
key = self._meta_key()
meta = self.metadata
if key in meta.tables:
del meta.tables[key]
if SQLA_07:
self.metadata._remove_table(self.name, self.schema)
else:
key = self._meta_key()
meta = self.metadata
if key in meta.tables:
del meta.tables[key]
class ChangesetColumn(object):
@@ -555,7 +553,10 @@ populated with defaults
def add_to_table(self, table):
if table is not None and self.table is None:
self._set_parent(table)
if SQLA_07:
table.append_column(self)
else:
self._set_parent(table)
def _col_name_in_constraint(self,cons,name):
return False
@@ -590,7 +591,10 @@ populated with defaults
table.constraints = table.constraints - to_drop
if table.c.contains_column(self):
table.c.remove(self)
if SQLA_07:
table._columns.remove(self)
else:
table.c.remove(self)
# TODO: this is fixed in 0.6
def copy_fixed(self, **kw):
+9 -8
View File
@@ -110,19 +110,19 @@ def script(description, repository, **opts):
@catch_known_errors
def script_sql(database, repository, **opts):
"""%prog script_sql DATABASE REPOSITORY_PATH
def script_sql(database, description, repository, **opts):
"""%prog script_sql DATABASE DESCRIPTION REPOSITORY_PATH
Create empty change SQL scripts for given DATABASE, where DATABASE
is either specific ('postgres', 'mysql', 'oracle', 'sqlite', etc.)
is either specific ('postgresql', 'mysql', 'oracle', 'sqlite', etc.)
or generic ('default').
For instance, manage.py script_sql postgres creates:
repository/versions/001_postgres_upgrade.sql and
repository/versions/001_postgres_postgres.sql
For instance, manage.py script_sql postgresql description creates:
repository/versions/001_description_postgresql_upgrade.sql and
repository/versions/001_description_postgresql_downgrade.sql
"""
repo = Repository(repository)
repo.create_script_sql(database, **opts)
repo.create_script_sql(database, description, **opts)
def version(repository, **opts):
@@ -212,14 +212,15 @@ def test(url, repository, **opts):
"""
engine = opts.pop('engine')
repos = Repository(repository)
script = repos.version(None).script()
# Upgrade
log.info("Upgrading...")
script = repos.version(None).script(engine.name, 'upgrade')
script.run(engine, 1)
log.info("done")
log.info("Downgrading...")
script = repos.version(None).script(engine.name, 'downgrade')
script.run(engine, -1)
log.info("done")
log.info("Success")
+105 -74
View File
@@ -1,9 +1,9 @@
"""
Code to generate a Python model from a database or differences
between a model and database.
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/
Some of this is borrowed heavily from the AutoCode project at:
http://code.google.com/p/sqlautocode/
"""
import sys
@@ -34,6 +34,13 @@ Base = declarative.declarative_base()
class ModelGenerator(object):
"""Various transformations from an A, B diff.
In the implementation, A tends to be called the model and B
the database (although this is not true of all diffs).
The diff is directionless, but transformations apply the diff
in a particular direction, described in the method name.
"""
def __init__(self, diff, engine, declarative=False):
self.diff = diff
@@ -59,7 +66,7 @@ class ModelGenerator(object):
pass
else:
kwarg.append('default')
ks = ', '.join('%s=%r' % (k, getattr(col, k)) for k in kwarg)
args = ['%s=%r' % (k, getattr(col, k)) for k in kwarg]
# crs: not sure if this is good idea, but it gets rid of extra
# u''
@@ -73,43 +80,38 @@ class ModelGenerator(object):
type_ = cls()
break
type_repr = repr(type_)
if type_repr.endswith('()'):
type_repr = type_repr[:-2]
constraints = [repr(cn) for cn in col.constraints]
data = {
'name': name,
'type': type_,
'constraints': ', '.join([repr(cn) for cn in col.constraints]),
'args': ks and ks or ''}
'commonStuff': ', '.join([type_repr] + constraints + args),
}
if data['constraints']:
if data['args']:
data['args'] = ',' + data['args']
if data['constraints'] or data['args']:
data['maybeComma'] = ','
else:
data['maybeComma'] = ''
commonStuff = """ %(maybeComma)s %(constraints)s %(args)s)""" % data
commonStuff = commonStuff.strip()
data['commonStuff'] = commonStuff
if self.declarative:
return """%(name)s = Column(%(type)r%(commonStuff)s""" % data
return """%(name)s = Column(%(commonStuff)s)""" % data
else:
return """Column(%(name)r, %(type)r%(commonStuff)s""" % data
return """Column(%(name)r, %(commonStuff)s)""" % data
def getTableDefn(self, table):
def _getTableDefn(self, table, metaName='meta'):
out = []
tableName = table.name
if self.declarative:
out.append("class %(table)s(Base):" % {'table': tableName})
out.append(" __tablename__ = '%(table)s'" % {'table': tableName})
out.append(" __tablename__ = '%(table)s'\n" %
{'table': tableName})
for col in table.columns:
out.append(" %s" % self.column_repr(col))
out.append(" %s" % self.column_repr(col))
out.append('\n')
else:
out.append("%(table)s = Table('%(table)s', meta," % \
{'table': tableName})
out.append("%(table)s = Table('%(table)s', %(meta)s," %
{'table': tableName, 'meta': metaName})
for col in table.columns:
out.append(" %s," % self.column_repr(col))
out.append(")")
out.append(" %s," % self.column_repr(col))
out.append(")\n")
return out
def _get_tables(self,missingA=False,missingB=False,modified=False):
@@ -122,9 +124,15 @@ class ModelGenerator(object):
if bool_:
for name in names:
yield metadata.tables.get(name)
def toPython(self):
"""Assume database is current and model is empty."""
def genBDefinition(self):
"""Generates the source code for a definition of B.
Assumes a diff where A is empty.
Was: toPython. Assume database (B) is current and model (A) is empty.
"""
out = []
if self.declarative:
out.append(DECLARATIVE_HEADER)
@@ -132,67 +140,89 @@ class ModelGenerator(object):
out.append(HEADER)
out.append("")
for table in self._get_tables(missingA=True):
out.extend(self.getTableDefn(table))
out.append("")
out.extend(self._getTableDefn(table))
return '\n'.join(out)
def toUpgradeDowngradePython(self, indent=' '):
''' Assume model is most current and database is out-of-date. '''
def genB2AMigration(self, indent=' '):
'''Generate a migration from B to A.
Was: toUpgradeDowngradePython
Assume model (A) is most current and database (B) is out-of-date.
'''
decls = ['from migrate.changeset import schema',
'meta = MetaData()']
for table in self._get_tables(
missingA=True,missingB=True,modified=True
):
decls.extend(self.getTableDefn(table))
'pre_meta = MetaData()',
'post_meta = MetaData()',
]
upgradeCommands = ['pre_meta.bind = migrate_engine',
'post_meta.bind = migrate_engine']
downgradeCommands = list(upgradeCommands)
upgradeCommands, downgradeCommands = [], []
for tableName in self.diff.tables_missing_from_A:
upgradeCommands.append("%(table)s.drop()" % {'table': tableName})
downgradeCommands.append("%(table)s.create()" % \
{'table': tableName})
for tableName in self.diff.tables_missing_from_B:
upgradeCommands.append("%(table)s.create()" % {'table': tableName})
downgradeCommands.append("%(table)s.drop()" % {'table': tableName})
for tn in self.diff.tables_missing_from_A:
pre_table = self.diff.metadataB.tables[tn]
decls.extend(self._getTableDefn(pre_table, metaName='pre_meta'))
upgradeCommands.append(
"pre_meta.tables[%(table)r].drop()" % {'table': tn})
downgradeCommands.append(
"pre_meta.tables[%(table)r].create()" % {'table': tn})
for tableName in self.diff.tables_different:
dbTable = self.diff.metadataB.tables[tableName]
missingInDatabase, missingInModel, diffDecl = \
self.diff.colDiffs[tableName]
for col in missingInDatabase:
upgradeCommands.append('%s.columns[%r].create()' % (
modelTable, col.name))
downgradeCommands.append('%s.columns[%r].drop()' % (
modelTable, col.name))
for col in missingInModel:
upgradeCommands.append('%s.columns[%r].drop()' % (
modelTable, col.name))
downgradeCommands.append('%s.columns[%r].create()' % (
modelTable, col.name))
for modelCol, databaseCol, modelDecl, databaseDecl in diffDecl:
for tn in self.diff.tables_missing_from_B:
post_table = self.diff.metadataA.tables[tn]
decls.extend(self._getTableDefn(post_table, metaName='post_meta'))
upgradeCommands.append(
"post_meta.tables[%(table)r].create()" % {'table': tn})
downgradeCommands.append(
"post_meta.tables[%(table)r].drop()" % {'table': tn})
for (tn, td) in self.diff.tables_different.iteritems():
if td.columns_missing_from_A or td.columns_different:
pre_table = self.diff.metadataB.tables[tn]
decls.extend(self._getTableDefn(
pre_table, metaName='pre_meta'))
if td.columns_missing_from_B or td.columns_different:
post_table = self.diff.metadataA.tables[tn]
decls.extend(self._getTableDefn(
post_table, metaName='post_meta'))
for col in td.columns_missing_from_A:
upgradeCommands.append(
'pre_meta.tables[%r].columns[%r].drop()' % (tn, col))
downgradeCommands.append(
'pre_meta.tables[%r].columns[%r].create()' % (tn, col))
for col in td.columns_missing_from_B:
upgradeCommands.append(
'post_meta.tables[%r].columns[%r].create()' % (tn, col))
downgradeCommands.append(
'post_meta.tables[%r].columns[%r].drop()' % (tn, col))
for modelCol, databaseCol, modelDecl, databaseDecl in td.columns_different:
upgradeCommands.append(
'assert False, "Can\'t alter columns: %s:%s=>%s"' % (
modelTable, modelCol.name, databaseCol.name))
tn, modelCol.name, databaseCol.name))
downgradeCommands.append(
'assert False, "Can\'t alter columns: %s:%s=>%s"' % (
modelTable, modelCol.name, databaseCol.name))
pre_command = ' meta.bind = migrate_engine'
tn, modelCol.name, databaseCol.name))
return (
'\n'.join(decls),
'\n'.join([pre_command] + ['%s%s' % (indent, line) for line in upgradeCommands]),
'\n'.join([pre_command] + ['%s%s' % (indent, line) for line in downgradeCommands]))
'\n'.join('%s%s' % (indent, line) for line in upgradeCommands),
'\n'.join('%s%s' % (indent, line) for line in downgradeCommands))
def _db_can_handle_this_change(self,td):
"""Check if the database can handle going from B to A."""
if (td.columns_missing_from_B
and not td.columns_missing_from_A
and not td.columns_different):
# Even sqlite can handle this.
# Even sqlite can handle column additions.
return True
else:
return not self.engine.url.drivername.startswith('sqlite')
def applyModel(self):
"""Apply model to current database."""
def runB2A(self):
"""Goes from B to A.
Was: applyModel. Apply model (A) to current database (B).
"""
meta = sqlalchemy.MetaData(self.engine)
@@ -208,9 +238,9 @@ class ModelGenerator(object):
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:
@@ -252,3 +282,4 @@ class ModelGenerator(object):
except:
trans.rollback()
raise
+13 -2
View File
@@ -115,6 +115,7 @@ class Repository(pathed.Pathed):
options.setdefault('version_table', 'migrate_version')
options.setdefault('repository_id', name)
options.setdefault('required_dbs', [])
options.setdefault('use_timestamp_numbering', False)
tmpl = open(os.path.join(tmpl_dir, cls._config)).read()
ret = TempitaTemplate(tmpl).substitute(options)
@@ -152,11 +153,14 @@ class Repository(pathed.Pathed):
def create_script(self, description, **k):
"""API to :meth:`migrate.versioning.version.Collection.create_new_python_version`"""
k['use_timestamp_numbering'] = self.use_timestamp_numbering
self.versions.create_new_python_version(description, **k)
def create_script_sql(self, database, **k):
def create_script_sql(self, database, description, **k):
"""API to :meth:`migrate.versioning.version.Collection.create_new_sql_version`"""
self.versions.create_new_sql_version(database, **k)
k['use_timestamp_numbering'] = self.use_timestamp_numbering
self.versions.create_new_sql_version(database, description, **k)
@property
def latest(self):
@@ -173,6 +177,13 @@ class Repository(pathed.Pathed):
"""Returns repository id specified in config"""
return self.config.get('db_settings', 'repository_id')
@property
def use_timestamp_numbering(self):
"""Returns use_timestamp_numbering specified in config"""
if self.config.has_option('db_settings', 'use_timestamp_numbering'):
return self.config.getboolean('db_settings', 'use_timestamp_numbering')
return False
def version(self, *p, **k):
"""API to :attr:`migrate.versioning.version.Collection.version`"""
return self.versions.version(*p, **k)
+14 -7
View File
@@ -11,6 +11,7 @@ from sqlalchemy import exceptions as sa_exceptions
from sqlalchemy.sql import bindparam
from migrate import exceptions
from migrate.changeset import SQLA_07
from migrate.versioning import genmodel, schemadiff
from migrate.versioning.repository import Repository
from migrate.versioning.util import load_model
@@ -57,14 +58,20 @@ class ControlledSchema(object):
"""
Remove version control from a database.
"""
try:
self.table.drop()
except (sa_exceptions.SQLError):
raise exceptions.DatabaseNotControlledError(str(self.table))
if SQLA_07:
try:
self.table.drop()
except sa_exceptions.DatabaseError:
raise exceptions.DatabaseNotControlledError(str(self.table))
else:
try:
self.table.drop()
except (sa_exceptions.SQLError):
raise exceptions.DatabaseNotControlledError(str(self.table))
def changeset(self, version=None):
"""API to Changeset creation.
Uses self.version for start version and engine.name
to get database name.
"""
@@ -110,7 +117,7 @@ class ControlledSchema(object):
diff = schemadiff.getDiffOfModelAgainstDatabase(
model, self.engine, excludeTables=[self.repository.version_table]
)
genmodel.ModelGenerator(diff,self.engine).applyModel()
genmodel.ModelGenerator(diff,self.engine).runB2A()
self.update_repository_table(self.version, int(self.repository.latest))
@@ -210,4 +217,4 @@ class ControlledSchema(object):
diff = schemadiff.getDiffOfModelAgainstDatabase(
MetaData(), engine, excludeTables=[repository.version_table]
)
return genmodel.ModelGenerator(diff, engine, declarative).toPython()
return genmodel.ModelGenerator(diff, engine, declarative).genBDefinition()
+42 -35
View File
@@ -5,7 +5,6 @@
import logging
import sqlalchemy
from migrate.changeset import SQLA_06
from sqlalchemy.types import Float
log = logging.getLogger(__name__)
@@ -17,8 +16,16 @@ def getDiffOfModelAgainstDatabase(metadata, engine, excludeTables=None):
:return: object which will evaluate to :keyword:`True` if there \
are differences else :keyword:`False`.
"""
return SchemaDiff(metadata,
sqlalchemy.MetaData(engine, reflect=True),
db_metadata = sqlalchemy.MetaData(engine, reflect=True)
# sqlite will include a dynamically generated 'sqlite_sequence' table if
# there are autoincrement sequences in the database; this should not be
# compared.
if engine.dialect.name == 'sqlite':
if 'sqlite_sequence' in db_metadata.tables:
db_metadata.remove(db_metadata.tables['sqlite_sequence'])
return SchemaDiff(metadata, db_metadata,
labelA='model',
labelB='database',
excludeTables=excludeTables)
@@ -39,11 +46,11 @@ 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.
@@ -51,15 +58,15 @@ class ColDiff(object):
.. attribute:: type_A
The most generic type of the :class:`~sqlalchemy.schema.Column`
object in A.
object in A.
.. attribute:: type_B
The most generic type of the :class:`~sqlalchemy.schema.Column`
object in A.
object in A.
"""
diff = False
def __init__(self,col_A,col_B):
@@ -87,10 +94,10 @@ class ColDiff(object):
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`
@@ -101,12 +108,12 @@ class TableDiff(object):
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
A dictionary containing information about columns that were
@@ -126,7 +133,7 @@ class TableDiff(object):
self.columns_missing_from_B or
self.columns_different
)
class SchemaDiff(object):
"""
Compute the difference between two :class:`~sqlalchemy.schema.MetaData`
@@ -139,34 +146,34 @@ class SchemaDiff(object):
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:
:class:`~sqlalchemy.schema.MetaData`.
:param labelB:
The label to use in messages about the second
:class:`~sqlalchemy.schema.MetaData`.
:class:`~sqlalchemy.schema.MetaData`.
: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
@@ -195,26 +202,26 @@ class SchemaDiff(object):
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()
A_table = metadataA.tables[table_name]
B_table = metadataB.tables[table_name]
A_column_names = set(A_table.columns.keys())
B_column_names = set(B_table.columns.keys())
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 = {}
for col_name in A_column_names.intersection(B_column_names):
@@ -226,7 +233,7 @@ class SchemaDiff(object):
if cd:
td.columns_different[col_name]=cd
# XXX - index and constraint differences should
# be checked for here
@@ -237,7 +244,7 @@ class SchemaDiff(object):
''' Summarize differences. '''
out = []
column_template =' %%%is: %%r' % self.label_width
for names,label in (
(self.tables_missing_from_A,self.labelA),
(self.tables_missing_from_B,self.labelB),
@@ -248,7 +255,7 @@ class SchemaDiff(object):
label,', '.join(sorted(names))
)
)
for name,td in sorted(self.tables_different.items()):
out.append(
' table with differences: %s' % name
@@ -267,7 +274,7 @@ class SchemaDiff(object):
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:')
return '\n'.join(out)
+8 -8
View File
@@ -25,7 +25,7 @@ class PythonScript(base.BaseScript):
@classmethod
def create(cls, path, **opts):
"""Create an empty migration script at specified path
:returns: :class:`PythonScript instance <migrate.versioning.script.py.PythonScript>`"""
cls.require_notfound(path)
@@ -38,7 +38,7 @@ class PythonScript(base.BaseScript):
def make_update_script_for_model(cls, engine, oldmodel,
model, repository, **opts):
"""Create a migration script based on difference between two SA models.
:param repository: path to migrate repository
:param oldmodel: dotted.module.name:SAClass or SAClass object
:param model: dotted.module.name:SAClass or SAClass object
@@ -50,7 +50,7 @@ class PythonScript(base.BaseScript):
:returns: Upgrade / Downgrade script
:rtype: string
"""
if isinstance(repository, basestring):
# oh dear, an import cycle!
from migrate.versioning.repository import Repository
@@ -61,12 +61,12 @@ class PythonScript(base.BaseScript):
# Compute differences.
diff = schemadiff.getDiffOfModelAgainstModel(
oldmodel,
model,
oldmodel,
excludeTables=[repository.version_table])
# TODO: diff can be False (there is no difference?)
decls, upgradeCommands, downgradeCommands = \
genmodel.ModelGenerator(diff,engine).toUpgradeDowngradePython()
genmodel.ModelGenerator(diff,engine).genB2AMigration()
# Store differences into file.
src = Template(opts.pop('templates_path', None)).get_script(opts.pop('templates_theme', None))
@@ -86,7 +86,7 @@ class PythonScript(base.BaseScript):
@classmethod
def verify_module(cls, path):
"""Ensure path is a valid script
:param path: Script location
:type path: string
:raises: :exc:`InvalidScriptError <migrate.exceptions.InvalidScriptError>`
@@ -101,7 +101,7 @@ class PythonScript(base.BaseScript):
return module
def preview_sql(self, url, step, **args):
"""Mocks SQLAlchemy Engine to store all executed calls in a string
"""Mocks SQLAlchemy Engine to store all executed calls in a string
and runs :meth:`PythonScript.run <migrate.versioning.script.py.PythonScript.run>`
:returns: SQL file
@@ -119,7 +119,7 @@ class PythonScript(base.BaseScript):
return go(url, step, **args)
def run(self, engine, step):
"""Core method of Script file.
"""Core method of Script file.
Exectues :func:`update` or :func:`downgrade` functions
:param engine: SQLAlchemy Engine
-1
View File
@@ -38,7 +38,6 @@ class Template(pathed.Pathed):
if `path` is not provided.
"""
pkg = 'migrate.versioning.templates'
_manage = 'manage.py_tmpl'
def __new__(cls, path=None):
if path is None:
@@ -1,5 +0,0 @@
#!/usr/bin/env python
from migrate.versioning.shell import main
if __name__ == '__main__':
main(%(defaults)s)
@@ -7,4 +7,6 @@ del _vars['__template_name__']
_vars.pop('repository_name', None)
defaults = ", ".join(["%s='%s'" % var for var in _vars.iteritems()])
}}
main({{ defaults }})
if __name__ == '__main__':
main({{ defaults }})
@@ -26,4 +26,5 @@ conf_dict = ConfigLoader(conf_path).parser._sections['app:main']
# migrate supports passing url as an existing Engine instance (since 0.6.0)
# usage: migrate -c path/to/config.ini COMMANDS
main(url=engine_from_config(conf_dict), repository=migrations.__path__[0],{{ defaults }})
if __name__ == '__main__':
main(url=engine_from_config(conf_dict), repository=migrations.__path__[0],{{ defaults }})
@@ -18,3 +18,8 @@ version_table={{ locals().pop('version_table') }}
# be using to ensure your updates to that database work properly.
# This must be a list; example: ['postgres','sqlite']
required_dbs={{ locals().pop('required_dbs') }}
# When creating new change scripts, Migrate will stamp the new script with
# a version number. By default this is latest_version + 1. You can set this
# to 'true' to tell Migrate to use the UTC timestamp instead.
use_timestamp_numbering={{ locals().pop('use_timestamp_numbering') }}
@@ -18,3 +18,8 @@ version_table={{ locals().pop('version_table') }}
# be using to ensure your updates to that database work properly.
# This must be a list; example: ['postgres','sqlite']
required_dbs={{ locals().pop('required_dbs') }}
# When creating new change scripts, Migrate will stamp the new script with
# a version number. By default this is latest_version + 1. You can set this
# to 'true' to tell Migrate to use the UTC timestamp instead.
use_timestamp_numbering={{ locals().pop('use_timestamp_numbering') }}
@@ -1,11 +1,13 @@
from sqlalchemy import *
from migrate import *
def upgrade(migrate_engine):
# Upgrade operations go here. Don't create your own engine; bind migrate_engine
# to your metadata
# Upgrade operations go here. Don't create your own engine; bind
# migrate_engine to your metadata
pass
def downgrade(migrate_engine):
# Operations to reverse the above upgrade go here.
pass
@@ -1,11 +1,13 @@
from sqlalchemy import *
from migrate import *
def upgrade(migrate_engine):
# Upgrade operations go here. Don't create your own engine; bind migrate_engine
# to your metadata
# Upgrade operations go here. Don't create your own engine; bind
# migrate_engine to your metadata
pass
def downgrade(migrate_engine):
# Operations to reverse the above upgrade go here.
pass
+31 -8
View File
@@ -8,6 +8,7 @@ import logging
from migrate import exceptions
from migrate.versioning import pathed, script
from datetime import datetime
log = logging.getLogger(__name__)
@@ -88,9 +89,15 @@ class Collection(pathed.Pathed):
""":returns: Latest version in Collection"""
return max([VerNum(0)] + self.versions.keys())
def _next_ver_num(self, use_timestamp_numbering):
if use_timestamp_numbering == True:
return VerNum(int(datetime.utcnow().strftime('%Y%m%d%H%M%S')))
else:
return self.latest + 1
def create_new_python_version(self, description, **k):
"""Create Python files for new version"""
ver = self.latest + 1
ver = self._next_ver_num(k.pop('use_timestamp_numbering', False))
extra = str_to_filename(description)
if extra:
@@ -105,14 +112,22 @@ class Collection(pathed.Pathed):
script.PythonScript.create(filepath, **k)
self.versions[ver] = Version(ver, self.path, [filename])
def create_new_sql_version(self, database, **k):
def create_new_sql_version(self, database, description, **k):
"""Create SQL files for new version"""
ver = self.latest + 1
ver = self._next_ver_num(k.pop('use_timestamp_numbering', False))
self.versions[ver] = Version(ver, self.path, [])
extra = str_to_filename(description)
if extra:
if extra == '_':
extra = ''
elif not extra.startswith('_'):
extra = '_%s' % extra
# Create new files.
for op in ('upgrade', 'downgrade'):
filename = '%03d_%s_%s.sql' % (ver, database, op)
filename = '%03d%s_%s_%s.sql' % (ver, extra, database, op)
filepath = self._version_path(filename)
script.SqlScript.create(filepath, **k)
self.versions[ver].add_script(filepath)
@@ -176,18 +191,26 @@ class Version(object):
elif path.endswith(Extensions.sql):
self._add_script_sql(path)
SQL_FILENAME = re.compile(r'^(\d+)_([^_]+)_([^_]+).sql')
SQL_FILENAME = re.compile(r'^.*\.sql')
def _add_script_sql(self, path):
basename = os.path.basename(path)
match = self.SQL_FILENAME.match(basename)
if match:
version, dbms, op = match.group(1), match.group(2), match.group(3)
basename = basename.replace('.sql', '')
parts = basename.split('_')
if len(parts) < 3:
raise exceptions.ScriptError(
"Invalid SQL script name %s " % basename + \
"(needs to be ###_description_database_operation.sql)")
version = parts[0]
op = parts[-1]
dbms = parts[-2]
else:
raise exceptions.ScriptError(
"Invalid SQL script name %s " % basename + \
"(needs to be ###_database_operation.sql)")
"(needs to be ###_description_database_operation.sql)")
# File the script into a dictionary
self.sql.setdefault(dbms, {})[op] = script.SqlScript(path)