]> git.ipfire.org Git - thirdparty/sqlalchemy/alembic.git/commitdiff
- Implemented support for :meth:`.BatchOperations.create_primary_key`
authorMike Bayer <mike_mp@zzzcomputing.com>
Fri, 3 Jul 2015 21:29:17 +0000 (17:29 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Fri, 3 Jul 2015 21:36:27 +0000 (17:36 -0400)
and :meth:`.BatchOperations.create_check_constraint`.
fixes #305
- table keyword arguments are copied from the original reflected table,
such as the "mysql_engine" keyword argument.

alembic/batch.py
alembic/ddl/postgresql.py
alembic/operations.py
alembic/testing/requirements.py
docs/build/changelog.rst
tests/requirements.py
tests/test_batch.py

index 1006739651ad70e092b1be7d98947ba8ea8a95a2..6f6306b8b4a34f1ca9129c025f0e87685817855f 100644 (file)
@@ -23,7 +23,7 @@ class BatchOperationsImpl(object):
         self.recreate = recreate
         self.copy_from = copy_from
         self.table_args = table_args
-        self.table_kwargs = table_kwargs
+        self.table_kwargs = dict(table_kwargs)
         self.reflect_args = reflect_args
         self.reflect_kwargs = reflect_kwargs
         self.naming_convention = naming_convention
@@ -139,11 +139,15 @@ class ApplyBatchImpl(object):
         for idx in self.table.indexes:
             self.indexes[idx.name] = idx
 
+        for k in self.table.kwargs:
+            self.table_kwargs.setdefault(k, self.table.kwargs[k])
+
     def _transfer_elements_to_new_table(self):
         assert self.new_table is None, "Can only create new table once"
 
         m = MetaData()
         schema = self.table.schema
+
         self.new_table = new_table = Table(
             '_alembic_batch_temp', m,
             *(list(self.columns.values()) + list(self.table_args)),
@@ -264,6 +268,10 @@ class ApplyBatchImpl(object):
     def add_constraint(self, const):
         if not const.name:
             raise ValueError("Constraint must have a name")
+        if isinstance(const, sql_schema.PrimaryKeyConstraint):
+            if self.table.primary_key in self.unnamed_constraints:
+                self.unnamed_constraints.remove(self.table.primary_key)
+
         self.named_constraints[const.name] = const
 
     def drop_constraint(self, const):
index 9f97b3450f1345436c060526a0d6d07056283fd1..faeabad1ac8d1dc5f204316ff912c4cffedf4436 100644 (file)
@@ -23,7 +23,8 @@ class PostgresqlImpl(DefaultImpl):
 
     def prep_table_for_batch(self, table):
         for constraint in table.constraints:
-            self.drop_constraint(constraint)
+            if constraint.name is not None:
+                self.drop_constraint(constraint)
 
     def compare_server_default(self, inspector_column,
                                metadata_column,
index 2bf806055fc1f8bbf7114924f1225a2c39397b30..dcacd06caabc0f718b95f9e3d7eec81a1ce024df 100644 (file)
@@ -67,11 +67,12 @@ class Operations(object):
     def _primary_key_constraint(self, name, table_name, cols, schema=None):
         m = self._metadata()
         columns = [sa_schema.Column(n, NULLTYPE) for n in cols]
-        t1 = sa_schema.Table(table_name, m,
-                             *columns,
-                             schema=schema)
-        p = sa_schema.PrimaryKeyConstraint(*columns, name=name)
-        t1.append_constraint(p)
+        t = sa_schema.Table(
+            table_name, m,
+            *columns,
+            schema=schema)
+        p = sa_schema.PrimaryKeyConstraint(*[t.c[n] for n in cols], name=name)
+        t.append_constraint(p)
         return p
 
     def _foreign_key_constraint(self, name, source, referent,
@@ -1356,7 +1357,7 @@ class BatchOperations(Operations):
         return super(BatchOperations, self).drop_column(
             self.impl.table_name, column_name, schema=self.impl.schema)
 
-    def create_primary_key(self, name, cols):
+    def create_primary_key(self, name, cols, **kw):
         """Issue a "create primary key" instruction using the
         current batch migration context.
 
@@ -1368,7 +1369,9 @@ class BatchOperations(Operations):
             :meth:`.Operations.create_primary_key`
 
         """
-        raise NotImplementedError("not yet implemented")
+        kw['schema'] = self.impl.schema
+        return super(BatchOperations, self).create_primary_key(
+            name, self.impl.table_name, cols, **kw)
 
     def create_foreign_key(
             self, name, referent, local_cols, remote_cols, **kw):
@@ -1422,7 +1425,9 @@ class BatchOperations(Operations):
             :meth:`.Operations.create_check_constraint`
 
         """
-        raise NotImplementedError("not yet implemented")
+        kw['schema'] = self.impl.schema
+        return super(BatchOperations, self).create_check_constraint(
+            name, self.impl.table_name, condition, **kw)
 
     def drop_constraint(self, name, type_=None):
         """Issue a "drop constraint" instruction using the
index b98195128a5c241a2f731b2a140e0dc410d186f5..2889ea57a806a02865939861c09c2fa13bbf1b42 100644 (file)
@@ -31,6 +31,17 @@ class SuiteRequirements(Requirements):
             "MATCH for foreign keys added in SQLAlchemy 0.8.0"
         )
 
+    @property
+    def check_constraints_w_enforcement(self):
+        """Target database must support check constraints
+        and also enforce them."""
+
+        return exclusions.open()
+
+    @property
+    def reflects_pk_names(self):
+        return exclusions.closed()
+
     @property
     def fail_before_sqla_079(self):
         return exclusions.fails_if(
index 193c87fcea82003aae11143eded50f8a24d2fed4..473a1ebf92cad3fb1c8fec3ab8ae541479c9b1a3 100644 (file)
@@ -6,6 +6,16 @@ Changelog
 .. changelog::
     :version: 0.7.7
 
+    .. change::
+      :tags: feature, batch
+      :tickets: 305
+
+      Implemented support for :meth:`.BatchOperations.create_primary_key`
+      and :meth:`.BatchOperations.create_check_constraint`.  Additionally,
+      table keyword arguments are copied from the original reflected table,
+      such as the "mysql_engine" keyword argument.
+
+
     .. change::
       :tags: bug, environment
       :tickets: 300
index 03049196b2788182d53722a1124c5606b62e1395..c5d538dfa54dd3521805b015c57b23cc35317c92 100644 (file)
@@ -40,6 +40,10 @@ class DefaultRequirements(SuiteRequirements):
             and not util.sqla_100
         )
 
+    @property
+    def check_constraints_w_enforcement(self):
+        return exclusions.fails_on("mysql")
+
     @property
     def unnamed_constraints(self):
         """constraints without names are supported."""
@@ -53,3 +57,10 @@ class DefaultRequirements(SuiteRequirements):
     @property
     def reflects_unique_constraints_unambiguously(self):
         return exclusions.fails_on("mysql")
+
+    @property
+    def reflects_pk_names(self):
+        """Target driver reflects the name of primary key constraints."""
+
+        return exclusions.fails_on_everything_except(
+            'postgresql', 'oracle', 'mssql', 'sybase')
index a498c3635125c73cdb0f31689d94da2ab32e52cd..ce16422319d833dfd85e9421d456557956737f54 100644 (file)
@@ -4,6 +4,7 @@ import re
 import io
 
 from alembic.testing import exclusions
+from alembic.testing import assert_raises_message
 from alembic.testing import TestBase, eq_, config
 from alembic.testing.fixtures import op_fixture
 from alembic.testing import mock
@@ -16,8 +17,9 @@ from sqlalchemy import Integer, Table, Column, String, MetaData, ForeignKey, \
     UniqueConstraint, ForeignKeyConstraint, Index, Boolean, CheckConstraint, \
     Enum
 from sqlalchemy.engine.reflection import Inspector
-from sqlalchemy.sql import column
+from sqlalchemy.sql import column, text
 from sqlalchemy.schema import CreateTable, CreateIndex
+from sqlalchemy import exc
 
 
 class BatchApplyTest(TestBase):
@@ -627,6 +629,50 @@ class BatchAPITest(TestBase):
                 self.mock_schema.UniqueConstraint())]
         )
 
+    def test_create_pk(self):
+        with self._fixture() as batch:
+            batch.create_primary_key('pk1', ['a', 'b'])
+
+        eq_(
+            self.mock_schema.Table().c.__getitem__.mock_calls,
+            [mock.call('a'), mock.call('b')]
+        )
+
+        eq_(
+            self.mock_schema.PrimaryKeyConstraint.mock_calls,
+            [
+                mock.call(
+                    self.mock_schema.Table().c.__getitem__(),
+                    self.mock_schema.Table().c.__getitem__(),
+                    name='pk1'
+                )
+            ]
+        )
+        eq_(
+            batch.impl.operations.impl.mock_calls,
+            [mock.call.add_constraint(
+                self.mock_schema.PrimaryKeyConstraint())]
+        )
+
+    def test_create_check(self):
+        expr = text("a > b")
+        with self._fixture() as batch:
+            batch.create_check_constraint('ck1', expr)
+
+        eq_(
+            self.mock_schema.CheckConstraint.mock_calls,
+            [
+                mock.call(
+                    expr, name="ck1"
+                )
+            ]
+        )
+        eq_(
+            batch.impl.operations.impl.mock_calls,
+            [mock.call.add_constraint(
+                self.mock_schema.CheckConstraint())]
+        )
+
     def test_drop_constraint(self):
         with self._fixture() as batch:
             batch.drop_constraint('uq1')
@@ -795,6 +841,25 @@ class BatchRoundTripTest(TestBase):
         context = MigrationContext.configure(self.conn)
         self.op = Operations(context)
 
+    def _no_pk_fixture(self):
+        nopk = Table(
+            'nopk', self.metadata,
+            Column('a', Integer),
+            Column('b', Integer),
+            Column('c', Integer),
+            mysql_engine='InnoDB'
+        )
+        nopk.create(self.conn)
+        self.conn.execute(
+            nopk.insert(),
+            [
+                {"a": 1, "b": 2, "c": 3},
+                {"a": 2, "b": 4, "c": 5},
+            ]
+
+        )
+        return nopk
+
     def tearDown(self):
         self.metadata.drop_all(self.conn)
         self.conn.close()
@@ -854,6 +919,32 @@ class BatchRoundTripTest(TestBase):
             {"id": 5, "x": 9}
         ])
 
+    def test_add_pk_constraint(self):
+        self._no_pk_fixture()
+        with self.op.batch_alter_table("nopk", recreate="always") as batch_op:
+            batch_op.create_primary_key('newpk', ['a', 'b'])
+
+        pk_const = Inspector.from_engine(self.conn).get_pk_constraint('nopk')
+        with config.requirements.reflects_pk_names.fail_if():
+            eq_(pk_const['name'], 'newpk')
+        eq_(pk_const['constrained_columns'], ['a', 'b'])
+
+    @config.requirements.check_constraints_w_enforcement
+    def test_add_ck_constraint(self):
+        with self.op.batch_alter_table("foo", recreate="always") as batch_op:
+            batch_op.create_check_constraint("newck", text("x > 0"))
+
+        # we dont support reflection of CHECK constraints
+        # so test this by just running invalid data in
+        foo = self.metadata.tables['foo']
+
+        assert_raises_message(
+            exc.IntegrityError,
+            "newck",
+            self.conn.execute,
+            foo.insert(), {"id": 6, "data": 5, "x": -2}
+        )
+
     @config.requirements.sqlalchemy_094
     @config.requirements.unnamed_constraints
     def test_drop_foreign_key(self):