]> 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>
Wed, 19 Mar 2025 07:47:40 +0000 (08:47 +0100)
committerLouis-Amaury Chaib <louisamaury.chaib@partoo.fr>
Tue, 13 May 2025 07:38:54 +0000 (09:38 +0200)
Fixes #1626

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
alembic/script/revision.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_postgresql.py

index 50c51fa9304a13b038c24397c112efbec3167058..0a0344bb0d9516526b1e2eb67ba865a28c1d79f0 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 bd55c56d570493202a6bd6c9151a65c07325676a..f2411c903eaf311a05ee4074b764686cd6e42da8 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):
index c116fcfa6120f38e25fe2aeba89c1c63d5834840..62aae2e5dd20550c030bf09a83bcdeb7226eb7a1 100644 (file)
@@ -370,17 +370,30 @@ class DefaultImpl(metaclass=ImplMeta):
         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..47c997e176d6e46dfa13606ed80bc4355e3a8044 100644 (file)
@@ -203,6 +203,7 @@ class MSSQLImpl(DefaultImpl):
         table_name: str,
         column: Column[Any],
         schema: Optional[str] = None,
+        if_exists: Optional[bool] = None,
         **kw,
     ) -> None:
         drop_default = kw.pop("mssql_drop_default", False)
@@ -222,7 +223,9 @@ class MSSQLImpl(DefaultImpl):
         drop_fks = kw.pop("mssql_drop_foreign_key", False)
         if drop_fks:
             self._exec(_ExecDropFKConstraint(table_name, column, schema))
-        super().drop_column(table_name, column, schema=schema, **kw)
+        super().drop_column(
+            table_name, column, schema=schema, if_exists=if_exists, **kw
+        )
 
     def compare_server_default(
         self,
index c7b3905cba35d99777a42432ae2ae555bb54e95d..47e9d80871e3781cb6391e0825d4d92517bfbe23 100644 (file)
@@ -12,12 +12,14 @@ from typing import Union
 from sqlalchemy import schema
 from sqlalchemy import types as sqltypes
 
+from .base import AddColumn
 from .base import alter_table
 from .base import AlterColumn
 from .base import ColumnDefault
 from .base import ColumnName
 from .base import ColumnNullable
 from .base import ColumnType
+from .base import DropColumn
 from .base import format_column_name
 from .base import format_server_default
 from .impl import DefaultImpl
@@ -391,6 +393,29 @@ def _mysql_alter_default(
     )
 
 
+@compiles(AddColumn, "mysql", "mariadb")
+def _mysql_add_column(
+    element: AddColumn, compiler: MySQLDDLCompiler, **kw
+) -> str:
+
+    return "%s ADD COLUMN %s%s" % (
+        alter_table(compiler, element.table_name, element.schema),
+        "IF NOT EXISTS " if element.if_not_exists else "",
+        compiler.get_column_specification(element.column, **kw),
+    )
+
+
+@compiles(DropColumn, "mysql", "mariadb")
+def _mysql_drop_column(
+    element: DropColumn, compiler: MySQLDDLCompiler, **kw
+) -> str:
+    return "%s DROP COLUMN %s%s" % (
+        alter_table(compiler, element.table_name, element.schema),
+        "IF EXISTS " if element.if_exists else "",
+        format_column_name(compiler, element.column.name),
+    )
+
+
 @compiles(MySQLModifyColumn, "mysql", "mariadb")
 def _mysql_modify_column(
     element: MySQLModifyColumn, compiler: MySQLDDLCompiler, **kw
index 7cd8d35b0896194dad51e4affa9fd9e094035082..244281a2d61b9f08ce9e4793198475ca7c34a5b4 100644 (file)
@@ -27,15 +27,18 @@ from sqlalchemy.dialects.postgresql import BIGINT
 from sqlalchemy.dialects.postgresql import ExcludeConstraint
 from sqlalchemy.dialects.postgresql import INTEGER
 from sqlalchemy.schema import CreateIndex
+from sqlalchemy.sql.compiler import DDLCompiler
 from sqlalchemy.sql.elements import ColumnClause
 from sqlalchemy.sql.elements import TextClause
 from sqlalchemy.sql.functions import FunctionElement
 from sqlalchemy.types import NULLTYPE
 
+from .base import AddColumn
 from .base import alter_column
 from .base import alter_table
 from .base import AlterColumn
 from .base import ColumnComment
+from .base import DropColumn
 from .base import format_column_name
 from .base import format_table_name
 from .base import format_type
@@ -52,6 +55,7 @@ from ..operations.base import Operations
 from ..util import sqla_compat
 from ..util.sqla_compat import compiles
 
+
 if TYPE_CHECKING:
     from typing import Literal
 
@@ -512,6 +516,34 @@ class PostgresqlColumnType(AlterColumn):
         self.using = using
 
 
+@compiles(AddColumn, "postgresql")
+def visit_add_column(element: AddColumn, compiler: PGDDLCompiler, **kw) -> str:
+    return "%s %s" % (
+        alter_table(compiler, element.table_name, element.schema),
+        add_column(
+            compiler,
+            element.column,
+            if_not_exists=element.if_not_exists,
+            **kw,
+        ),
+    )
+
+
+@compiles(DropColumn, "postgresql")
+def visit_drop_column(
+    element: DropColumn, compiler: PGDDLCompiler, **kw
+) -> str:
+    return "%s %s" % (
+        alter_table(compiler, element.table_name, element.schema),
+        drop_column(
+            compiler,
+            element.column.name,
+            if_exists=element.if_exists,
+            **kw,
+        ),
+    )
+
+
 @compiles(RenameTable, "postgresql")
 def visit_rename_table(
     element: RenameTable, compiler: PGDDLCompiler, **kw
@@ -848,3 +880,39 @@ def _render_potential_column(
             autogen_context,
             wrap_in_element=isinstance(value, (TextClause, FunctionElement)),
         )
+
+
+def add_column(
+    compiler: DDLCompiler,
+    column: Column[Any],
+    *,
+    if_not_exists: Optional[bool] = None,
+    **kw,
+) -> str:
+    text = "ADD COLUMN "
+    if if_not_exists:
+        text += "IF NOT EXISTS "
+
+    text += compiler.get_column_specification(column, **kw)
+
+    const = " ".join(
+        compiler.process(constraint) for constraint in column.constraints
+    )
+    if const:
+        text += " " + const
+
+    return text
+
+
+def drop_column(
+    compiler: DDLCompiler,
+    name: str,
+    *,
+    if_exists: Optional[bool] = None,
+    **kw,
+) -> str:
+    text = "DROP COLUMN "
+    if if_exists:
+        text += "IF EXISTS "
+    text += format_column_name(compiler, name)
+    return text
index 13a5852f6b1310c1e4a6ab6f9eb421456bb545f6..f440836558e99889b0d5c220e5525304a36417a7 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.15.3
 
     """
 
@@ -925,6 +933,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.15.3
+
     :param mssql_drop_check: Optional boolean.  When ``True``, on
      Microsoft SQL Server only, first
      drop the CHECK constraint on the column using a
@@ -946,7 +959,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(
index 139931147eba583ab5626195a2011763b601f466..efcb590a6ee5e4720a2835f23b6155c25670850c 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.15.3
 
             """  # 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.15.3
+
             :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
             ...
 
@@ -1646,6 +1655,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 eebc23235b4c09ffa4dc8fda3dd8697c5a45a573..7a3d7ce8d23a913e387dffde102ca560be76d2e2 100644 (file)
@@ -2034,16 +2034,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,
@@ -2074,6 +2078,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.
@@ -2150,10 +2155,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.15.3
 
         """
 
-        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
@@ -2164,6 +2178,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.
@@ -2184,6 +2199,7 @@ class AddColumnOp(AlterTableOp):
             operations.impl.table_name,
             column,
             schema=operations.impl.schema,
+            if_not_exists=if_not_exists,
             **kw,
         )
         return operations.invoke(op)
@@ -2200,12 +2216,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(
@@ -2225,9 +2243,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(
@@ -2274,6 +2294,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.15.3
+
         :param mssql_drop_check: Optional boolean.  When ``True``, on
          Microsoft SQL Server only, first
          drop the CHECK constraint on the column using a
@@ -2295,7 +2320,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 528c05428c910aba18639a9a60ac0f6dcd7377c2..cc7b0e085c74a11aca10a0905613c048a032f59d 100644 (file)
@@ -92,7 +92,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,
     )
 
 
@@ -167,7 +171,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 c3108e985a0a013922793489dbd9b03df95b3e07..587e90497ce0e4313452441b3611957453eb6349 100644 (file)
@@ -1708,7 +1708,7 @@ def tuple_rev_as_scalar(rev: None) -> None: ...
 
 @overload
 def tuple_rev_as_scalar(
-    rev: Union[Tuple[_T, ...], List[_T]]
+    rev: Union[Tuple[_T, ...], List[_T]],
 ) -> Union[_T, Tuple[_T, ...], List[_T]]: ...
 
 
index ce5fb5438e8ced95aef47fc76ab5794359bd223f..2652770a80fbc3d318cfa6237aa06540b0a8b636 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..afe50ab
--- /dev/null
@@ -0,0 +1,8 @@
+.. change::
+    :tags: usecase, autogenerate, postgresql
+
+    Render `if_exists`` and `if_not_exists`` parameters in
+    :class:`.AddColumnOp` and :class:`.DropColumnOp`, in an autogenerate context.
+    They can be enabled using a custom :class:`.Rewriter` in the ``env.py`` file, where they will now be
+    part of the rendered Python code in revision files.
+    Pull request courtesy of Louis-Amaury Chaib (@lachaib).
\ No newline at end of file
index 600e57834fdf07d89479c774228f21aad6af2544..33d5941e3a7093ef21d3e2de6bf1f67be5bfe6e4 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 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))