- Fully implemented the

:paramref:`~.Operations.batch_alter_table.copy_from` parameter for
batch mode, which previously was not functioning.  This allows
"batch mode" to be usable in conjunction with ``--sql``.
fixes #289
- sqlite dialect checks for "create_index" and "drop_index" as exceptions
for "recreate" in batch mode, the same way as "add_column", so that
unnecessary table recreates don't emit for index-only operations
This commit is contained in:
Mike Bayer
2015-03-27 18:55:00 -04:00
parent 9545e87008
commit f02f211b93
7 changed files with 182 additions and 11 deletions

View File

@@ -58,12 +58,15 @@ class BatchOperationsImpl(object):
else:
m1 = MetaData()
existing_table = Table(
self.table_name, m1,
schema=self.schema,
autoload=True,
autoload_with=self.operations.get_bind(),
*self.reflect_args, **self.reflect_kwargs)
if self.copy_from is not None:
existing_table = self.copy_from
else:
existing_table = Table(
self.table_name, m1,
schema=self.schema,
autoload=True,
autoload_with=self.operations.get_bind(),
*self.reflect_args, **self.reflect_kwargs)
batch_impl = ApplyBatchImpl(
existing_table, self.table_args, self.table_kwargs)

View File

@@ -21,7 +21,7 @@ class SQLiteImpl(DefaultImpl):
"""
for op in batch_op.batch:
if op[0] != 'add_column':
if op[0] not in ('add_column', 'create_index', 'drop_index'):
return True
else:
return False

View File

@@ -242,20 +242,28 @@ class Operations(object):
.. note:: The table copy operation will currently not copy
CHECK constraints, and may not copy UNIQUE constraints that are
unnamed, as is possible on SQLite.
unnamed, as is possible on SQLite. See the section
:ref:`sqlite_batch_constraints` for workarounds.
:param table_name: name of table
:param schema: optional schema name.
:param recreate: under what circumstances the table should be
recreated. At its default of ``"auto"``, the SQLite dialect will
recreate the table if any operations other than ``add_column()`` are
recreate the table if any operations other than ``add_column()``,
``create_index()``, or ``drop_index()`` are
present. Other options include ``"always"`` and ``"never"``.
:param copy_from: optional :class:`~sqlalchemy.schema.Table` object
that will act as the structure of the table being copied. If omitted,
table reflection is used to retrieve the structure of the table.
.. versionadded:: 0.7.6 Fully implemented the
:paramref:`~.Operations.batch_alter_table.copy_from`
parameter.
.. seealso::
:ref:`batch_offline_mode`
:paramref:`~.Operations.batch_alter_table.reflect_args`
:paramref:`~.Operations.batch_alter_table.reflect_kwargs`

View File

@@ -100,7 +100,17 @@ def op_fixture(dialect='default', as_sql=False, naming_convention=None):
# TODO: this might need to
# be more like a real connection
# as tests get more involved
self.connection = mock.Mock(dialect=dialect)
if as_sql and self.dialect.name != 'default':
# act similarly to MigrationContext
def dump(construct, *multiparams, **params):
self._exec(construct)
self.connection = create_engine(
"%s://" % self.dialect.name,
strategy="mock", executor=dump)
else:
self.connection = mock.Mock(dialect=dialect)
def _exec(self, construct, *args, **kw):
if isinstance(construct, string_types):
@@ -128,6 +138,9 @@ def op_fixture(dialect='default', as_sql=False, naming_convention=None):
self.opts = opts
self.as_sql = as_sql
def clear_assertions(self):
self.impl.assertion[:] = []
def assert_(self, *sql):
# TODO: make this more flexible about
# whitespace and such

View File

@@ -110,6 +110,8 @@ pre-fabricated :class:`~sqlalchemy.schema.Table` object; see
added :paramref:`.Operations.batch_alter_table.reflect_args`
and :paramref:`.Operations.batch_alter_table.reflect_kwargs` options.
.. _sqlite_batch_constraints:
Dealing with Constraints
------------------------
@@ -251,6 +253,10 @@ preferred style of working; however, if one needs to do SQLite-compatible
"move and copy" migrations and need them to generate flat SQL files in
"offline" mode, there's not much alternative.
.. versionadded:: 0.7.6 Fully implemented the
:paramref:`~.Operations.batch_alter_table.copy_from`
parameter.
Batch mode with Autogenerate
----------------------------

View File

@@ -6,13 +6,28 @@ Changelog
.. changelog::
:version: 0.7.6
.. change::
:tags: bug, batch
:tickets: 289
Fully implemented the
:paramref:`~.Operations.batch_alter_table.copy_from` parameter for
batch mode, which previously was not functioning. This allows
"batch mode" to be usable in conjunction with ``--sql``.
.. change::
:tags: bug, batch
:tickets: 287
Repaired support for the :meth:`.BatchOperations.create_index`
directive, which was mis-named internally such that the operation
within a batch context could not proceed.
within a batch context could not proceed. The create index
operation will proceed as part of a larger "batch table recreate"
operation only if
:paramref:`~.Operations.batch_alter_table.recreate` is set to
"always", or if the batch operation includes other instructions that
require a table recreate.
.. changelog::
:version: 0.7.5

View File

@@ -1,6 +1,8 @@
from contextlib import contextmanager
import re
import io
from alembic.testing import exclusions
from alembic.testing import TestBase, eq_, config
from alembic.testing.fixtures import op_fixture
@@ -9,6 +11,7 @@ from alembic.operations import Operations
from alembic.batch import ApplyBatchImpl
from alembic.migration import MigrationContext
from sqlalchemy import inspect
from sqlalchemy import Integer, Table, Column, String, MetaData, ForeignKey, \
UniqueConstraint, ForeignKeyConstraint, Index, Boolean, CheckConstraint, \
@@ -641,6 +644,129 @@ class BatchAPITest(TestBase):
)
class CopyFromTest(TestBase):
__requires__ = ('sqlalchemy_08', )
def _fixture(self):
self.metadata = MetaData()
self.table = Table(
'foo', self.metadata,
Column('id', Integer, primary_key=True),
Column('data', String(50)),
Column('x', Integer),
)
context = op_fixture(dialect="sqlite", as_sql=True)
self.op = Operations(context)
return context
def test_change_type(self):
context = self._fixture()
with self.op.batch_alter_table(
"foo", copy_from=self.table) as batch_op:
batch_op.alter_column('data', type_=Integer)
context.assert_(
'CREATE TABLE _alembic_batch_temp (id INTEGER NOT NULL, '
'data INTEGER, x INTEGER, PRIMARY KEY (id))',
'INSERT INTO _alembic_batch_temp (id, data, x) SELECT foo.id, '
'CAST(foo.data AS INTEGER) AS anon_1, foo.x FROM foo',
'DROP TABLE foo',
'ALTER TABLE _alembic_batch_temp RENAME TO foo'
)
def test_create_drop_index_w_always(self):
context = self._fixture()
with self.op.batch_alter_table(
"foo", copy_from=self.table, recreate='always') as batch_op:
batch_op.create_index(
batch_op.f('ix_data'), ['data'], unique=True)
context.assert_(
'CREATE TABLE _alembic_batch_temp (id INTEGER NOT NULL, '
'data VARCHAR(50), '
'x INTEGER, PRIMARY KEY (id))',
'CREATE UNIQUE INDEX ix_data ON _alembic_batch_temp (data)',
'INSERT INTO _alembic_batch_temp (id, data, x) '
'SELECT foo.id, foo.data, foo.x FROM foo',
'DROP TABLE foo',
'ALTER TABLE _alembic_batch_temp RENAME TO foo'
)
context.clear_assertions()
Index('ix_data', self.table.c.data, unique=True)
with self.op.batch_alter_table(
"foo", copy_from=self.table, recreate='always') as batch_op:
batch_op.drop_index('ix_data')
context.assert_(
'CREATE TABLE _alembic_batch_temp (id INTEGER NOT NULL, '
'data VARCHAR(50), x INTEGER, PRIMARY KEY (id))',
'INSERT INTO _alembic_batch_temp (id, data, x) '
'SELECT foo.id, foo.data, foo.x FROM foo',
'DROP TABLE foo',
'ALTER TABLE _alembic_batch_temp RENAME TO foo'
)
def test_create_drop_index_wo_always(self):
context = self._fixture()
with self.op.batch_alter_table(
"foo", copy_from=self.table) as batch_op:
batch_op.create_index(
batch_op.f('ix_data'), ['data'], unique=True)
context.assert_(
'CREATE UNIQUE INDEX ix_data ON foo (data)'
)
context.clear_assertions()
Index('ix_data', self.table.c.data, unique=True)
with self.op.batch_alter_table(
"foo", copy_from=self.table) as batch_op:
batch_op.drop_index('ix_data')
context.assert_(
'DROP INDEX ix_data'
)
def test_create_drop_index_w_other_ops(self):
context = self._fixture()
with self.op.batch_alter_table(
"foo", copy_from=self.table) as batch_op:
batch_op.alter_column('data', type_=Integer)
batch_op.create_index(
batch_op.f('ix_data'), ['data'], unique=True)
context.assert_(
'CREATE TABLE _alembic_batch_temp (id INTEGER NOT NULL, '
'data INTEGER, x INTEGER, PRIMARY KEY (id))',
'CREATE UNIQUE INDEX ix_data ON _alembic_batch_temp (data)',
'INSERT INTO _alembic_batch_temp (id, data, x) SELECT foo.id, '
'CAST(foo.data AS INTEGER) AS anon_1, foo.x FROM foo',
'DROP TABLE foo',
'ALTER TABLE _alembic_batch_temp RENAME TO foo'
)
context.clear_assertions()
Index('ix_data', self.table.c.data, unique=True)
with self.op.batch_alter_table(
"foo", copy_from=self.table) as batch_op:
batch_op.drop_index('ix_data')
batch_op.alter_column('data', type_=String)
context.assert_(
'CREATE TABLE _alembic_batch_temp (id INTEGER NOT NULL, '
'data VARCHAR, x INTEGER, PRIMARY KEY (id))',
'INSERT INTO _alembic_batch_temp (id, data, x) SELECT foo.id, '
'CAST(foo.data AS VARCHAR) AS anon_1, foo.x FROM foo',
'DROP TABLE foo',
'ALTER TABLE _alembic_batch_temp RENAME TO foo'
)
class BatchRoundTripTest(TestBase):
__requires__ = ('sqlalchemy_08', )
__only_on__ = "sqlite"