SQLAlchemy 反射 PostgreSQL 18 的 PERIOD 外键时报 NoneType.groups
PostgreSQL 18 时间外键可触发旧 SQLAlchemy 的反射正则错误。使用包含修复的正式版本后仍须复核迁移,因为 PERIOD 语义尚未完整保存在反射结果中。
数据库里已经存在合法的时间外键,调用 inspect(engine).get_foreign_keys() 或 Alembic 自动生成迁移时,却在 PGDialect._parse_fk 报:
AttributeError: 'NoneType' object has no attribute 'groups'
如果环境是 PostgreSQL 18,约束定义包含 PERIOD,应检查 SQLAlchemy 对新语法的反射支持。这个 NoneType 来自约束文本解析未匹配,不是数据库返回了一个空外键对象。
PERIOD 让旧解析规则匹配不到约束
例如,已有约束的文本可能具有下面的形状:
FOREIGN KEY (parent_id, PERIOD valid)
REFERENCES timeline(id, PERIOD valid)
timeline、valid 是示例名称。这只是已有约束的形状,不应直接复制去生产创建外键;实际表还要满足时间范围、唯一约束及关联关系的要求。
旧方言按普通列名列表解析 REFERENCES 后的列,不能处理 PERIOD valid 中的关键字与列名组合。正则匹配返回 None 后,继续调用 .groups(),整张表的反射就失败了。改用户名、驱动连接参数或给数据库列补默认值,都不会修正这个解析入口。
先用包含修复的正式版本恢复反射
明确记录过问题的组合是 SQLAlchemy 2.1.1、psycopg 3.3.6 与 PostgreSQL 18.6。已核对的正式 SQLAlchemy 2.0.55 与 2.1.5 包含对应解析修复;这是确认包含修复的版本,不要求现有项目为了一个问题跨到另一个主分支。
在隔离环境恢复项目锁定依赖、使用包含修复的版本后,对已有目标表进行只读反射:
from sqlalchemy import inspect
# engine 为应用已有、获准访问相应测试数据库的 Engine
foreign_keys = inspect(engine).get_foreign_keys("timeline")
for key in foreign_keys:
print(key["constrained_columns"], key["referred_columns"])
这里不创建或删除表。若数据库使用非默认 schema,还要在反射调用中传入正确的 schema,避免检查到另一张同名表。
能反射列名,不代表能完整重建时间外键
修复版本能够识别这段语法并保留普通外键列,当前反射结果却仍会丢弃 PERIOD 的标记。不能据此从反射对象重新生成 DDL,再认为时间约束一定原样保留。
尤其是 Alembic autogenerate:先生成候选迁移,逐项对照数据库原约束定义,检查有没有错误删除时间约束或改建为普通外键。保留数据库结构备份和原迁移,不要看到自动生成成功就直接执行。
暂时无法升级时,按表名在反射前排除受影响表可以缩小范围;include_object 若在反射后才执行,未必来得及阻止解析失败。排除还要与 target_metadata 的管理范围保持一致,防止把未反射到的表误认成待创建对象。仅为绕过一次自动生成,删除真实数据库外键并不合适。
完成标准包括原表反射成功,以及生成迁移没有丢掉时间关系语义。若其他普通约束仍报同样异常,应保留它的实际定义继续检查,不能将所有 .groups() 错误都归到 PostgreSQL 18。


