What this usually means
The failure depends on the PostgreSQL version and migration sequence. Current PostgreSQL releases allow ALTER TYPE ... ADD VALUE inside a transaction, but the newly added value cannot be referenced until that transaction commits. Older releases reject the ALTER TYPE statement inside a transaction block. Migration tools can therefore fail either while adding the value or later in the same migration when a default, constraint, or data update tries to use it.
The first ten minutes — establish facts before touching code.
- 1Run SHOW server_version first, then reproduce the migration in psql so you know whether the ALTER TYPE statement itself fails or only the first use of the new value fails.
- 2Inspect the migration boundary: if the script adds a value and then uses it in a default, constraint, or data update, split those operations across commits.
- 3Verify if the enum value already exists: SELECT enum_range(NULL::your_enum_type);
- 4Check whether the same transaction uses the new enum value after adding it; current PostgreSQL deliberately blocks that use until commit.
- 5Review migration order: if you add a value and then use it in a column default or constraint in the same migration, it will fail because the new value isn't committed.
The specific files, logs, configs, and dashboards that usually own this bug.
- searchMigration script files (e.g., alembic/versions/*.py, flyway/V*.sql)
- searchpsql logs or migration tool logs (e.g., alembic.log, flyway-*.log)
- searchPostgreSQL error log: typically /var/log/postgresql/postgresql-*.log or via pg_stat_activity
- searchDatabase schema dump: pg_dump --schema-only your_db
- searchEnum type definition: SELECT oid, typname FROM pg_type WHERE typcategory='E'; then SELECT enumlabel FROM pg_enum WHERE enumtypid=oid;
- searchCurrent transaction state: SELECT pg_current_xact_id_if_assigned(); (if inside transaction, returns an XID)
Practical causes, not theory. These are the things you will actually find.
- warningAn older PostgreSQL release rejects ADD VALUE inside the transaction opened by the migration tool
- warningThe migration adds an enum value and then tries to use it before the surrounding transaction commits
- warningAttempting to add an enum value that already exists (duplicate value error)
- warningUsing BEFORE or AFTER with a neighboring enum label that does not exist
- warningUsing the new enum value in the same transaction before it is committed
- warningRunning multiple migrations concurrently that both add values to the same enum type, causing deadlocks
Concrete fix directions. Pick the one that matches your root cause.
- buildOn current PostgreSQL, commit the ADD VALUE migration before a later migration uses the new enum value.
- buildOn older PostgreSQL, configure the migration tool to execute the ALTER TYPE statement outside its transaction wrapper.
- buildFor raw SQL on an older server, remove the surrounding BEGIN/COMMIT or use a separate autocommit connection.
- buildUse IF NOT EXISTS where the deployed PostgreSQL version supports it, or guard the change by checking pg_enum before rerunning it.
- buildAdd the enum value in a separate migration step that runs before any code that uses it.
- buildUse ALTER TYPE ... ADD VALUE ... BEFORE/AFTER to maintain ordering if needed.
A fix you cannot prove is a guess. Close the loop.
- verifiedAfter running the migration, connect to the database and run SELECT enum_range(NULL::your_enum_type); to see the new value.
- verifiedVerify that new rows can be inserted with the new enum value.
- verifiedCheck that no errors appear in application logs when using the new value.
- verifiedRun the migration a second time (idempotency check)—it should skip or gracefully handle if the value already exists.
- verifiedTest rollback before deployment; PostgreSQL has no direct DROP VALUE command, so plan data reconciliation and type recreation if a committed value must be removed.
Things that make this bug worse or harder to find.
- warningDo not add the enum value and then use it in the same transaction; current PostgreSQL requires the new value to be committed first.
- warningDo not apply advice for one PostgreSQL version without checking the production server version.
- warningDo not add duplicate values—Postgres will throw an error; always check existing values first.
- warningDo not ignore the ordering: if your application expects a specific order, use BEFORE/AFTER appropriately.
- warningDo not run multiple migrations that add values to the same enum type concurrently—they can deadlock.
Production outage: Enum migration fails during deploy causing rollback chaos
Timeline
- 09:15Deploy starts: migration to add new status 'ARCHIVED' to 'order_status' enum
- 09:17Alembic migration fails: 'ALTER TYPE ... ADD VALUE cannot run inside a transaction block'
- 09:18CircleCI marks the migration job failed; the enum value was not added
- 09:20On-call engineer manually adds the enum value via psql to unblock
- 09:25Second migration that uses 'ARCHIVED' in a column default runs and commits successfully
- 09:30A later application rollback does not remove the enum value that was added and committed manually
- 09:35The rolled-back application encounters rows containing the newer 'ARCHIVED' value it does not handle
- 09:45Manual cleanup: reconcile affected rows, recreate the type without the unwanted value, and rerun the migration with the correct transaction boundary
This historical incident ran on PostgreSQL 9.6. The CI/CD pipeline wrapped Alembic migrations in a transaction, so adding 'ARCHIVED' to the 'order_status' enum was rejected inside that transaction block. On current PostgreSQL releases the ALTER TYPE statement is allowed, but the new value still cannot be used until commit.
The on-call engineer manually ran and committed the ALTER TYPE statement through psql to unblock the deployment. That changed the database outside Alembic's migration history. When the application was later rolled back, the separately committed enum change naturally remained, leaving code and schema versions out of sync.
The application version restored by the rollback expected only 'PENDING', 'SHIPPED', and 'DELIVERED', while rows created during the deployment window could contain 'ARCHIVED'. The team reconciled the rows, rebuilt the enum type without the unwanted value, and then reran a version-aware migration. Lesson: keep manual database changes and migration history aligned, and commit an added enum value before using it.
Root cause
The PostgreSQL 9.6 server rejected ALTER TYPE ... ADD VALUE inside the transaction opened by the migration workflow. Manual intervention and a later rollback made the state harder to reconcile.
The fix
Run the enum addition outside the transaction on the older server, make the migration safe to rerun, and commit before any later migration uses the value.
The lesson
Check the production PostgreSQL version, understand the exact transaction rule for each DDL statement, and test both forward migration and rollback before deployment.
Older PostgreSQL releases rejected ALTER TYPE ... ADD VALUE inside an explicit transaction block. Current releases allow the statement, but the official documentation says the newly added value cannot be used until after the transaction commits.
That distinction changes the fix. First check SHOW server_version. For an older server, run the enum addition outside the migration transaction. For a current server, it is usually enough to split the addition and its first use into separate committed migrations.
The safest cross-version approach is to place ALTER TYPE ... ADD VALUE in its own migration and commit it before any default, constraint, or data update references the new value. If the production server is old enough to reject the statement inside a transaction, use the migration tool's documented non-transactional or autocommit mechanism for that step.
Additionally, make the migration idempotent by checking if the value already exists. In PostgreSQL 9.1+, you can use `DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_enum WHERE enumlabel = 'new_value' AND enumtypid = 'enum_type'::regtype) THEN ALTER TYPE enum_type ADD VALUE 'new_value'; END IF; END $$;` This prevents errors on re-run.
By default, new values are added at the end of the enum's sort order. If you need the new value to appear in a specific position (e.g., for sorting by TEXT representation), you can use `ALTER TYPE enum_type ADD VALUE 'new_value' BEFORE 'existing_value'` or `AFTER`. However, you cannot specify a relative position that is invalid (e.g., BEFORE a non-existent value).
If you need to reorder enum values, you must create a new enum type with the desired order, update all columns to use the new type, and drop the old type. This is a heavy operation and should be avoided if possible. In practice, most applications don't depend on enum ordering.
When multiple sessions attempt to add values to the same enum type concurrently, they may deadlock because each ADD VALUE acquires an exclusive lock on the enum type. PostgreSQL's lock manager will detect the deadlock and abort one of the transactions. To avoid this, ensure that enum additions are serialized (e.g., run as part of a single migration or use advisory locks).
A common pattern is to add all needed enum values in one migration step, rather than multiple incremental migrations. This reduces the chance of concurrent modifications.
Related debugging guides
Frequently asked questions
Can I use ALTER TYPE ... ADD VALUE inside a stored procedure or function?
Check the server version and transaction boundary. On current PostgreSQL, ADD VALUE can execute in a transaction, but the new value cannot be used until commit. On older releases, execute the ALTER TYPE step outside the transaction.
How do I remove an enum value if I accidentally added it?
PostgreSQL does not support dropping a single enum value. You must create a new enum type without the unwanted value, update all columns using the old enum to the new type (using ALTER COLUMN TYPE ... USING), then drop the old type. This is a multi-step process that can lock tables. Alternatively, you can rename the enum type and recreate it with the desired values.
Does the same restriction apply to ALTER TYPE ... RENAME VALUE or ALTER TYPE ... SET SCHEMA?
No. ALTER TYPE subcommands have different behavior. Check the official documentation for the exact subcommand and PostgreSQL version instead of applying the ADD VALUE rule to RENAME VALUE or SET SCHEMA.
What if I'm using a migration tool that doesn't support non-transactional scripts?
For an older PostgreSQL server, use a separate autocommit connection or a documented pre-migration step outside the tool's transaction wrapper. On current PostgreSQL, split the addition and its first use into separate committed migrations.
Can I add multiple enum values in one ALTER TYPE statement?
No. ADD VALUE adds one label per statement. On current PostgreSQL, multiple additions can share a transaction, but none of the new values can be used until commit. Older servers may require those statements to run outside a transaction block.