AI 生成的迁移脚本在开发环境通过但在生产环境失败或静默截断数据。揭示 AI 缺乏对实际数据和环境的感知。
数据库迁移是 AI 生成代码最容易悄无声息地失败、并造成高昂损失的地方。AI 助手生成的迁移文件可以通过 lint 检查,也能在空的开发数据库上顺利执行。然后它被部署到生产环境——要么在执行迁移时失败,要么悄悄截断那些无法适配新列类型的值。linter 没发现任何问题,代码审查也没有。因为问题出在结构上,而不是语法上。
AI 编程助手通过匹配训练数据中的模式来生成迁移——它们可以生成语法正确的 Alembic 或 Django 迁移文件,却不会检查实际运行的数据库 schema、现有数据的分布情况,也不了解生产环境数据库引擎的差异,而这些因素决定了一次迁移是否安全。BrassCoders 会扫描迁移文件中的安全风险模式,例如硬编码连接字符串、使用格式化字符串的原始 SQL,但它不会评估 schema 变更本身在结构上是否安全。
这种缺口源于这些工具本身的工作方式。AI 助手能够看到你在当前对话中描述的模型定义,以及从 import 中获取的一些 ORM 样板代码。它不知道 users 表中已经存放了哪些数据,也不了解生产环境使用的 PostgreSQL 版本,以及你正在修改的列上有哪些下游外键约束。Alembic 的 autogenerate 模式会检查实际数据库并基于差异生成迁移,从而弥补部分缺口,但 AI 助手经常完全绕过 autogenerate,直接从头编写 op.add_column() 调用。结构性错误正是从这里混入的。
BrassCoders 会标记用于生成迁移的 Python 代码,但真正的数据丢失风险存在于三种结构性模式中:向已有数据的表添加没有 server_default 的 NOT NULL 列;修改列类型,导致现有值被截断;以及生成在 SQLite 测试数据上可以正确执行、却会在 PostgreSQL 生产 schema 上失败的迁移。
NOT NULL 是最常见的模式。AI 助手看到模型中新增了一个设置为 nullable=False 的字段,便会生成 op.add_column(table, Column('status', String, nullable=False))。在空表上,这条迁移可以顺利执行。但对于一张已有 50,000 行数据的表,PostgreSQL 会在执行迁移时拒绝该操作:现有行没有新列对应的值,因此无法满足约束。正确的迁移需要分两步进行:首先将该列添加为 nullable,然后单独执行一次数据迁移,回填现有行,最后再将其转换为 NOT NULL。AI 助手通常只会生成列定义,却漏掉数据回填。
列类型变更的问题更加隐蔽。在 SQLAlchemy 中将 String(500) 缩窄为 String(100),会在 PostgreSQL 下生成 ALTER TABLE ... ALTER COLUMN,从而悄悄截断长度超过 100 个字符的值。有些数据库会抛出错误;而 PostgreSQL 可能会直接截断,具体取决于版本和配置。AI 助手生成的代码在语法上完全正确,却会在语义上破坏数据。
SQLite 几乎接受任何列类型,并会默默进行数据类型转换;PostgreSQL 则会严格执行类型约束,拒绝可能造成数据丢失的操作。因此,当 AI 助手针对 SQLite 运行测试时,看到的是一次顺利通过的迁移,但同一迁移部署到 PostgreSQL 生产数据库后,却可能破坏数据或被数据库拒绝。BrassCoders 的安全扫描器会标记生产配置中硬编码的 SQLite 路径,但类型转换方面的差异属于运行时行为差异,而不是源代码中的模式。
来看一个具体示例:SQLite 没有原生 boolean 类型。它在 integer 列中将 True 存储为 1,将 False 存储为 0。如果 AI 助手编写了一次将 boolean 列改为 string 的迁移,SQLite 会悄悄地把 integer 转换为 string。PostgreSQL 的类型系统则更加严格。同一个迁移可能会报错,也可能产生意料之外的类型转换。测试套件在 SQLite 上通过,生产环境却在 PostgreSQL 上失败。这并不是一个静态分析问题。静态分析可以标记列类型变更,但这种变更是否安全,取决于列中已经存在什么数据。只有实际运行的数据库才能给出答案。
JSON 列、ARRAY 类型和时间戳精度也会反复出现相同的问题。SQLite 宽松的类型系统会吸收这些不匹配,而 PostgreSQL 则会以错误或数据变更的形式将其暴露出来。
BrassCoders 会扫描 Python 迁移文件中的安全风险模式,例如硬编码连接字符串、使用字符串格式化的原始 SQL,但结构性安全检查应当放在一个单独的步骤中:在迁移接触生产环境之前,先在 CI 中针对生产 schema 的克隆执行迁移。
具体做法是:在 CI 中启动一个 PostgreSQL 实例——大多数 CI provider 都提供原生 PostgreSQL service——然后使用生产数据库 dump 为其填充数据,保留具有生产代表性的行数,但对 PII 进行匿名化。之后,在每个 pull request 中针对该数据库运行完整的迁移套件。对于 Django,执行 python manage.py migrate --database=ci。对于 Alembic,针对指向 CI 数据库的 connection string 执行 alembic upgrade head。如果迁移在真实数据规模下失败或产生意料之外的结果,你就能在部署前发现问题。
Django 的 migrations 文档介绍了 CI 测试数据库的连接模式,Alembic 关于 autogenerate 的文档则介绍了该工具如何检查实际 schema 并生成差异。两种方法都能捕获 AI 助手遗漏的结构性错误——但它们都需要一个包含真实数据的真实数据库。
在同一个 CI 步骤中,对迁移文件运行 BrassCoders。它可以发现结构性测试无法捕获的安全问题:数据迁移中的 op.execute(f"UPDATE users SET role = '{role}'") 即使在 schema 变更结构合理的情况下,仍然存在 SQL injection 风险。将两项检查结合起来,才能覆盖任何单项检查都无法独立处理的问题。
使用 pip install brasscoders 安装 OSS core,然后运行 brasscoders scan .——它会像扫描 Python codebase 的其他部分一样找到迁移文件,并标记其中的安全风险模式,全程不会将任何代码发送到外部。结构性数据库审查需要一个实际运行的 CI 数据库;安全审查则在本地运行,不会产生任何 outbound network call。
如需采取进一步措施,你可以考虑屏蔽此人和/或举报滥用行为。