From: Louis-Amaury Chaib Date: Mon, 19 May 2025 13:11:00 +0000 (-0400) Subject: Add support of IF [NOT] EXISTS for ADD/DROP COLUMN in Postgresql X-Git-Tag: rel_1_16_0~18 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=cfc9de8c6c45c68f90205c3e9d414d5e451b7d74;p=thirdparty%2Fsqlalchemy%2Falembic.git Add support of IF [NOT] EXISTS for ADD/DROP COLUMN in 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). Fixes: #1626 Closes: #1627 Pull-request: https://github.com/sqlalchemy/alembic/pull/1627 Pull-request-sha: c503b049f453a12e7fdb87464606cddd58ad306c Change-Id: I4d55f65f072b9f03698b2f45f066872b5c3e8c58 --- diff --git a/alembic/autogenerate/render.py b/alembic/autogenerate/render.py index 66c67673..50e7057d 100644 --- a/alembic/autogenerate/render.py +++ b/alembic/autogenerate/render.py @@ -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 diff --git a/alembic/ddl/base.py b/alembic/ddl/base.py index 7c36a887..ad2847eb 100644 --- a/alembic/ddl/base.py +++ b/alembic/ddl/base.py @@ -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 diff --git a/alembic/ddl/impl.py b/alembic/ddl/impl.py index a1070223..d352f12e 100644 --- a/alembic/ddl/impl.py +++ b/alembic/ddl/impl.py @@ -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): diff --git a/alembic/ddl/mssql.py b/alembic/ddl/mssql.py index baa43d5e..5376da5a 100644 --- a/alembic/ddl/mssql.py +++ b/alembic/ddl/mssql.py @@ -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: diff --git a/alembic/ddl/mysql.py b/alembic/ddl/mysql.py index d92e3cd7..3f8c0628 100644 --- a/alembic/ddl/mysql.py +++ b/alembic/ddl/mysql.py @@ -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]] diff --git a/alembic/ddl/postgresql.py b/alembic/ddl/postgresql.py index 4be04c56..90ecf70c 100644 --- a/alembic/ddl/postgresql.py +++ b/alembic/ddl/postgresql.py @@ -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]] diff --git a/alembic/op.pyi b/alembic/op.pyi index 548beb09..f39ff054 100644 --- a/alembic/op.pyi +++ b/alembic/op.pyi @@ -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 """ diff --git a/alembic/operations/base.py b/alembic/operations/base.py index 8ce0c45a..9f975031 100644 --- a/alembic/operations/base.py +++ b/alembic/operations/base.py @@ -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. diff --git a/alembic/operations/ops.py b/alembic/operations/ops.py index 633defe8..b2603272 100644 --- a/alembic/operations/ops.py +++ b/alembic/operations/ops.py @@ -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) diff --git a/alembic/operations/toimpl.py b/alembic/operations/toimpl.py index bc5e73dd..c18ec790 100644 --- a/alembic/operations/toimpl.py +++ b/alembic/operations/toimpl.py @@ -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): diff --git a/docs/build/cookbook.rst b/docs/build/cookbook.rst index e52753a8..154a4165 100644 --- a/docs/build/cookbook.rst +++ b/docs/build/cookbook.rst @@ -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 index 00000000..f64ac406 --- /dev/null +++ b/docs/build/unreleased/1626.rst @@ -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 diff --git a/tests/test_autogen_render.py b/tests/test_autogen_render.py index 913e1017..be8cc19a 100644 --- a/tests/test_autogen_render.py +++ b/tests/test_autogen_render.py @@ -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( diff --git a/tests/test_batch.py b/tests/test_batch.py index b802b63b..b1a2a623 100644 --- a/tests/test_batch.py +++ b/tests/test_batch.py @@ -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 ) diff --git a/tests/test_mysql.py b/tests/test_mysql.py index bb0a5a31..a23c9d73 100644 --- a/tests/test_mysql.py +++ b/tests/test_mysql.py @@ -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( diff --git a/tests/test_op.py b/tests/test_op.py index 510ec374..d85cd59d 100644 --- a/tests/test_op.py +++ b/tests/test_op.py @@ -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) diff --git a/tests/test_postgresql.py b/tests/test_postgresql.py index 18576ec4..44f422dd 100644 --- a/tests/test_postgresql.py +++ b/tests/test_postgresql.py @@ -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))