diff --git a/docs/api.rst b/docs/api.rst index a17ec64..d20c0d3 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -116,12 +116,14 @@ Module :mod:`repository ` .. automodule:: migrate.versioning.repository :members: :synopsis: SQLAlchemy migrate repository management + :member-order: groupwise Module :mod:`schema ` ------------------------------------------------ .. automodule:: migrate.versioning.schema :members: + :member-order: groupwise :synopsis: Database schema management Module :mod:`schemadiff ` @@ -136,15 +138,18 @@ Module :mod:`script ` .. automodule:: migrate.versioning.script.base :synopsis: Script utilities + :member-order: groupwise :members: .. automodule:: migrate.versioning.script.py :members: + :member-order: groupwise :inherited-members: :show-inheritance: .. automodule:: migrate.versioning.script.sql :members: + :member-order: groupwise :show-inheritance: :inherited-members: @@ -167,4 +172,5 @@ Module :mod:`version ` .. automodule:: migrate.versioning.version :members: + :member-order: groupwise :synopsis: Version management diff --git a/migrate/changeset/ansisql.py b/migrate/changeset/ansisql.py index 6679208..92ed8b7 100644 --- a/migrate/changeset/ansisql.py +++ b/migrate/changeset/ansisql.py @@ -13,7 +13,7 @@ from migrate.changeset import constraint, exceptions SchemaIterator = sa.engine.SchemaIterator -class RawAlterTableVisitor(object): +class AlterTableVisitor(SchemaIterator): """Common operations for ``ALTER TABLE`` statements.""" def _to_table(self, param): @@ -24,13 +24,6 @@ class RawAlterTableVisitor(object): ret = param return ret - def _to_table_name(self, param): - """Returns the table name for the given param object.""" - ret = self._to_table(param) - if isinstance(ret, sa.Table): - ret = ret.fullname - return ret - def start_alter_table(self, param): """Returns the start of an ``ALTER TABLE`` SQL-Statement. @@ -70,11 +63,6 @@ class RawAlterTableVisitor(object): return ret -class AlterTableVisitor(SchemaIterator, RawAlterTableVisitor): - """Common operations for ``ALTER TABLE`` statements""" - pass - - class ANSIColumnGenerator(AlterTableVisitor, SchemaGenerator): """Extends ansisql generator for column creation (alter table add col)""" @@ -82,7 +70,7 @@ class ANSIColumnGenerator(AlterTableVisitor, SchemaGenerator): """Create a column (table already exists). :param column: column object - :type column: :class:`sqlalchemy.Column` + :type column: :class:`sqlalchemy.Column` instance """ table = self.start_alter_table(column) self.append("ADD ") @@ -94,7 +82,7 @@ class ANSIColumnGenerator(AlterTableVisitor, SchemaGenerator): """Default table visitor, does nothing. :param table: table object - :type table: :class:`sqlalchemy.Table` + :type table: :class:`sqlalchemy.Table` instance """ pass @@ -124,7 +112,7 @@ class ANSISchemaChanger(AlterTableVisitor, SchemaGenerator): All items may be renamed. Columns can also have many of their properties - type, for example - changed. - Each function is passed a tuple, containing (object,name); where + Each function is passed a tuple, containing (object, name); where object is a type of object you'd expect for that function (ie. table for visit_table) and name is the object's new name. NONE means the name is unchanged. @@ -137,6 +125,14 @@ class ANSISchemaChanger(AlterTableVisitor, SchemaGenerator): self.append("RENAME TO %s" % self.preparer.quote(newname, table.quote)) self.execute() + def visit_index(self, param): + """Rename an index""" + index, newname = param + self.append("ALTER INDEX %s RENAME TO %s" % + (self.preparer.quote(self._validate_identifier(index.name, True), index.quote), + self.preparer.quote(self._validate_identifier(newname, True) , index.quote))) + self.execute() + def visit_column(self, delta): """Rename/change a column.""" # ALTER COLUMN is implemented as several ALTER statements @@ -152,14 +148,12 @@ class ANSISchemaChanger(AlterTableVisitor, SchemaGenerator): if 'name' in keys: self._run_subvisit(delta, self._visit_column_name) - def _run_subvisit(self, delta, func, col_name=None, table_name=None): - if table_name is None: - table_name = self._to_table(delta.table) - if col_name is None: - col_name = delta.current_name - ret = func(table_name, col_name, delta) + def _run_subvisit(self, delta, func): + """Runs visit method based on what needs to be changed on column""" + table = self._to_table(delta.table) + col_name = delta.current_name + ret = func(table, col_name, delta) self.execute() - return ret def _visit_column_foreign_key(self, delta): table = delta.table @@ -183,10 +177,10 @@ class ANSISchemaChanger(AlterTableVisitor, SchemaGenerator): cons.drop() cons.create() - def _visit_column_nullable(self, table_name, col_name, delta): + def _visit_column_nullable(self, table, col_name, delta): nullable = delta['nullable'] - table = self._to_table(delta) - self.start_alter_table(table_name) + table = self._to_table(table) + self.start_alter_table(table) # TODO: use preparer.format_column self.append("ALTER COLUMN %s " % self.preparer.quote_identifier(col_name)) if nullable: @@ -194,50 +188,41 @@ class ANSISchemaChanger(AlterTableVisitor, SchemaGenerator): else: self.append("SET NOT NULL") - def _visit_column_default(self, table_name, col_name, delta): + def _visit_column_default(self, table, col_name, delta): server_default = delta['server_default'] # Dummy column: get_col_default_string needs a column for some # reason dummy = sa.Column(None, None, server_default=server_default) default_text = self.get_column_default_string(dummy) - self.start_alter_table(table_name) + self.start_alter_table(table) # TODO: use preparer.format_column self.append("ALTER COLUMN %s " % self.preparer.quote_identifier(col_name)) if default_text is not None: - # TODO: format needed? self.append("SET DEFAULT %s" % default_text) else: self.append("DROP DEFAULT") - def _visit_column_type(self, table_name, col_name, delta): - type = delta['type'] - if not isinstance(type, sa.types.AbstractType): + def _visit_column_type(self, table, col_name, delta): + type_ = delta['type'] + if not isinstance(type_, sa.types.AbstractType): # It's the class itself, not an instance... make an # instance - type = type() - type_text = type.dialect_impl(self.dialect).get_col_spec() - self.start_alter_table(table_name) + type_ = type_() + type_text = type_.dialect_impl(self.dialect).get_col_spec() + self.start_alter_table(table) # TODO: does type need formating? # TODO: use preparer.format_column self.append("ALTER COLUMN %s TYPE %s" % (self.preparer.quote_identifier(col_name), type_text)) - def _visit_column_name(self, table_name, col_name, delta): + def _visit_column_name(self, table, col_name, delta): new_name = delta['name'] - self.start_alter_table(table_name) + self.start_alter_table(table) # TODO: use preparer.format_column self.append('RENAME COLUMN %s TO %s' % \ (self.preparer.quote_identifier(col_name), self.preparer.quote_identifier(new_name))) - def visit_index(self, param): - """Rename an index; #36""" - index, newname = param - self.append("ALTER INDEX %s RENAME TO %s" % - (self.preparer.quote(self._validate_identifier(index.name, True), index.quote), - self.preparer.quote(self._validate_identifier(newname, True) , index.quote))) - self.execute() - class ANSIConstraintCommon(AlterTableVisitor): """ @@ -250,12 +235,10 @@ class ANSIConstraintCommon(AlterTableVisitor): """Gets a name for the given constraint. If the name is already set it will be used otherwise the - constraint's :meth:`autoname - ` + constraint's :meth:`autoname ` method is used. :param cons: constraint object - :type cons: :class:`migrate.changeset.constraint.ConstraintChangeset` """ if cons.name is not None: ret = cons.name @@ -331,9 +314,7 @@ class ANSIFKGenerator(AlterTableVisitor, SchemaGenerator): """Extends ansisql generator for column creation (alter table add col)""" def __init__(self, *args, **kwargs): - self.fk = kwargs.get('fk', None) - if self.fk: - del kwargs['fk'] + self.fk = kwargs.pop('fk', None) super(ANSIFKGenerator, self).__init__(*args, **kwargs) def visit_column(self, column): diff --git a/migrate/changeset/constraint.py b/migrate/changeset/constraint.py index 8599877..21058b6 100644 --- a/migrate/changeset/constraint.py +++ b/migrate/changeset/constraint.py @@ -6,10 +6,9 @@ from sqlalchemy import schema class ConstraintChangeset(object): - """Base class for Constraint classes. - """ + """Base class for Constraint classes.""" - def _normalize_columns(self, cols, fullname=False): + def _normalize_columns(self, cols, table_name=False): """Given: column objects or names; return col names and (maybe) a table""" colnames = [] @@ -18,62 +17,48 @@ class ConstraintChangeset(object): if isinstance(col, schema.Column): if col.table is not None and table is None: table = col.table - if fullname: + if table_name: col = '.'.join((col.table.name, col.name)) else: col = col.name colnames.append(col) return colnames, table - def create(self, engine=None): + def create(self, *args, **kwargs): """Create the constraint in the database. - :param engine: the database engine to use. If this is - :keyword:`None` the instance's engine will be used + :param engine: the database engine to use. If this is \ + :keyword:`None` the instance's engine will be used :type engine: :class:`sqlalchemy.engine.base.Engine` """ - if engine is None: - engine = self.engine - engine.create(self) + from migrate.changeset.databases.visitor import get_engine_visitor + visitorcallable = get_engine_visitor(self.table.bind, + 'constraintgenerator') + _engine_run_visitor(self.table.bind, visitorcallable, self) - def drop(self, engine=None): + def drop(self, *args, **kwargs): """Drop the constraint from the database. :param engine: the database engine to use. If this is :keyword:`None` the instance's engine will be used :type engine: :class:`sqlalchemy.engine.base.Engine` """ - if engine is None: - engine = self.engine - engine.drop(self) - - def _derived_metadata(self): - return self.table._derived_metadata() + from migrate.changeset.databases.visitor import get_engine_visitor + visitorcallable = get_engine_visitor(self.table.bind, + 'constraintdropper') + _engine_run_visitor(self.table.bind, visitorcallable, self) + self.columns.clear() + return self def accept_schema_visitor(self, visitor, *p, **k): - """ - :raises: :exc:`NotImplementedError` if this method is not \ -overridden by a subclass - """ - raise NotImplementedError() - - def _accept_schema_visitor(self, visitor, func, *p, **k): """Call the visitor only if it defines the given function""" - try: - func = getattr(visitor, func) - except AttributeError: - return - return func(self) + return getattr(visitor, self._func)(self) def autoname(self): """Automatically generate a name for the constraint instance. Subclasses must implement this method. - - :raises: :exc:`NotImplementedError` if this method is not \ -overridden by a subclass """ - raise NotImplementedError() def _engine_run_visitor(engine, visitorcallable, element, **kwargs): @@ -87,6 +72,8 @@ def _engine_run_visitor(engine, visitorcallable, element, **kwargs): class PrimaryKeyConstraint(ConstraintChangeset, schema.PrimaryKeyConstraint): """Primary key constraint class.""" + _func = 'visit_migrate_primary_key_constraint' + def __init__(self, *cols, **kwargs): colnames, table = self._normalize_columns(cols) table = kwargs.pop('table', table) @@ -94,86 +81,47 @@ class PrimaryKeyConstraint(ConstraintChangeset, schema.PrimaryKeyConstraint): if table is not None: self._set_parent(table) - def _set_parent(self, table): - self.table = table - return super(ConstraintChangeset, self)._set_parent(table) - - def create(self, *args, **kwargs): - from migrate.changeset.databases.visitor import get_engine_visitor - visitorcallable = get_engine_visitor(self.table.bind, - 'constraintgenerator') - _engine_run_visitor(self.table.bind, visitorcallable, self) - def autoname(self): """Mimic the database's automatic constraint names""" - ret = "%(table)s_pkey" % dict(table=self.table.name) - return ret - - def drop(self, *args, **kwargs): - from migrate.changeset.databases.visitor import get_engine_visitor - visitorcallable = get_engine_visitor(self.table.bind, - 'constraintdropper') - _engine_run_visitor(self.table.bind, visitorcallable, self) - self.columns.clear() - return self - - def accept_schema_visitor(self, visitor, *p, **k): - func = 'visit_migrate_primary_key_constraint' - return self._accept_schema_visitor(visitor, func, *p, **k) + return "%s_pkey" % self.table.name class ForeignKeyConstraint(ConstraintChangeset, schema.ForeignKeyConstraint): """Foreign key constraint class.""" + _func = 'visit_migrate_foreign_key_constraint' + def __init__(self, columns, refcolumns, *p, **k): colnames, table = self._normalize_columns(columns) table = k.pop('table', table) refcolnames, reftable = self._normalize_columns(refcolumns, - fullname=True) + table_name=True) super(ForeignKeyConstraint, self).__init__(colnames, refcolnames, *p, **k) if table is not None: self._set_parent(table) - def _get_referenced(self): + @property + def referenced(self): return [e.column for e in self.elements] - referenced = property(_get_referenced) - def _get_reftable(self): + @property + def reftable(self): return self.referenced[0].table - reftable = property(_get_reftable) def autoname(self): """Mimic the database's automatic constraint names""" - ret = "%(table)s_%(reftable)s_fkey"%dict( + ret = "%(table)s_%(reftable)s_fkey" % dict( table=self.table.name, - reftable=self.reftable.name, - ) + reftable=self.reftable.name,) return ret - def create(self, *args, **kwargs): - from migrate.changeset.databases.visitor import get_engine_visitor - visitorcallable = get_engine_visitor(self.table.bind, - 'constraintgenerator') - _engine_run_visitor(self.table.bind, visitorcallable, self) - return self - - def drop(self, *args, **kwargs): - from migrate.changeset.databases.visitor import get_engine_visitor - visitorcallable = get_engine_visitor(self.table.bind, - 'constraintdropper') - _engine_run_visitor(self.table.bind, visitorcallable, self) - self.columns.clear() - return self - - def accept_schema_visitor(self, visitor, *p, **k): - func = 'visit_migrate_foreign_key_constraint' - return self._accept_schema_visitor(visitor, func, *p, **k) - class CheckConstraint(ConstraintChangeset, schema.CheckConstraint): """Check constraint class.""" + _func = 'visit_migrate_check_constraint' + def __init__(self, sqltext, *args, **kwargs): cols = kwargs.pop('columns') colnames, table = self._normalize_columns(cols) @@ -184,28 +132,6 @@ class CheckConstraint(ConstraintChangeset, schema.CheckConstraint): self._set_parent(table) self.colnames = colnames - def _set_parent(self, table): - self.table = table - return super(ConstraintChangeset, self)._set_parent(table) - - def create(self): - from migrate.changeset.databases.visitor import get_engine_visitor - visitorcallable = get_engine_visitor(self.table.bind, - 'constraintgenerator') - _engine_run_visitor(self.table.bind, visitorcallable, self) - - def drop(self): - from migrate.changeset.databases.visitor import get_engine_visitor - visitorcallable = get_engine_visitor(self.table.bind, - 'constraintdropper') - _engine_run_visitor(self.table.bind, visitorcallable, self) - self.columns.clear() - return self - def autoname(self): return "%(table)s_%(cols)s_check" % \ - {"table": self.table.name, "cols": "_".join(self.colnames)} - - def accept_schema_visitor(self, visitor, *args, **kwargs): - func = 'visit_migrate_check_constraint' - return self._accept_schema_visitor(visitor, func, *args, **kwargs) + dict(table=self.table.name, cols="_".join(self.colnames)) diff --git a/migrate/changeset/databases/__init__.py b/migrate/changeset/databases/__init__.py index 17a12d2..4abc187 100644 --- a/migrate/changeset/databases/__init__.py +++ b/migrate/changeset/databases/__init__.py @@ -3,8 +3,8 @@ implementations. """ __all__=[ -'postgres', -'sqlite', -'mysql', -'oracle', + 'postgres', + 'sqlite', + 'mysql', + 'oracle', ] diff --git a/migrate/changeset/databases/mysql.py b/migrate/changeset/databases/mysql.py index 08ad4f3..468bbcb 100644 --- a/migrate/changeset/databases/mysql.py +++ b/migrate/changeset/databases/mysql.py @@ -2,9 +2,10 @@ MySQL database specific implementations of changeset classes. """ -from migrate.changeset import ansisql, exceptions from sqlalchemy.databases import mysql as sa_base -#import sqlalchemy as sa + +from migrate.changeset import ansisql, exceptions + MySQLSchemaGenerator = sa_base.MySQLSchemaGenerator diff --git a/migrate/changeset/databases/oracle.py b/migrate/changeset/databases/oracle.py index 2716fa2..fc45a0f 100644 --- a/migrate/changeset/databases/oracle.py +++ b/migrate/changeset/databases/oracle.py @@ -9,8 +9,7 @@ import sqlalchemy as sa OracleSchemaGenerator = sa_base.OracleSchemaGenerator -class OracleColumnGenerator(OracleSchemaGenerator, - ansisql.ANSIColumnGenerator): +class OracleColumnGenerator(OracleSchemaGenerator, ansisql.ANSIColumnGenerator): pass @@ -40,7 +39,7 @@ class OracleSchemaChanger(OracleSchemaGenerator, ansisql.ANSISchemaChanger): if 'name' in keys: self._run_subvisit(delta, self._visit_column_name) - def _visit_column_change(self, table_name, col_name, delta): + def _visit_column_change(self, table, col_name, delta): if not hasattr(delta, 'result_column'): # Oracle needs the whole column definition, not just a # lone name/type @@ -76,8 +75,7 @@ class OracleSchemaChanger(OracleSchemaGenerator, ansisql.ANSISchemaChanger): if dropdefault_hack: column.server_default = None - # TODO: format from table - self.start_alter_table(self.preparer.quote(table_name, True)) + self.start_alter_table(self.preparer.format_table(table)) self.append("MODIFY ") self.append(colspec) diff --git a/migrate/changeset/schema.py b/migrate/changeset/schema.py index 609f67a..dc75ac5 100644 --- a/migrate/changeset/schema.py +++ b/migrate/changeset/schema.py @@ -7,6 +7,7 @@ import sqlalchemy from migrate.changeset.databases.visitor import get_engine_visitor + __all__ = [ 'create_column', 'drop_column', @@ -37,10 +38,18 @@ def drop_column(column, table=None, *p, **k): def rename_table(table, name, engine=None): - """Rename a table, given the table's current name and the new - name. - + """Rename a table. + + If Table instance is given, engine is not used. + API to :meth:`table.rename` + + :param table: Table to be renamed + :param name: new name + :param engine: Engine instance + :type table: string or Table instance + :type name: string + :type engine: obj """ table = _to_table(table, engine) table.rename(name) @@ -49,15 +58,24 @@ def rename_table(table, name, engine=None): def rename_index(index, name, table=None, engine=None): """Rename an index. - Takes an index name/object, a table name/object, and an - engine. Engine and table aren't required if an index object is - given. + If Index and Table object instances are given, + table and engine are not used. API to :meth:`index.rename` + + :param index: Index to be renamed + :param name: new name + :param table: Table to which Index is reffered + :param engine: Engine instance + :type index: string or Index instance + :type name: string + :type table: string or Table instance + :type engine: obj """ index = _to_index(index, table, engine) index.rename(name) + def alter_column(*p, **k): """Alter a column. @@ -70,10 +88,12 @@ def alter_column(*p, **k): col = p[0] else: col = None + if 'table' not in k: k['table'] = col.table if 'engine' not in k: k['engine'] = k['table'].bind + engine = k['engine'] delta = _ColumnDelta(*p, **k) visitorcallable = get_engine_visitor(engine, 'schemachanger') @@ -102,8 +122,10 @@ def alter_column(*p, **k): def _to_table(table, engine=None): + """Return if instance of Table, else construct new with metadata""" if isinstance(table, sqlalchemy.Table): return table + # Given: table name, maybe an engine meta = sqlalchemy.MetaData() if engine is not None: @@ -112,8 +134,10 @@ def _to_table(table, engine=None): def _to_index(index, table=None, engine=None): + """Return if instance of Index, else construct new with metadata""" if isinstance(index, sqlalchemy.Index): return index + # Given: index name; table name required table = _to_table(table, engine) ret = sqlalchemy.Index(index) @@ -149,13 +173,10 @@ class _WrapRename(object): self.name = name def accept_schema_visitor(self, visitor): - if isinstance(self.item, sqlalchemy.Table): - suffix = 'table' - elif isinstance(self.item, sqlalchemy.Column): - suffix = 'column' - elif isinstance(self.item, sqlalchemy.Index): - suffix = 'index' + """Map Class (Table, Index, Column) to visitor function""" + suffix = self.item.__class__.__name__.lower() funcname = 'visit_%s' % suffix + func = getattr(visitor, funcname) param = self.item, self.name return func(param) @@ -207,14 +228,6 @@ class _ColumnDelta(dict): 'primary_key', 'foreign_key') - @property - def table_name(self): - if isinstance(self._table, basestring): - ret = self._table - else: - ret = self._table.name - return ret - @property def table(self): if isinstance(self._table, sqlalchemy.Table): @@ -310,16 +323,6 @@ class ChangesetTable(object): column = sqlalchemy.Column(str(column)) column.drop(table=self) - def _meta_key(self): - 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] - def rename(self, name, *args, **kwargs): """Rename this table. @@ -337,16 +340,15 @@ class ChangesetTable(object): self.name = name self._set_parent(meta) - def _get_fullname(self): - """Fullname should always be up to date""" - # Copied from Table constructor - if self.schema is not None: - ret = "%s.%s" % (self.schema, self.name) - else: - ret = self.name - return ret + def _meta_key(self): + return sqlalchemy.schema._get_table_key(self.name, self.schema) - fullname = property(_get_fullname, (lambda self, val: None)) + 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] class ChangesetColumn(object): @@ -384,7 +386,7 @@ class ChangesetColumn(object): visitorcallable = get_engine_visitor(engine, 'columngenerator') engine._run_visitor(visitorcallable, self, *args, **kwargs) - #add in foreign keys + # add in foreign keys if self.foreign_keys: for fk in self.foreign_keys: visitorcallable = get_engine_visitor(engine, diff --git a/test/changeset/test_constraint.py b/test/changeset/test_constraint.py index 24ded82..da7fbe0 100644 --- a/test/changeset/test_constraint.py +++ b/test/changeset/test_constraint.py @@ -8,6 +8,7 @@ from migrate.changeset import * from test import fixture + class TestConstraint(fixture.DB): level = fixture.DB.CONNECT @@ -37,7 +38,7 @@ class TestConstraint(fixture.DB): def _define_pk(self, *cols): # Add a pk by creating a PK constraint pk = PrimaryKeyConstraint(table=self.table, *cols) - self.assertEquals(list(pk.columns),list(cols)) + self.assertEquals(list(pk.columns), list(cols)) if self.url.startswith('oracle'): # Can't drop Oracle PKs without an explicit name pk.name = 'fgsfds' @@ -54,7 +55,7 @@ class TestConstraint(fixture.DB): pk.drop() self.refresh_table() #self.assertEquals(list(self.table.primary_key),list()) - self.assertEquals(len(self.table.primary_key),0) + self.assertEquals(len(self.table.primary_key), 0) self.assert_(isinstance(self.table.primary_key, schema.PrimaryKeyConstraint),self.table.primary_key.__class__) return pk @@ -75,7 +76,7 @@ class TestConstraint(fixture.DB): if self.url.startswith('mysql'): # MySQL FKs need an index - index = Index('index_name',self.table.c.fkey) + index = Index('index_name', self.table.c.fkey) index.create() if self.url.startswith('oracle'): # Oracle constraints need a name