Skip to content

Add ALTER TYPE ... ADD VALUE fast path for append-only enum changes #133

Description

@Afstkla

The detector at alembic_postgresql_enum/detection_of_changes/enum_alteration.py always emits SyncEnumValuesOp, regardless of how the enum changed. For pure appends (the common case — adding a new variant), SyncEnumValuesOp is dramatically more expensive than necessary: it renames the type, creates a new one, then casts every column referencing the enum to the new type, which rewrites every row under ACCESS EXCLUSIVE.

We just hit this in production. Migration added one new variant to an enum. The referencing table has ~2.9M rows. The full rewrite exceeded our deploy's startup-probe deadline, the migrator was killed mid-rewrite, and instances served traffic against the old schema → 500s on every request that used the new variant.

PostgreSQL has ALTER TYPE name ADD VALUE IF NOT EXISTS 'X', which is O(1), no table rewrite, no exclusive lock on the referencing table.

Notably, enum_alteration.py:2 already documents this as the intended behavior:

Alembic extension to generate ALTER TYPE ... ADD VALUE statements to update SQLAlchemy enums.

…but the code emits SyncEnumValuesOp instead.

Proposal

When old_values is an ordered prefix of new_values (or, with ignore_enum_values_order=True, when set(old) ⊂ set(new)) and enum_values_to_rename is empty, emit a new AddEnumValuesOp that renders to one op.execute("ALTER TYPE ... ADD VALUE IF NOT EXISTS '...'") per added value. Everything else still routes to SyncEnumValuesOp as today.

Design notes / things to decide

  1. API surface — opt-in vs default-on? Default-off (Config.use_alter_type_for_appends=False) is a zero-behavior-change minor release. Default-on is safer for users but a behavior change → would want a major bump. I'd lean default-on given the risk profile, but happy with either. Prefer your call.
  2. PG version. ALTER TYPE ... ADD VALUE inside a transaction requires PG ≥ 12 (PG 11 has been EOL since Nov 2023). Easiest: fast path assumes PG ≥ 12; fall back to SyncEnumValuesOp otherwise. Docstring warns that the newly-added value can't be referenced in the same transaction.
  3. Downgrade. PostgreSQL has no DROP VALUE, so AddEnumValuesOp.reverse() returns a SyncEnumValuesOp with values swapped. Asymmetric but unavoidable.

Happy to send a PR if the proposal sounds good — want to confirm API preference before I write it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions