Skip to content

Commit cfc9de8

Browse files
lachaibzzzeek
authored andcommitted
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: #1627 Pull-request-sha: c503b04 Change-Id: I4d55f65f072b9f03698b2f45f066872b5c3e8c58
1 parent 76791bb commit cfc9de8

File tree

17 files changed

+263
-30
lines changed

17 files changed

+263
-30
lines changed

alembic/autogenerate/render.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -457,40 +457,56 @@ def _drop_constraint(
457457

458458
@renderers.dispatch_for(ops.AddColumnOp)
459459
def _add_column(autogen_context: AutogenContext, op: ops.AddColumnOp) -> str:
460-
schema, tname, column = op.schema, op.table_name, op.column
460+
schema, tname, column, if_not_exists = (
461+
op.schema,
462+
op.table_name,
463+
op.column,
464+
op.if_not_exists,
465+
)
461466
if autogen_context._has_batch:
462467
template = "%(prefix)sadd_column(%(column)s)"
463468
else:
464469
template = "%(prefix)sadd_column(%(tname)r, %(column)s"
465470
if schema:
466471
template += ", schema=%(schema)r"
472+
if if_not_exists is not None:
473+
template += ", if_not_exists=%(if_not_exists)r"
467474
template += ")"
468475
text = template % {
469476
"prefix": _alembic_autogenerate_prefix(autogen_context),
470477
"tname": tname,
471478
"column": _render_column(column, autogen_context),
472479
"schema": schema,
480+
"if_not_exists": if_not_exists,
473481
}
474482
return text
475483

476484

477485
@renderers.dispatch_for(ops.DropColumnOp)
478486
def _drop_column(autogen_context: AutogenContext, op: ops.DropColumnOp) -> str:
479-
schema, tname, column_name = op.schema, op.table_name, op.column_name
487+
schema, tname, column_name, if_exists = (
488+
op.schema,
489+
op.table_name,
490+
op.column_name,
491+
op.if_exists,
492+
)
480493

481494
if autogen_context._has_batch:
482495
template = "%(prefix)sdrop_column(%(cname)r)"
483496
else:
484497
template = "%(prefix)sdrop_column(%(tname)r, %(cname)r"
485498
if schema:
486499
template += ", schema=%(schema)r"
500+
if if_exists is not None:
501+
template += ", if_exists=%(if_exists)r"
487502
template += ")"
488503

489504
text = template % {
490505
"prefix": _alembic_autogenerate_prefix(autogen_context),
491506
"tname": _ident(tname),
492507
"cname": _ident(column_name),
493508
"schema": _ident(schema),
509+
"if_exists": if_exists,
494510
}
495511
return text
496512

alembic/ddl/base.py

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -154,17 +154,24 @@ def __init__(
154154
name: str,
155155
column: Column[Any],
156156
schema: Optional[Union[quoted_name, str]] = None,
157+
if_not_exists: Optional[bool] = None,
157158
) -> None:
158159
super().__init__(name, schema=schema)
159160
self.column = column
161+
self.if_not_exists = if_not_exists
160162

161163

162164
class DropColumn(AlterTable):
163165
def __init__(
164-
self, name: str, column: Column[Any], schema: Optional[str] = None
166+
self,
167+
name: str,
168+
column: Column[Any],
169+
schema: Optional[str] = None,
170+
if_exists: Optional[bool] = None,
165171
) -> None:
166172
super().__init__(name, schema=schema)
167173
self.column = column
174+
self.if_exists = if_exists
168175

169176

170177
class ColumnComment(AlterColumn):
@@ -189,15 +196,19 @@ def visit_rename_table(
189196
def visit_add_column(element: AddColumn, compiler: DDLCompiler, **kw) -> str:
190197
return "%s %s" % (
191198
alter_table(compiler, element.table_name, element.schema),
192-
add_column(compiler, element.column, **kw),
199+
add_column(
200+
compiler, element.column, if_not_exists=element.if_not_exists, **kw
201+
),
193202
)
194203

195204

196205
@compiles(DropColumn)
197206
def visit_drop_column(element: DropColumn, compiler: DDLCompiler, **kw) -> str:
198207
return "%s %s" % (
199208
alter_table(compiler, element.table_name, element.schema),
200-
drop_column(compiler, element.column.name, **kw),
209+
drop_column(
210+
compiler, element.column.name, if_exists=element.if_exists, **kw
211+
),
201212
)
202213

203214

@@ -320,16 +331,29 @@ def alter_table(
320331
return "ALTER TABLE %s" % format_table_name(compiler, name, schema)
321332

322333

323-
def drop_column(compiler: DDLCompiler, name: str, **kw) -> str:
324-
return "DROP COLUMN %s" % format_column_name(compiler, name)
334+
def drop_column(
335+
compiler: DDLCompiler, name: str, if_exists: Optional[bool] = None, **kw
336+
) -> str:
337+
return "DROP COLUMN %s%s" % (
338+
"IF EXISTS " if if_exists else "",
339+
format_column_name(compiler, name),
340+
)
325341

326342

327343
def alter_column(compiler: DDLCompiler, name: str) -> str:
328344
return "ALTER COLUMN %s" % format_column_name(compiler, name)
329345

330346

331-
def add_column(compiler: DDLCompiler, column: Column[Any], **kw) -> str:
332-
text = "ADD COLUMN %s" % compiler.get_column_specification(column, **kw)
347+
def add_column(
348+
compiler: DDLCompiler,
349+
column: Column[Any],
350+
if_not_exists: Optional[bool] = None,
351+
**kw,
352+
) -> str:
353+
text = "ADD COLUMN %s%s" % (
354+
"IF NOT EXISTS " if if_not_exists else "",
355+
compiler.get_column_specification(column, **kw),
356+
)
333357

334358
const = " ".join(
335359
compiler.process(constraint) for constraint in column.constraints

alembic/ddl/impl.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ def alter_column(
256256
self,
257257
table_name: str,
258258
column_name: str,
259+
*,
259260
nullable: Optional[bool] = None,
260261
server_default: Optional[
261262
Union[_ServerDefault, Literal[False]]
@@ -370,18 +371,33 @@ def add_column(
370371
self,
371372
table_name: str,
372373
column: Column[Any],
374+
*,
373375
schema: Optional[Union[str, quoted_name]] = None,
376+
if_not_exists: Optional[bool] = None,
374377
) -> None:
375-
self._exec(base.AddColumn(table_name, column, schema=schema))
378+
self._exec(
379+
base.AddColumn(
380+
table_name,
381+
column,
382+
schema=schema,
383+
if_not_exists=if_not_exists,
384+
)
385+
)
376386

377387
def drop_column(
378388
self,
379389
table_name: str,
380390
column: Column[Any],
391+
*,
381392
schema: Optional[str] = None,
393+
if_exists: Optional[bool] = None,
382394
**kw,
383395
) -> None:
384-
self._exec(base.DropColumn(table_name, column, schema=schema))
396+
self._exec(
397+
base.DropColumn(
398+
table_name, column, schema=schema, if_exists=if_exists
399+
)
400+
)
385401

386402
def add_constraint(self, const: Any) -> None:
387403
if const._create_rule is None or const._create_rule(self):

alembic/ddl/mssql.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,11 @@ def emit_commit(self) -> None:
8383
if self.as_sql and self.batch_separator:
8484
self.static_output(self.batch_separator)
8585

86-
def alter_column( # type:ignore[override]
86+
def alter_column(
8787
self,
8888
table_name: str,
8989
column_name: str,
90+
*,
9091
nullable: Optional[bool] = None,
9192
server_default: Optional[
9293
Union[_ServerDefault, Literal[False]]
@@ -202,6 +203,7 @@ def drop_column(
202203
self,
203204
table_name: str,
204205
column: Column[Any],
206+
*,
205207
schema: Optional[str] = None,
206208
**kw,
207209
) -> None:

alembic/ddl/mysql.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,11 @@ class MySQLImpl(DefaultImpl):
4747
)
4848
type_arg_extract = [r"character set ([\w\-_]+)", r"collate ([\w\-_]+)"]
4949

50-
def alter_column( # type:ignore[override]
50+
def alter_column(
5151
self,
5252
table_name: str,
5353
column_name: str,
54+
*,
5455
nullable: Optional[bool] = None,
5556
server_default: Optional[
5657
Union[_ServerDefault, Literal[False]]

alembic/ddl/postgresql.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
from ..util import sqla_compat
5353
from ..util.sqla_compat import compiles
5454

55+
5556
if TYPE_CHECKING:
5657
from typing import Literal
5758

@@ -148,10 +149,11 @@ def compare_server_default(
148149
select(literal_column(conn_col_default) == metadata_default)
149150
)
150151

151-
def alter_column( # type:ignore[override]
152+
def alter_column(
152153
self,
153154
table_name: str,
154155
column_name: str,
156+
*,
155157
nullable: Optional[bool] = None,
156158
server_default: Optional[
157159
Union[_ServerDefault, Literal[False]]

alembic/op.pyi

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,11 @@ _C = TypeVar("_C", bound=Callable[..., Any])
6060
### end imports ###
6161

6262
def add_column(
63-
table_name: str, column: Column[Any], *, schema: Optional[str] = None
63+
table_name: str,
64+
column: Column[Any],
65+
*,
66+
schema: Optional[str] = None,
67+
if_not_exists: Optional[bool] = None,
6468
) -> None:
6569
"""Issue an "add column" instruction using the current
6670
migration context.
@@ -137,6 +141,10 @@ def add_column(
137141
quoting of the schema outside of the default behavior, use
138142
the SQLAlchemy construct
139143
:class:`~sqlalchemy.sql.elements.quoted_name`.
144+
:param if_not_exists: If True, adds IF NOT EXISTS operator
145+
when creating the new column for compatible dialects
146+
147+
.. versionadded:: 1.16.0
140148
141149
"""
142150

@@ -927,6 +935,11 @@ def drop_column(
927935
quoting of the schema outside of the default behavior, use
928936
the SQLAlchemy construct
929937
:class:`~sqlalchemy.sql.elements.quoted_name`.
938+
:param if_exists: If True, adds IF EXISTS operator when
939+
dropping the new column for compatible dialects
940+
941+
.. versionadded:: 1.16.0
942+
930943
:param mssql_drop_check: Optional boolean. When ``True``, on
931944
Microsoft SQL Server only, first
932945
drop the CHECK constraint on the column using a
@@ -948,7 +961,6 @@ def drop_column(
948961
then exec's a separate DROP CONSTRAINT for that default. Only
949962
works if the column has exactly one FK constraint which refers to
950963
it, at the moment.
951-
952964
"""
953965

954966
def drop_constraint(
@@ -972,7 +984,7 @@ def drop_constraint(
972984
:param if_exists: If True, adds IF EXISTS operator when
973985
dropping the constraint
974986
975-
.. versionadded:: 1.15.3
987+
.. versionadded:: 1.16.0
976988
977989
"""
978990

alembic/operations/base.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -618,6 +618,7 @@ def add_column(
618618
column: Column[Any],
619619
*,
620620
schema: Optional[str] = None,
621+
if_not_exists: Optional[bool] = None,
621622
) -> None:
622623
"""Issue an "add column" instruction using the current
623624
migration context.
@@ -694,6 +695,10 @@ def add_column(
694695
quoting of the schema outside of the default behavior, use
695696
the SQLAlchemy construct
696697
:class:`~sqlalchemy.sql.elements.quoted_name`.
698+
:param if_not_exists: If True, adds IF NOT EXISTS operator
699+
when creating the new column for compatible dialects
700+
701+
.. versionadded:: 1.16.0
697702
698703
""" # noqa: E501
699704
...
@@ -1361,6 +1366,11 @@ def drop_column(
13611366
quoting of the schema outside of the default behavior, use
13621367
the SQLAlchemy construct
13631368
:class:`~sqlalchemy.sql.elements.quoted_name`.
1369+
:param if_exists: If True, adds IF EXISTS operator when
1370+
dropping the new column for compatible dialects
1371+
1372+
.. versionadded:: 1.16.0
1373+
13641374
:param mssql_drop_check: Optional boolean. When ``True``, on
13651375
Microsoft SQL Server only, first
13661376
drop the CHECK constraint on the column using a
@@ -1382,7 +1392,6 @@ def drop_column(
13821392
then exec's a separate DROP CONSTRAINT for that default. Only
13831393
works if the column has exactly one FK constraint which refers to
13841394
it, at the moment.
1385-
13861395
""" # noqa: E501
13871396
...
13881397

@@ -1408,7 +1417,7 @@ def drop_constraint(
14081417
:param if_exists: If True, adds IF EXISTS operator when
14091418
dropping the constraint
14101419
1411-
.. versionadded:: 1.15.3
1420+
.. versionadded:: 1.16.0
14121421
14131422
""" # noqa: E501
14141423
...
@@ -1651,6 +1660,7 @@ def add_column(
16511660
*,
16521661
insert_before: Optional[str] = None,
16531662
insert_after: Optional[str] = None,
1663+
if_not_exists: Optional[bool] = None,
16541664
) -> None:
16551665
"""Issue an "add column" instruction using the current
16561666
batch migration context.

0 commit comments

Comments
 (0)