Skip to content

Commit 7456d83

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

File tree

13 files changed

+228
-14
lines changed

13 files changed

+228
-14
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: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -370,17 +370,27 @@ 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, column, schema=schema, if_not_exists=if_not_exists
378+
)
379+
)
375380

376381
def drop_column(
377382
self,
378383
table_name: str,
379384
column: Column[Any],
380385
schema: Optional[str] = None,
386+
if_exists: Optional[bool] = None,
381387
**kw,
382388
) -> None:
383-
self._exec(base.DropColumn(table_name, column, schema=schema))
389+
self._exec(
390+
base.DropColumn(
391+
table_name, column, schema=schema, if_exists=if_exists
392+
)
393+
)
384394

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

alembic/ddl/postgresql.py

Lines changed: 58 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,28 @@ 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, element.column, if_not_exists=element.if_not_exists, **kw
525+
),
526+
)
527+
528+
529+
@compiles(DropColumn, "postgresql")
530+
def visit_drop_column(
531+
element: DropColumn, compiler: PGDDLCompiler, **kw
532+
) -> str:
533+
return "%s %s" % (
534+
alter_table(compiler, element.table_name, element.schema),
535+
drop_column(
536+
compiler, element.column.name, if_exists=element.if_exists, **kw
537+
),
538+
)
539+
540+
515541
@compiles(RenameTable, "postgresql")
516542
def visit_rename_table(
517543
element: RenameTable, compiler: PGDDLCompiler, **kw
@@ -848,3 +874,35 @@ def _render_potential_column(
848874
autogen_context,
849875
wrap_in_element=isinstance(value, (TextClause, FunctionElement)),
850876
)
877+
878+
879+
def add_column(
880+
compiler: DDLCompiler,
881+
column: Column[Any],
882+
*,
883+
if_not_exists: Optional[bool] = None,
884+
**kw,
885+
) -> str:
886+
text = "ADD COLUMN "
887+
if if_not_exists:
888+
text += "IF NOT EXISTS "
889+
890+
text += compiler.get_column_specification(column, **kw)
891+
892+
const = " ".join(
893+
compiler.process(constraint) for constraint in column.constraints
894+
)
895+
if const:
896+
text += " " + const
897+
898+
return text
899+
900+
901+
def drop_column(
902+
compiler: DDLCompiler, name: str, *, if_exists: Optional[bool] = None, **kw
903+
) -> str:
904+
text = "DROP COLUMN "
905+
if if_exists:
906+
text += "IF EXISTS "
907+
text += format_column_name(compiler, name)
908+
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,8 @@ 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 when
145+
creating the new column for databases that support it.
140146
141147
"""
142148

@@ -910,6 +916,7 @@ def drop_column(
910916
column_name: str,
911917
*,
912918
schema: Optional[str] = None,
919+
if_exists: Optional[bool] = None,
913920
**kw: Any,
914921
) -> None:
915922
"""Issue a "drop column" instruction using the current
@@ -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 column if the database supports it.
949958
950959
"""
951960

alembic/operations/base.py

Lines changed: 14 additions & 1 deletion
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,8 @@ 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 when
699+
creating the new column for databases that support it.
697700
698701
""" # noqa: E501
699702
...
@@ -1346,6 +1349,7 @@ def drop_column(
13461349
column_name: str,
13471350
*,
13481351
schema: Optional[str] = None,
1352+
if_exists: Optional[bool] = None,
13491353
**kw: Any,
13501354
) -> None:
13511355
"""Issue a "drop column" instruction using the current
@@ -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 column if the database supports it.
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.
@@ -1839,7 +1846,13 @@ def create_unique_constraint(
18391846
""" # noqa: E501
18401847
...
18411848

1842-
def drop_column(self, column_name: str, **kw: Any) -> None:
1849+
def drop_column(
1850+
self,
1851+
column_name: str,
1852+
*,
1853+
if_exists: Optional[bool] = None,
1854+
**kw: Any,
1855+
) -> None:
18431856
"""Issue a "drop column" instruction using the current
18441857
batch migration context.
18451858

alembic/operations/ops.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2034,16 +2034,20 @@ def __init__(
20342034
column: Column[Any],
20352035
*,
20362036
schema: Optional[str] = None,
2037+
if_not_exists: Optional[bool] = None,
20372038
**kw: Any,
20382039
) -> None:
20392040
super().__init__(table_name, schema=schema)
20402041
self.column = column
2042+
self.if_not_exists = if_not_exists
20412043
self.kw = kw
20422044

20432045
def reverse(self) -> DropColumnOp:
2044-
return DropColumnOp.from_column_and_tablename(
2046+
op = DropColumnOp.from_column_and_tablename(
20452047
self.schema, self.table_name, self.column
20462048
)
2049+
op.if_exists = self.if_not_exists
2050+
return op
20472051

20482052
def to_diff_tuple(
20492053
self,
@@ -2074,6 +2078,7 @@ def add_column(
20742078
column: Column[Any],
20752079
*,
20762080
schema: Optional[str] = None,
2081+
if_not_exists: Optional[bool] = None,
20772082
) -> None:
20782083
"""Issue an "add column" instruction using the current
20792084
migration context.
@@ -2153,7 +2158,9 @@ def add_column(
21532158
21542159
"""
21552160

2156-
op = cls(table_name, column, schema=schema)
2161+
op = cls(
2162+
table_name, column, schema=schema, if_not_exists=if_not_exists
2163+
)
21572164
return operations.invoke(op)
21582165

21592166
@classmethod
@@ -2200,12 +2207,14 @@ def __init__(
22002207
column_name: str,
22012208
*,
22022209
schema: Optional[str] = None,
2210+
if_exists: Optional[bool] = None,
22032211
_reverse: Optional[AddColumnOp] = None,
22042212
**kw: Any,
22052213
) -> None:
22062214
super().__init__(table_name, schema=schema)
22072215
self.column_name = column_name
22082216
self.kw = kw
2217+
self.if_exists = if_exists
22092218
self._reverse = _reverse
22102219

22112220
def to_diff_tuple(
@@ -2225,9 +2234,11 @@ def reverse(self) -> AddColumnOp:
22252234
"original column is not present"
22262235
)
22272236

2228-
return AddColumnOp.from_column_and_tablename(
2237+
op = AddColumnOp.from_column_and_tablename(
22292238
self.schema, self.table_name, self._reverse.column
22302239
)
2240+
op.if_not_exists = self.if_exists
2241+
return op
22312242

22322243
@classmethod
22332244
def from_column_and_tablename(

alembic/operations/toimpl.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,11 @@ def drop_column(
9292
) -> None:
9393
column = operation.to_column(operations.migration_context)
9494
operations.impl.drop_column(
95-
operation.table_name, column, schema=operation.schema, **operation.kw
95+
operation.table_name,
96+
column,
97+
schema=operation.schema,
98+
if_exists=operation.if_exists,
99+
**operation.kw,
96100
)
97101

98102

@@ -167,7 +171,13 @@ def add_column(operations: "Operations", operation: "ops.AddColumnOp") -> None:
167171
column = _copy(column)
168172

169173
t = operations.schema_obj.table(table_name, column, schema=schema)
170-
operations.impl.add_column(table_name, column, schema=schema, **kw)
174+
operations.impl.add_column(
175+
table_name,
176+
column,
177+
schema=schema,
178+
if_not_exists=operation.if_not_exists,
179+
**kw,
180+
)
171181

172182
for constraint in t.constraints:
173183
if not isinstance(constraint, sa_schema.PrimaryKeyConstraint):

0 commit comments

Comments
 (0)