]> git.ipfire.org Git - thirdparty/sqlalchemy/alembic.git/commitdiff
Add support of IF [NOT] EXISTS for ADD/DROP COLUMN in Postgresql
authorLouis-Amaury Chaib <louisamaury.chaib@partoo.fr>
Mon, 19 May 2025 13:11:00 +0000 (09:11 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Mon, 19 May 2025 16:03:15 +0000 (12:03 -0400)
Added :paramref:`.Operations.add_column.if_not_exists` and
:paramref:`.Operations.drop_column.if_exists` to render ``IF [NOT] EXISTS``
for ``ADD COLUMN`` and ``DROP COLUMN`` operations, a feature available on
some database backends such as PostgreSQL, MariaDB, as well as third party
backends.  The parameters also support autogenerate rendering allowing them
to be turned on via a custom :class:`.Rewriter`. Pull request courtesy of
Louis-Amaury Chaib (@lachaib).

Fixes: #1626
Closes: #1627
Pull-request: https://github.com/sqlalchemy/alembic/pull/1627
Pull-request-sha: c503b049f453a12e7fdb87464606cddd58ad306c

Change-Id: I4d55f65f072b9f03698b2f45f066872b5c3e8c58

17 files changed:
alembic/autogenerate/render.py
alembic/ddl/base.py
alembic/ddl/impl.py
alembic/ddl/mssql.py
alembic/ddl/mysql.py
alembic/ddl/postgresql.py
alembic/op.pyi
alembic/operations/base.py
alembic/operations/ops.py
alembic/operations/toimpl.py
docs/build/cookbook.rst
docs/build/unreleased/1626.rst [new file with mode: 0644]
tests/test_autogen_render.py
tests/test_batch.py
tests/test_mysql.py
tests/test_op.py
tests/test_postgresql.py

index 66c67673360380e28ad9f9cc3dff8841a11ae131..50e7057dbc20efda814a814cbd5bd0a39a9c1119 100644 (file)
@@ -457,26 +457,39 @@ def _drop_constraint(
 
 @renderers.dispatch_for(ops.AddColumnOp)
 def _add_column(autogen_context: AutogenContext, op: ops.AddColumnOp) -> str:
-    schema, tname, column = op.schema, op.table_name, op.column
+    schema, tname, column, if_not_exists = (
+        op.schema,
+        op.table_name,
+        op.column,
+        op.if_not_exists,
+    )
     if autogen_context._has_batch:
         template = "%(prefix)sadd_column(%(column)s)"
     else:
         template = "%(prefix)sadd_column(%(tname)r, %(column)s"
         if schema:
             template += ", schema=%(schema)r"
+        if if_not_exists is not None:
+            template += ", if_not_exists=%(if_not_exists)r"
         template += ")"
     text = template % {
         "prefix": _alembic_autogenerate_prefix(autogen_context),
         "tname": tname,
         "column": _render_column(column, autogen_context),
         "schema": schema,
+        "if_not_exists": if_not_exists,
     }
     return text
 
 
 @renderers.dispatch_for(ops.DropColumnOp)
 def _drop_column(autogen_context: AutogenContext, op: ops.DropColumnOp) -> str:
-    schema, tname, column_name = op.schema, op.table_name, op.column_name
+    schema, tname, column_name, if_exists = (
+        op.schema,
+        op.table_name,
+        op.column_name,
+        op.if_exists,
+    )
 
     if autogen_context._has_batch:
         template = "%(prefix)sdrop_column(%(cname)r)"
@@ -484,6 +497,8 @@ def _drop_column(autogen_context: AutogenContext, op: ops.DropColumnOp) -> str:
         template = "%(prefix)sdrop_column(%(tname)r, %(cname)r"
         if schema:
             template += ", schema=%(schema)r"
+        if if_exists is not None:
+            template += ", if_exists=%(if_exists)r"
         template += ")"
 
     text = template % {
@@ -491,6 +506,7 @@ def _drop_column(autogen_context: AutogenContext, op: ops.DropColumnOp) -> str:
         "tname": _ident(tname),
         "cname": _ident(column_name),
         "schema": _ident(schema),
+        "if_exists": if_exists,
     }
     return text
 
index 7c36a887326b27f8b46c6d1f04c1955023583385..ad2847eb2f76066264f2218ede2e173032082f92 100644 (file)
@@ -154,17 +154,24 @@ class AddColumn(AlterTable):
         name: str,
         column: Column[Any],
         schema: Optional[Union[quoted_name, str]] = None,
+        if_not_exists: Optional[bool] = None,
     ) -> None:
         super().__init__(name, schema=schema)
         self.column = column
+        self.if_not_exists = if_not_exists
 
 
 class DropColumn(AlterTable):
     def __init__(
-        self, name: str, column: Column[Any], schema: Optional[str] = None
+        self,
+        name: str,
+        column: Column[Any],
+        schema: Optional[str] = None,
+        if_exists: Optional[bool] = None,
     ) -> None:
         super().__init__(name, schema=schema)
         self.column = column
+        self.if_exists = if_exists
 
 
 class ColumnComment(AlterColumn):
@@ -189,7 +196,9 @@ def visit_rename_table(
 def visit_add_column(element: AddColumn, compiler: DDLCompiler, **kw) -> str:
     return "%s %s" % (
         alter_table(compiler, element.table_name, element.schema),
-        add_column(compiler, element.column, **kw),
+        add_column(
+            compiler, element.column, if_not_exists=element.if_not_exists, **kw
+        ),
     )
 
 
@@ -197,7 +206,9 @@ def visit_add_column(element: AddColumn, compiler: DDLCompiler, **kw) -> str:
 def visit_drop_column(element: DropColumn, compiler: DDLCompiler, **kw) -> str:
     return "%s %s" % (
         alter_table(compiler, element.table_name, element.schema),
-        drop_column(compiler, element.column.name, **kw),
+        drop_column(
+            compiler, element.column.name, if_exists=element.if_exists, **kw
+        ),
     )
 
 
@@ -320,16 +331,29 @@ def alter_table(
     return "ALTER TABLE %s" % format_table_name(compiler, name, schema)
 
 
-def drop_column(compiler: DDLCompiler, name: str, **kw) -> str:
-    return "DROP COLUMN %s" % format_column_name(compiler, name)
+def drop_column(
+    compiler: DDLCompiler, name: str, if_exists: Optional[bool] = None, **kw
+) -> str:
+    return "DROP COLUMN %s%s" % (
+        "IF EXISTS " if if_exists else "",
+        format_column_name(compiler, name),
+    )
 
 
 def alter_column(compiler: DDLCompiler, name: str) -> str:
     return "ALTER COLUMN %s" % format_column_name(compiler, name)
 
 
-def add_column(compiler: DDLCompiler, column: Column[Any], **kw) -> str:
-    text = "ADD COLUMN %s" % compiler.get_column_specification(column, **kw)
+def add_column(
+    compiler: DDLCompiler,
+    column: Column[Any],
+    if_not_exists: Optional[bool] = None,
+    **kw,
+) -> str:
+    text = "ADD COLUMN %s%s" % (
+        "IF NOT EXISTS " if if_not_exists else "",
+        compiler.get_column_specification(column, **kw),
+    )
 
     const = " ".join(
         compiler.process(constraint) for constraint in column.constraints
index a10702231f1803129d4d11c558509d63b5ca99f9..d352f12ee7dd90ce5121d81908e0f915b03d2e23 100644 (file)
@@ -256,6 +256,7 @@ class DefaultImpl(metaclass=ImplMeta):
         self,
         table_name: str,
         column_name: str,
+        *,
         nullable: Optional[bool] = None,
         server_default: Optional[
             Union[_ServerDefault, Literal[False]]
@@ -370,18 +371,33 @@ class DefaultImpl(metaclass=ImplMeta):
         self,
         table_name: str,
         column: Column[Any],
+        *,
         schema: Optional[Union[str, quoted_name]] = None,
+        if_not_exists: Optional[bool] = None,
     ) -> None:
-        self._exec(base.AddColumn(table_name, column, schema=schema))
+        self._exec(
+            base.AddColumn(
+                table_name,
+                column,
+                schema=schema,
+                if_not_exists=if_not_exists,
+            )
+        )
 
     def drop_column(
         self,
         table_name: str,
         column: Column[Any],
+        *,
         schema: Optional[str] = None,
+        if_exists: Optional[bool] = None,
         **kw,
     ) -> None:
-        self._exec(base.DropColumn(table_name, column, schema=schema))
+        self._exec(
+            base.DropColumn(
+                table_name, column, schema=schema, if_exists=if_exists
+            )
+        )
 
     def add_constraint(self, const: Any) -> None:
         if const._create_rule is None or const._create_rule(self):
index baa43d5e73abb3e40294c0000d0f2694182744eb..5376da5adece8799051c35126bbddcf938acef0f 100644 (file)
@@ -83,10 +83,11 @@ class MSSQLImpl(DefaultImpl):
         if self.as_sql and self.batch_separator:
             self.static_output(self.batch_separator)
 
-    def alter_column(  # type:ignore[override]
+    def alter_column(
         self,
         table_name: str,
         column_name: str,
+        *,
         nullable: Optional[bool] = None,
         server_default: Optional[
             Union[_ServerDefault, Literal[False]]
@@ -202,6 +203,7 @@ class MSSQLImpl(DefaultImpl):
         self,
         table_name: str,
         column: Column[Any],
+        *,
         schema: Optional[str] = None,
         **kw,
     ) -> None:
index d92e3cd7d76be7d7df75e51a2a767ca141a94719..3f8c0628e557ef244b06c5350ba23083c4af185d 100644 (file)
@@ -47,10 +47,11 @@ class MySQLImpl(DefaultImpl):
     )
     type_arg_extract = [r"character set ([\w\-_]+)", r"collate ([\w\-_]+)"]
 
-    def alter_column(  # type:ignore[override]
+    def alter_column(
         self,
         table_name: str,
         column_name: str,
+        *,
         nullable: Optional[bool] = None,
         server_default: Optional[
             Union[_ServerDefault, Literal[False]]
index 4be04c56ceae17bb02d8c36fa6f001e69b69edc2..90ecf70c19eaf3240ed9722786c91efb3129cc66 100644 (file)
@@ -52,6 +52,7 @@ from ..operations.base import Operations
 from ..util import sqla_compat
 from ..util.sqla_compat import compiles
 
+
 if TYPE_CHECKING:
     from typing import Literal
 
@@ -148,10 +149,11 @@ class PostgresqlImpl(DefaultImpl):
             select(literal_column(conn_col_default) == metadata_default)
         )
 
-    def alter_column(  # type:ignore[override]
+    def alter_column(
         self,
         table_name: str,
         column_name: str,
+        *,
         nullable: Optional[bool] = None,
         server_default: Optional[
             Union[_ServerDefault, Literal[False]]
index 548beb09e021fdc8918b847eebe2833b522150ef..f39ff0548e7c493782bab6e9fafcff81e7128141 100644 (file)
@@ -60,7 +60,11 @@ _C = TypeVar("_C", bound=Callable[..., Any])
 ### end imports ###
 
 def add_column(
-    table_name: str, column: Column[Any], *, schema: Optional[str] = None
+    table_name: str,
+    column: Column[Any],
+    *,
+    schema: Optional[str] = None,
+    if_not_exists: Optional[bool] = None,
 ) -> None:
     """Issue an "add column" instruction using the current
     migration context.
@@ -137,6 +141,10 @@ def add_column(
      quoting of the schema outside of the default behavior, use
      the SQLAlchemy construct
      :class:`~sqlalchemy.sql.elements.quoted_name`.
+    :param if_not_exists: If True, adds IF NOT EXISTS operator
+     when creating the new column for compatible dialects
+
+     .. versionadded:: 1.16.0
 
     """
 
@@ -927,6 +935,11 @@ def drop_column(
      quoting of the schema outside of the default behavior, use
      the SQLAlchemy construct
      :class:`~sqlalchemy.sql.elements.quoted_name`.
+    :param if_exists: If True, adds IF EXISTS operator when
+     dropping the new column for compatible dialects
+
+     .. versionadded:: 1.16.0
+
     :param mssql_drop_check: Optional boolean.  When ``True``, on
      Microsoft SQL Server only, first
      drop the CHECK constraint on the column using a
@@ -948,7 +961,6 @@ def drop_column(
      then exec's a separate DROP CONSTRAINT for that default.  Only
      works if the column has exactly one FK constraint which refers to
      it, at the moment.
-
     """
 
 def drop_constraint(
@@ -972,7 +984,7 @@ def drop_constraint(
     :param if_exists: If True, adds IF EXISTS operator when
      dropping the constraint
 
-     .. versionadded:: 1.15.3
+     .. versionadded:: 1.16.0
 
     """
 
index 8ce0c45a8a48e5c575c5cffebd4d6bb6dfc3c989..9f9750315132c54e43e6474993cd71f323604056 100644 (file)
@@ -618,6 +618,7 @@ class Operations(AbstractOperations):
             column: Column[Any],
             *,
             schema: Optional[str] = None,
+            if_not_exists: Optional[bool] = None,
         ) -> None:
             """Issue an "add column" instruction using the current
             migration context.
@@ -694,6 +695,10 @@ class Operations(AbstractOperations):
              quoting of the schema outside of the default behavior, use
              the SQLAlchemy construct
              :class:`~sqlalchemy.sql.elements.quoted_name`.
+            :param if_not_exists: If True, adds IF NOT EXISTS operator
+             when creating the new column for compatible dialects
+
+             .. versionadded:: 1.16.0
 
             """  # noqa: E501
             ...
@@ -1361,6 +1366,11 @@ class Operations(AbstractOperations):
              quoting of the schema outside of the default behavior, use
              the SQLAlchemy construct
              :class:`~sqlalchemy.sql.elements.quoted_name`.
+            :param if_exists: If True, adds IF EXISTS operator when
+             dropping the new column for compatible dialects
+
+             .. versionadded:: 1.16.0
+
             :param mssql_drop_check: Optional boolean.  When ``True``, on
              Microsoft SQL Server only, first
              drop the CHECK constraint on the column using a
@@ -1382,7 +1392,6 @@ class Operations(AbstractOperations):
              then exec's a separate DROP CONSTRAINT for that default.  Only
              works if the column has exactly one FK constraint which refers to
              it, at the moment.
-
             """  # noqa: E501
             ...
 
@@ -1408,7 +1417,7 @@ class Operations(AbstractOperations):
             :param if_exists: If True, adds IF EXISTS operator when
              dropping the constraint
 
-             .. versionadded:: 1.15.3
+             .. versionadded:: 1.16.0
 
             """  # noqa: E501
             ...
@@ -1651,6 +1660,7 @@ class BatchOperations(AbstractOperations):
             *,
             insert_before: Optional[str] = None,
             insert_after: Optional[str] = None,
+            if_not_exists: Optional[bool] = None,
         ) -> None:
             """Issue an "add column" instruction using the current
             batch migration context.
index 633defe8ffabea877fb5bf4c0096afabf7d2dc14..b26032726602235225d0d706d7e8484527194009 100644 (file)
@@ -220,7 +220,7 @@ class DropConstraintOp(MigrateOperation):
         :param if_exists: If True, adds IF EXISTS operator when
          dropping the constraint
 
-         .. versionadded:: 1.15.3
+         .. versionadded:: 1.16.0
 
         """
 
@@ -2047,16 +2047,20 @@ class AddColumnOp(AlterTableOp):
         column: Column[Any],
         *,
         schema: Optional[str] = None,
+        if_not_exists: Optional[bool] = None,
         **kw: Any,
     ) -> None:
         super().__init__(table_name, schema=schema)
         self.column = column
+        self.if_not_exists = if_not_exists
         self.kw = kw
 
     def reverse(self) -> DropColumnOp:
-        return DropColumnOp.from_column_and_tablename(
+        op = DropColumnOp.from_column_and_tablename(
             self.schema, self.table_name, self.column
         )
+        op.if_exists = self.if_not_exists
+        return op
 
     def to_diff_tuple(
         self,
@@ -2087,6 +2091,7 @@ class AddColumnOp(AlterTableOp):
         column: Column[Any],
         *,
         schema: Optional[str] = None,
+        if_not_exists: Optional[bool] = None,
     ) -> None:
         """Issue an "add column" instruction using the current
         migration context.
@@ -2163,10 +2168,19 @@ class AddColumnOp(AlterTableOp):
          quoting of the schema outside of the default behavior, use
          the SQLAlchemy construct
          :class:`~sqlalchemy.sql.elements.quoted_name`.
+        :param if_not_exists: If True, adds IF NOT EXISTS operator
+         when creating the new column for compatible dialects
+
+         .. versionadded:: 1.16.0
 
         """
 
-        op = cls(table_name, column, schema=schema)
+        op = cls(
+            table_name,
+            column,
+            schema=schema,
+            if_not_exists=if_not_exists,
+        )
         return operations.invoke(op)
 
     @classmethod
@@ -2177,6 +2191,7 @@ class AddColumnOp(AlterTableOp):
         *,
         insert_before: Optional[str] = None,
         insert_after: Optional[str] = None,
+        if_not_exists: Optional[bool] = None,
     ) -> None:
         """Issue an "add column" instruction using the current
         batch migration context.
@@ -2197,6 +2212,7 @@ class AddColumnOp(AlterTableOp):
             operations.impl.table_name,
             column,
             schema=operations.impl.schema,
+            if_not_exists=if_not_exists,
             **kw,
         )
         return operations.invoke(op)
@@ -2213,12 +2229,14 @@ class DropColumnOp(AlterTableOp):
         column_name: str,
         *,
         schema: Optional[str] = None,
+        if_exists: Optional[bool] = None,
         _reverse: Optional[AddColumnOp] = None,
         **kw: Any,
     ) -> None:
         super().__init__(table_name, schema=schema)
         self.column_name = column_name
         self.kw = kw
+        self.if_exists = if_exists
         self._reverse = _reverse
 
     def to_diff_tuple(
@@ -2238,9 +2256,11 @@ class DropColumnOp(AlterTableOp):
                 "original column is not present"
             )
 
-        return AddColumnOp.from_column_and_tablename(
+        op = AddColumnOp.from_column_and_tablename(
             self.schema, self.table_name, self._reverse.column
         )
+        op.if_not_exists = self.if_exists
+        return op
 
     @classmethod
     def from_column_and_tablename(
@@ -2287,6 +2307,11 @@ class DropColumnOp(AlterTableOp):
          quoting of the schema outside of the default behavior, use
          the SQLAlchemy construct
          :class:`~sqlalchemy.sql.elements.quoted_name`.
+        :param if_exists: If True, adds IF EXISTS operator when
+         dropping the new column for compatible dialects
+
+         .. versionadded:: 1.16.0
+
         :param mssql_drop_check: Optional boolean.  When ``True``, on
          Microsoft SQL Server only, first
          drop the CHECK constraint on the column using a
@@ -2308,7 +2333,6 @@ class DropColumnOp(AlterTableOp):
          then exec's a separate DROP CONSTRAINT for that default.  Only
          works if the column has exactly one FK constraint which refers to
          it, at the moment.
-
         """
 
         op = cls(table_name, column_name, schema=schema, **kw)
index bc5e73dddb89c9d4e9c95bb3eb39da710d988f05..c18ec790176d6db1a848e962f190202bbed47162 100644 (file)
@@ -93,7 +93,11 @@ def drop_column(
 ) -> None:
     column = operation.to_column(operations.migration_context)
     operations.impl.drop_column(
-        operation.table_name, column, schema=operation.schema, **operation.kw
+        operation.table_name,
+        column,
+        schema=operation.schema,
+        if_exists=operation.if_exists,
+        **operation.kw,
     )
 
 
@@ -168,7 +172,13 @@ def add_column(operations: "Operations", operation: "ops.AddColumnOp") -> None:
         column = _copy(column)
 
     t = operations.schema_obj.table(table_name, column, schema=schema)
-    operations.impl.add_column(table_name, column, schema=schema, **kw)
+    operations.impl.add_column(
+        table_name,
+        column,
+        schema=schema,
+        if_not_exists=operation.if_not_exists,
+        **kw,
+    )
 
     for constraint in t.constraints:
         if not isinstance(constraint, sa_schema.PrimaryKeyConstraint):
index e52753a8ee942167fed06b744524d043c43c346c..154a41650f2a1ba212fc56477424e2fc00f96e9d 100644 (file)
@@ -1175,6 +1175,50 @@ This will render in the autogenerated file as::
         )
         # ### end Alembic commands ###
 
+
+.. _cookbook_add_if_exists:
+
+
+Add IF [NOT] EXISTS to CREATE/DROP operations
+=============================================
+
+Sometimes CREATE/DROP operations take too long during a production deployment
+and it is preferable to apply them offline,
+and still keep alembic migrations aligned and/or for test environments.
+
+Using the rewriter makes it possible::
+
+    from alembic.operations import ops
+    from alembic.autogenerate import rewriter
+
+    writer = rewriter.Rewriter()
+
+    @writer.rewrites(ops.CreateTableOp)
+    @writer.rewrites(ops.CreateIndexOp)
+    def add_if_not_exists(context, revision, op):
+        op.if_not_exists = True
+        return op
+
+    @writer.rewrites(ops.DropTableOp)
+    @writer.rewrites(ops.DropIndexOp)
+    def add_if_exists(context, revision, op):
+        op.if_exists = True
+        return op
+
+Same operation is possible for ADD/DROP COLUMN on postgresql/mariadb::
+
+    @writer.rewrites(ops.AddColumnOp)
+    def add_column_if_not_exists(context, revision, op):
+        op.if_not_exists = True
+        return op
+
+    @writer.rewrites(ops.DropColumnOp)
+    def drop_column_if_not_exists(context, revision, op):
+        op.if_exists = True
+        return op
+
+
+
 Don't emit CREATE TABLE statements for Views
 ============================================
 
diff --git a/docs/build/unreleased/1626.rst b/docs/build/unreleased/1626.rst
new file mode 100644 (file)
index 0000000..f64ac40
--- /dev/null
@@ -0,0 +1,10 @@
+.. change::
+    :tags: usecase, autogenerate, postgresql
+
+    Added :paramref:`.Operations.add_column.if_not_exists` and
+    :paramref:`.Operations.drop_column.if_exists` to render ``IF [NOT] EXISTS``
+    for ``ADD COLUMN`` and ``DROP COLUMN`` operations, a feature available on
+    some database backends such as PostgreSQL, MariaDB, as well as third party
+    backends.  The parameters also support autogenerate rendering allowing them
+    to be turned on via a custom :class:`.Rewriter`. Pull request courtesy of
+    Louis-Amaury Chaib (@lachaib).
\ No newline at end of file
index 913e1017508486439ff42556820a218b642aa415..be8cc19acc036ec10dc5af07b7422844b7597f1b 100644 (file)
@@ -1168,6 +1168,19 @@ class AutogenRenderTest(TestBase):
             "server_default='5', nullable=True, somedialect_foobar='option'))",
         )
 
+    def test_render_add_column_if_not_exists(self):
+        op_obj = ops.AddColumnOp(
+            "foo",
+            Column("x", Integer, server_default="5", nullable=True),
+            if_not_exists=True,
+        )
+        eq_ignore_whitespace(
+            autogenerate.render_op_text(self.autogen_context, op_obj),
+            "op.add_column('foo', sa.Column('x', sa.Integer(), "
+            "server_default='5', nullable=True), "
+            "if_not_exists=True)",
+        )
+
     def test_render_add_column_system(self):
         # this would never actually happen since "system" columns
         # can't be added in any case.   However it will render as
@@ -1207,6 +1220,16 @@ class AutogenRenderTest(TestBase):
             "op.drop_column('bar', 'x', schema='foo')",
         )
 
+    def test_render_drop_column_if_exists(self):
+        op_obj = ops.DropColumnOp.from_column_and_tablename(
+            None, "foo", Column("x", Integer, server_default="5")
+        )
+        op_obj.if_exists = True
+        eq_ignore_whitespace(
+            autogenerate.render_op_text(self.autogen_context, op_obj),
+            "op.drop_column('foo', 'x', if_exists=True)",
+        )
+
     def test_render_quoted_server_default(self):
         eq_(
             autogenerate.render._render_server_default(
index b802b63b78a4071a4cdb5d8fe215859f3aeb75ae..b1a2a623f8563083714ab60766be9b207e9a7a7a 100644 (file)
@@ -919,7 +919,10 @@ class BatchAPITest(TestBase):
             batch.impl.operations.impl.mock_calls,
             [
                 mock.call.drop_column(
-                    "tname", self.mock_schema.Column(), schema=None
+                    "tname",
+                    self.mock_schema.Column(),
+                    schema=None,
+                    if_exists=None,
                 )
             ],
         )
@@ -931,7 +934,9 @@ class BatchAPITest(TestBase):
             batch.add_column(column)
 
         assert (
-            mock.call.add_column("tname", column, schema=None)
+            mock.call.add_column(
+                "tname", column, schema=None, if_not_exists=None
+            )
             in batch.impl.operations.impl.mock_calls
         )
 
index bb0a5a31bcf9e09b0babf08f4fcdad051d7e1a26..a23c9d736d707a3c4b81044c23ecc034fc0415ef 100644 (file)
@@ -64,6 +64,16 @@ class MySQLOpTest(TestBase):
             "ALTER TABLE t ADD COLUMN q INTEGER COMMENT 'This is a comment'"
         )
 
+    def test_add_column_if_not_exists(self):
+        context = op_fixture("mysql")
+        op.add_column("t", Column("c", Integer), if_not_exists=True)
+        context.assert_("ALTER TABLE t ADD COLUMN IF NOT EXISTS c INTEGER")
+
+    def test_drop_column_if_exists(self):
+        context = op_fixture("mysql")
+        op.drop_column("t", "c", if_exists=True)
+        context.assert_("ALTER TABLE t DROP COLUMN IF EXISTS c")
+
     def test_rename_column(self):
         context = op_fixture("mysql")
         op.alter_column(
index 510ec374de18d259b8f5c4d24c36423f4a188db2..d85cd59dd3bafe4018e60d3bd44224f4dbf18dcf 100644 (file)
@@ -109,6 +109,20 @@ class OpTest(TestBase):
         op.add_column("t1", Column("c1", Integer, nullable=False))
         context.assert_("ALTER TABLE t1 ADD COLUMN c1 INTEGER NOT NULL")
 
+    def test_add_column_exists_directives(self):
+        context = op_fixture()
+        op.add_column(
+            "t1", Column("c1", Integer, nullable=False), if_not_exists=True
+        )
+        context.assert_(
+            "ALTER TABLE t1 ADD COLUMN IF NOT EXISTS c1 INTEGER NOT NULL"
+        )
+
+    def test_drop_column_exists_directives(self):
+        context = op_fixture()
+        op.drop_column("t1", "c1", if_exists=True)
+        context.assert_("ALTER TABLE t1 DROP COLUMN IF EXISTS c1")
+
     def test_add_column_already_attached(self):
         context = op_fixture()
         c1 = Column("c1", Integer, nullable=False)
index 18576ec458668440a92abda1cf957e21f6649386..44f422dd52e8fb61cdacdf82bae5e7304b37deb0 100644 (file)
@@ -128,7 +128,7 @@ class PostgresqlOpTest(TestBase):
         op.create_index("i", "t", ["c1", "c2"], unique=False)
         context.assert_("CREATE INDEX i ON t (c1, c2)")
 
-    def test_create_index_postgresql_if_not_exists(self):
+    def test_create_index_if_not_exists(self):
         context = op_fixture("postgresql")
         op.create_index("i", "t", ["c1", "c2"], if_not_exists=True)
         context.assert_("CREATE INDEX IF NOT EXISTS i ON t (c1, c2)")
@@ -146,7 +146,7 @@ class PostgresqlOpTest(TestBase):
             op.drop_index("geocoded", postgresql_concurrently=True)
         context.assert_("DROP INDEX CONCURRENTLY geocoded")
 
-    def test_drop_index_postgresql_if_exists(self):
+    def test_drop_index_if_exists(self):
         context = op_fixture("postgresql")
         op.drop_index("geocoded", if_exists=True)
         context.assert_("DROP INDEX IF EXISTS geocoded")
@@ -158,6 +158,16 @@ class PostgresqlOpTest(TestBase):
             "ALTER TABLE t ALTER COLUMN c TYPE INTEGER USING c::integer"
         )
 
+    def test_add_column_if_not_exists(self):
+        context = op_fixture("postgresql")
+        op.add_column("t", Column("c", Integer), if_not_exists=True)
+        context.assert_("ALTER TABLE t ADD COLUMN IF NOT EXISTS c INTEGER")
+
+    def test_drop_column_if_exists(self):
+        context = op_fixture("postgresql")
+        op.drop_column("t", "c", if_exists=True)
+        context.assert_("ALTER TABLE t DROP COLUMN IF EXISTS c")
+
     def test_col_w_pk_is_serial(self):
         context = op_fixture("postgresql")
         op.add_column("some_table", Column("q", Integer, primary_key=True))