]> git.ipfire.org Git - thirdparty/sqlalchemy/alembic.git/commitdiff
- Fully implemented the
authorMike Bayer <mike_mp@zzzcomputing.com>
Fri, 27 Mar 2015 22:55:00 +0000 (18:55 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Fri, 27 Mar 2015 22:55:00 +0000 (18:55 -0400)
: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

alembic/batch.py
alembic/ddl/sqlite.py
alembic/operations.py
alembic/testing/fixtures.py
docs/build/batch.rst
docs/build/changelog.rst
tests/test_batch.py

index 6e5dc75d0e5ff4a4189e0d30cd2633da725cadcf..1006739651ad70e092b1be7d98947ba8ea8a95a2 100644 (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)
index 16beddf90b8c0101ab2107e2b204687f1b91db1f..5d231b5f744f0761c7340ad48dc418115ee77496 100644 (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
index 683d2bda3030cd7d41afbd9a44977501dd2a8d57..485943ee89c5bbbcada351e17ba5cf8f770fe431 100644 (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`
index 6336967fe9cec1aebf91d1e74e8c93b69b8d5917..4091388d2a94b5fae38600c0608fccfea984988c 100644 (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
index 307d2a1877a90cdeb63a763e2dff9359320f638e..64eeefb247d6f1e2f7345d3c9a7a6d1d355bce30 100644 (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
 ----------------------------
index 1d730e1ed205c45bd29be9d71f68374245c70937..9b27cc7214d447237e43f00a22a04a923c0bbbcc 100644 (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
index 76f0c1298233d26b974cba94cd564cef2edb4aab..ffd88cbb701b226634b4995686421bbddd257354 100644 (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"