Skip to content

Commit 7702b01

Browse files
committed
Add support of IF [NOT] EXISTS for ADD/DROP COLUMN in Postgresql
Fixes sqlalchemy#1626
1 parent f4b269a commit 7702b01

File tree

16 files changed

+293
-16
lines changed

16 files changed

+293
-16
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: 8 additions & 1 deletion
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):

alembic/ddl/impl.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -370,8 +370,16 @@ def add_column(
370370
table_name: str,
371371
column: Column[Any],
372372
schema: Optional[Union[str, quoted_name]] = None,
373+
if_not_exists: Optional[bool] = None,
373374
) -> None:
374-
self._exec(base.AddColumn(table_name, column, schema=schema))
375+
self._exec(
376+
base.AddColumn(
377+
table_name,
378+
column,
379+
schema=schema,
380+
if_not_exists=if_not_exists,
381+
)
382+
)
375383

376384
def drop_column(
377385
self,
@@ -380,7 +388,14 @@ def drop_column(
380388
schema: Optional[str] = None,
381389
**kw,
382390
) -> None:
383-
self._exec(base.DropColumn(table_name, column, schema=schema))
391+
self._exec(
392+
base.DropColumn(
393+
table_name,
394+
column,
395+
schema=schema,
396+
**kw,
397+
)
398+
)
384399

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

alembic/ddl/mysql.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@
1212
from sqlalchemy import schema
1313
from sqlalchemy import types as sqltypes
1414

15+
from .base import AddColumn
1516
from .base import alter_table
1617
from .base import AlterColumn
1718
from .base import ColumnDefault
1819
from .base import ColumnName
1920
from .base import ColumnNullable
2021
from .base import ColumnType
22+
from .base import DropColumn
2123
from .base import format_column_name
2224
from .base import format_server_default
2325
from .impl import DefaultImpl
@@ -391,6 +393,29 @@ def _mysql_alter_default(
391393
)
392394

393395

396+
@compiles(AddColumn, "mysql", "mariadb")
397+
def _mysql_add_column(
398+
element: AddColumn, compiler: MySQLDDLCompiler, **kw
399+
) -> str:
400+
401+
return "%s ADD COLUMN %s%s" % (
402+
alter_table(compiler, element.table_name, element.schema),
403+
"IF NOT EXISTS " if element.if_not_exists else "",
404+
compiler.get_column_specification(element.column, **kw),
405+
)
406+
407+
408+
@compiles(DropColumn, "mysql", "mariadb")
409+
def _mysql_drop_column(
410+
element: DropColumn, compiler: MySQLDDLCompiler, **kw
411+
) -> str:
412+
return "%s DROP COLUMN %s%s" % (
413+
alter_table(compiler, element.table_name, element.schema),
414+
"IF EXISTS " if element.if_exists else "",
415+
format_column_name(compiler, element.column.name),
416+
)
417+
418+
394419
@compiles(MySQLModifyColumn, "mysql", "mariadb")
395420
def _mysql_modify_column(
396421
element: MySQLModifyColumn, compiler: MySQLDDLCompiler, **kw

alembic/ddl/postgresql.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,18 @@
2727
from sqlalchemy.dialects.postgresql import ExcludeConstraint
2828
from sqlalchemy.dialects.postgresql import INTEGER
2929
from sqlalchemy.schema import CreateIndex
30+
from sqlalchemy.sql.compiler import DDLCompiler
3031
from sqlalchemy.sql.elements import ColumnClause
3132
from sqlalchemy.sql.elements import TextClause
3233
from sqlalchemy.sql.functions import FunctionElement
3334
from sqlalchemy.types import NULLTYPE
3435

36+
from .base import AddColumn
3537
from .base import alter_column
3638
from .base import alter_table
3739
from .base import AlterColumn
3840
from .base import ColumnComment
41+
from .base import DropColumn
3942
from .base import format_column_name
4043
from .base import format_table_name
4144
from .base import format_type
@@ -52,6 +55,7 @@
5255
from ..util import sqla_compat
5356
from ..util.sqla_compat import compiles
5457

58+
5559
if TYPE_CHECKING:
5660
from typing import Literal
5761

@@ -512,6 +516,34 @@ def __init__(
512516
self.using = using
513517

514518

519+
@compiles(AddColumn, "postgresql")
520+
def visit_add_column(element: AddColumn, compiler: PGDDLCompiler, **kw) -> str:
521+
return "%s %s" % (
522+
alter_table(compiler, element.table_name, element.schema),
523+
add_column(
524+
compiler,
525+
element.column,
526+
if_not_exists=element.if_not_exists,
527+
**kw,
528+
),
529+
)
530+
531+
532+
@compiles(DropColumn, "postgresql")
533+
def visit_drop_column(
534+
element: DropColumn, compiler: PGDDLCompiler, **kw
535+
) -> str:
536+
return "%s %s" % (
537+
alter_table(compiler, element.table_name, element.schema),
538+
drop_column(
539+
compiler,
540+
element.column.name,
541+
if_exists=element.if_exists,
542+
**kw,
543+
),
544+
)
545+
546+
515547
@compiles(RenameTable, "postgresql")
516548
def visit_rename_table(
517549
element: RenameTable, compiler: PGDDLCompiler, **kw
@@ -848,3 +880,39 @@ def _render_potential_column(
848880
autogen_context,
849881
wrap_in_element=isinstance(value, (TextClause, FunctionElement)),
850882
)
883+
884+
885+
def add_column(
886+
compiler: DDLCompiler,
887+
column: Column[Any],
888+
*,
889+
if_not_exists: Optional[bool] = None,
890+
**kw,
891+
) -> str:
892+
text = "ADD COLUMN "
893+
if if_not_exists:
894+
text += "IF NOT EXISTS "
895+
896+
text += compiler.get_column_specification(column, **kw)
897+
898+
const = " ".join(
899+
compiler.process(constraint) for constraint in column.constraints
900+
)
901+
if const:
902+
text += " " + const
903+
904+
return text
905+
906+
907+
def drop_column(
908+
compiler: DDLCompiler,
909+
name: str,
910+
*,
911+
if_exists: Optional[bool] = None,
912+
**kw,
913+
) -> str:
914+
text = "DROP COLUMN "
915+
if if_exists:
916+
text += "IF EXISTS "
917+
text += format_column_name(compiler, name)
918+
return text

alembic/op.pyi

Lines changed: 10 additions & 1 deletion
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,9 @@ 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+
140147
141148
"""
142149

@@ -946,6 +953,8 @@ def drop_column(
946953
then exec's a separate DROP CONSTRAINT for that default. Only
947954
works if the column has exactly one FK constraint which refers to
948955
it, at the moment.
956+
:param if_exists: If True, adds IF EXISTS operator when
957+
dropping the new column for compatible dialects
949958
950959
"""
951960

alembic/operations/base.py

Lines changed: 7 additions & 0 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,9 @@ 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+
697701
698702
""" # noqa: E501
699703
...
@@ -1382,6 +1386,8 @@ def drop_column(
13821386
then exec's a separate DROP CONSTRAINT for that default. Only
13831387
works if the column has exactly one FK constraint which refers to
13841388
it, at the moment.
1389+
:param if_exists: If True, adds IF EXISTS operator when
1390+
dropping the new column for compatible dialects
13851391
13861392
""" # noqa: E501
13871393
...
@@ -1646,6 +1652,7 @@ def add_column(
16461652
*,
16471653
insert_before: Optional[str] = None,
16481654
insert_after: Optional[str] = None,
1655+
if_not_exists: Optional[bool] = None,
16491656
) -> None:
16501657
"""Issue an "add column" instruction using the current
16511658
batch migration context.

0 commit comments

Comments
 (0)