DEBUG.WIKI
丽萨主机;为跨境业务选好 IP;原生 / 住宅 IP · 按套餐选择;查看 IP →
阿里云;从建站到云上业务;云服务器 · 数据库 · 对象存储;查看产品 →
Evoxt;最高 6.0GHz CPU;高频云服务器 · 免费每周备份;查看配置 →

PostgreSQL 新增枚举值后使用时报 unsafe use of new value

PostgreSQL 向既有枚举类型添加值后,默认值、约束或写入语句报 unsafe use of new value,需要先提交新增标签,再在下一事务使用。说明两阶段迁移、客户端外层事务、验证与回退边界。

数据库迁移先执行 ALTER TYPE ... ADD VALUE,随后在默认值、约束或写入语句中使用新标签,却报:

ERROR: unsafe use of new value "RUNNING" of enum type job_state
HINT: New enum values must be committed before they can be used.

如果枚举类型在这次迁移之前就已存在,首先检查添加标签和使用标签是否处于同一个尚未提交的事务。ALTER TYPE 返回成功,表示这条语句执行完了,并不表示事务已经提交。

针对既有枚举类型,处理方法是在添加新值后完成一次真正的提交,再让下一事务使用它。把两条 SQL 分成两个文件,只有在迁移工具也分别提交时才有效。

错误发生在“使用新值”这一步

假设 public.job_state 已经存在并提交,包含 PENDING、DONE,public.jobs.state 使用该类型。以下迁移会把添加与使用放在同一事务中:

BEGIN;
ALTER TYPE public.job_state ADD VALUE 'RUNNING';
ALTER TABLE public.jobs
  ALTER COLUMN state SET DEFAULT 'RUNNING'::public.job_state;
COMMIT;

新标签不仅可能在 INSERT 时被使用。默认表达式、CHECK 条件以及直接把字符串转换为枚举类型,都可能触发检查。因此,即使还没有向表写入新状态,也可能在第二条 DDL 就失败。

这种限制与枚举值在索引中的安全使用有关。对既有类型新增的标签,如果尚未提交就进入某些索引,随后事务回滚,可能留下无法安全处理的索引内容。数据库会阻止这类未提交标签被使用,不应直接修改系统目录来绕过检查。

这里的版本范围需要分清:PostgreSQL 12 起允许在事务块中向既有枚举类型添加值,但使用新值仍需等待提交。更早版本对 ADD VALUE 的事务限制不同,不应照搬下面的事务写法。类型也在同一事务中新建属于另一种情形,存在实现与版本差异;新库初始化时,直接在 CREATE TYPE ... AS ENUM (...) 中列出所需标签更清楚。

将新增标签与使用标签分成两个提交阶段

先确认当前连接没有停留在失败事务中。手工操作时,如果本次迁移已经报错且事务未恢复,应先回滚这次失败事务;这会撤销该事务里其他未提交的修改,不能在不了解其中内容时直接执行:

ROLLBACK;

随后按以下顺序执行。类型、表和列名都是示例,请换成实际对象;执行账号应具备相应对象的修改权限。变更前保留迁移脚本、原默认值或约束定义,并准备数据库备份。

第一阶段只添加枚举标签并提交:

BEGIN;
ALTER TYPE public.job_state ADD VALUE IF NOT EXISTS 'RUNNING';
COMMIT;

IF NOT EXISTS 方便在确认状态后重试:标签已存在时发出通知,不重复添加。它不会提前提交,也不会解除同一事务内使用刚新增标签的限制。

确认第一阶段成功提交后,再修改依赖新标签的定义:

BEGIN;
ALTER TABLE public.jobs
  ALTER COLUMN state SET DEFAULT 'RUNNING'::public.job_state;
COMMIT;

这段示例只改变默认值,不会把已有行的状态批量改成 RUNNING。如果真正需要修改约束或回填数据,把对应操作放在第二阶段,并分别核对约束逻辑、已有数据和影响范围。不要为了消除这个报错而先删除所有约束。

这两个阶段不再是一次整体原子提交:第一阶段成功后,第二阶段仍可能失败。应用发布顺序也要允许这种中间状态,例如先让旧应用容忍数据库多出一个尚未使用的标签,再切换会写入新标签的版本。

文件已经拆开,仍报错时检查外层事务

检查执行器实际发送给 PostgreSQL 的事务边界,不能只看文件中的分号:

  • 迁移工具可能把整批文件包在同一事务里。需要确保第一阶段提交后,才启动第二阶段;具体设置以正在使用的工具和版本为准。
  • psql -1 或 --single-transaction 会把指定的一组 -c、-f 操作包在一个事务中。两个文件放在同一次这样的调用里,不会天然得到两次提交。
  • 客户端关闭自动提交后,文件里没有 BEGIN 也可能仍处于事务内。应检查连接设置与执行日志。
  • 保存点的释放不等于提交外层事务。把第二步移到保存点之后,也不能替代第一阶段的提交。

不要在框架托管的迁移里随意插入 COMMIT,再假设框架会正确记录结果。先确认它允许的事务控制方式,保持迁移历史与数据库实际状态一致。使用 psql 执行脚本时,可启用 ON_ERROR_STOP,让错误终止后续脚本处理,避免错过前面的失败又继续执行下一阶段。

如果失败事务被完整回滚,刚添加的标签也可能已被撤销。先核对状态再重跑添加步骤,不要只重复执行使用新值的语句。

验证提交结果与回退边界

第一阶段提交后,用一个新连接执行只读转换:

SELECT 'RUNNING'::public.job_state;

能够返回该枚举值,说明这个连接已经可以使用已提交标签。它还不能证明默认值、约束或回填已经完成;第二阶段仍需核对迁移记录,并按应用的真实写入路径在测试环境验证。若变成 invalid input value for enum,应核对连接的数据库、schema、标签大小写以及第一阶段是否真的提交,不能把不同错误都归为同一个事务问题。

第一阶段提交之前,可以通过回滚撤销新增标签。提交之后,PostgreSQL 没有直接删除单个枚举标签的常规命令。第二阶段失败时,通常先恢复或保持旧默认值、旧约束与旧应用行为,暂留尚未使用的标签;如果已有数据写入新状态,还需评估旧应用能否读取这些行。

不要用 DROP TYPE ... CASCADE 清理一个新标签。它可能连带删除依赖对象。确实要缩减枚举集合时,应单独设计类型替换与数据迁移,并安排备份、依赖检查和停机评估。