diff --git a/TODO b/TODO index 686da9a..5ed58d2 100644 --- a/TODO +++ b/TODO @@ -10,11 +10,7 @@ make_update_script_for_model: 0.6.0 -- make logging stderr and stdout aware -- update documentation - update repository migration script -- readd transaction support -- wrap migration into transaction - interactive migration script resolution - port to unittest2 @@ -26,6 +22,7 @@ make_update_script_for_model: - verbose output on migration failures -Documentation - -- better document 'populate_default' +Transaction support in 0.6.1 +- script.run should call engine.transaction() +- API should support engine and connection as well +- tests for transactions diff --git a/docs/changelog.rst b/docs/changelog.rst index d080889..229c65f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -30,6 +30,7 @@ Features Bug fixes ***************** +- ORM methods now accept `connection` parameter commonly used for transactions - `server_defaults` passed to :meth:`Column.create ` are now issued correctly - use SQLAlchemy quoting system to avoid name conflicts (for issue 32) diff --git a/migrate/changeset/constraint.py b/migrate/changeset/constraint.py index 3c20d3f..866ea10 100644 --- a/migrate/changeset/constraint.py +++ b/migrate/changeset/constraint.py @@ -38,6 +38,8 @@ class ConstraintChangeset(object): :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` + :param connection: reuse connection istead of creating new one. + :type connection: :class:`sqlalchemy.engine.base.Connection` instance """ # TODO: set the parent here instead of in __init__ self.__do_imports('constraintgenerator', *a, **kw) @@ -50,6 +52,8 @@ class ConstraintChangeset(object): :param cascade: Issue CASCADE drop if database supports it :type engine: :class:`sqlalchemy.engine.base.Engine` :type cascade: bool + :param connection: reuse connection istead of creating new one. + :type connection: :class:`sqlalchemy.engine.base.Connection` instance :returns: Instance with cleared columns """ self.cascade = kw.pop('cascade', False) @@ -63,7 +67,7 @@ class ConstraintChangeset(object): class PrimaryKeyConstraint(ConstraintChangeset, schema.PrimaryKeyConstraint): """Construct PrimaryKeyConstraint - + Migrate's additional parameters: :param cols: Columns in constraint. @@ -89,7 +93,7 @@ class PrimaryKeyConstraint(ConstraintChangeset, schema.PrimaryKeyConstraint): class ForeignKeyConstraint(ConstraintChangeset, schema.ForeignKeyConstraint): """Construct ForeignKeyConstraint - + Migrate's additional parameters: :param columns: Columns in constraint @@ -132,7 +136,7 @@ class CheckConstraint(ConstraintChangeset, schema.CheckConstraint): """Construct CheckConstraint Migrate's additional parameters: - + :param sqltext: Plain SQL text to check condition :param columns: If not name is applied, you must supply this kw\ to autoname constraint @@ -165,7 +169,7 @@ class CheckConstraint(ConstraintChangeset, schema.CheckConstraint): class UniqueConstraint(ConstraintChangeset, schema.UniqueConstraint): """Construct UniqueConstraint - + Migrate's additional parameters: :param cols: Columns in constraint. diff --git a/migrate/changeset/databases/visitor.py b/migrate/changeset/databases/visitor.py index aa8a3e5..228b4d3 100644 --- a/migrate/changeset/databases/visitor.py +++ b/migrate/changeset/databases/visitor.py @@ -57,15 +57,22 @@ def get_dialect_visitor(sa_dialect, name): return visitor -def run_single_visitor(engine, visitorcallable, element, **kwargs): - """Runs only one method on the visitor""" - conn = engine.contextual_connect(close_with_result=False) +def run_single_visitor(engine, visitorcallable, element, + connection=None, **kwargs): + """Taken from :meth:`sqlalchemy.engine.base.Engine._run_single_visitor` + with support for migrate visitors. + """ + if connection is None: + conn = engine.contextual_connect(close_with_result=False) + else: + conn = connection + visitor = visitorcallable(engine.dialect, conn) try: - visitor = visitorcallable(engine.dialect, conn) if hasattr(element, '__migrate_visit_name__'): fn = getattr(visitor, 'visit_' + element.__migrate_visit_name__) else: fn = getattr(visitor, 'visit_' + element.__visit_name__) fn(element, **kwargs) finally: - conn.close() + if connection is None: + conn.close() diff --git a/migrate/changeset/schema.py b/migrate/changeset/schema.py index 22f86cc..349b497 100644 --- a/migrate/changeset/schema.py +++ b/migrate/changeset/schema.py @@ -426,19 +426,21 @@ class ChangesetTable(object): column = sqlalchemy.Column(str(column), sqlalchemy.Integer()) column.drop(table=self, *p, **kw) - def rename(self, name, *args, **kwargs): + def rename(self, name, connection=None, **kwargs): """Rename this table. :param name: New name of the table. :type name: string :param alter_metadata: If True, table will be removed from metadata :type alter_metadata: bool + :param connection: reuse connection istead of creating new one. + :type connection: :class:`sqlalchemy.engine.base.Connection` instance """ self.alter_metadata = kwargs.pop('alter_metadata', DEFAULT_ALTER_METADATA) engine = self.bind self.new_name = name visitorcallable = get_engine_visitor(engine, 'schemachanger') - run_single_visitor(engine, visitorcallable, self, *args, **kwargs) + run_single_visitor(engine, visitorcallable, self, connection, **kwargs) # Fix metadata registration if self.alter_metadata: @@ -485,7 +487,7 @@ class ChangesetColumn(object): return alter_column(self, *p, **k) def create(self, table=None, index_name=None, unique_name=None, - primary_key_name=None, *args, **kwargs): + primary_key_name=None, connection=None, **kwargs): """Create this column in the database. Assumes the given table exists. ``ALTER TABLE ADD COLUMN``, @@ -500,12 +502,16 @@ class ChangesetColumn(object): :param alter_metadata: If True, column will be added to table object. :param populate_default: If True, created column will be \ populated with defaults + :param connection: reuse connection istead of creating new one. :type table: Table instance :type index_name: string :type unique_name: string :type primary_key_name: string :type alter_metadata: bool :type populate_default: bool + :type connection: :class:`sqlalchemy.engine.base.Connection` instance + + :returns: self """ self.populate_default = kwargs.pop('populate_default', False) self.alter_metadata = kwargs.pop('alter_metadata', DEFAULT_ALTER_METADATA) @@ -514,26 +520,29 @@ populated with defaults self.primary_key_name = primary_key_name for cons in ('index_name', 'unique_name', 'primary_key_name'): self._check_sanity_constraints(cons) - + if self.alter_metadata: self.add_to_table(table) engine = self.table.bind visitorcallable = get_engine_visitor(engine, 'columngenerator') - engine._run_visitor(visitorcallable, self, *args, **kwargs) + engine._run_visitor(visitorcallable, self, connection, **kwargs) + # TODO: reuse existing connection if self.populate_default and self.default is not None: stmt = table.update().values({self: engine._execute_default(self.default)}) engine.execute(stmt) return self - def drop(self, table=None, *args, **kwargs): + def drop(self, table=None, connection=None, **kwargs): """Drop this column from the database, leaving its table intact. ``ALTER TABLE DROP COLUMN``, for most databases. :param alter_metadata: If True, column will be removed from table object. :type alter_metadata: bool + :param connection: reuse connection istead of creating new one. + :type connection: :class:`sqlalchemy.engine.base.Connection` instance """ self.alter_metadata = kwargs.pop('alter_metadata', DEFAULT_ALTER_METADATA) if table is not None: @@ -542,7 +551,7 @@ populated with defaults if self.alter_metadata: self.remove_from_table(self.table, unset_table=False) visitorcallable = get_engine_visitor(engine, 'columndropper') - engine._run_visitor(visitorcallable, self, *args, **kwargs) + engine._run_visitor(visitorcallable, self, connection, **kwargs) if self.alter_metadata: self.table = None return self @@ -557,7 +566,7 @@ populated with defaults self.table = None if table.c.contains_column(self): table.c.remove(self) - + # TODO: this is fixed in 0.6 def copy_fixed(self, **kw): """Create a copy of this ``Column``, with all attributes.""" @@ -590,19 +599,21 @@ class ChangesetIndex(object): __visit_name__ = 'index' - def rename(self, name, *args, **kwargs): + def rename(self, name, connection=None, **kwargs): """Change the name of an index. :param name: New name of the Index. :type name: string :param alter_metadata: If True, Index object will be altered. :type alter_metadata: bool + :param connection: reuse connection istead of creating new one. + :type connection: :class:`sqlalchemy.engine.base.Connection` instance """ self.alter_metadata = kwargs.pop('alter_metadata', DEFAULT_ALTER_METADATA) engine = self.table.bind self.new_name = name visitorcallable = get_engine_visitor(engine, 'schemachanger') - engine._run_visitor(visitorcallable, self, *args, **kwargs) + engine._run_visitor(visitorcallable, self, connection, **kwargs) if self.alter_metadata: self.name = name