用独立验证工具对 AI 生成的迁移脚本在临时 SQLite 数据库上重放,拦截无回滚证据的危险操作,避免生产事故。
AI 辅助数据库工作最难的部分不是生成迁移,而是判断这条迁移能否在真实的数据库引擎上存活。文本 review 能发现缺失的索引或可疑的命名规范,但往往漏掉真正重要的失败场景:比如一条 DROP COLUMN 作用在某后台任务仍在查询的列上,或者一条 DELETE 在本地能跑通但在带有生成列的表上却不行。
当前关于限制 AI 工具的讨论通常集中在权限层面。这很重要,但对数据库工作来说,更实用的切分点是确定性验证。模型提出一个变更;一个独立的、枯燥的验证工具在独立环境中让这个变更证明自己可行,然后才需要人介入判断。
本文展示了一个完成上述工作的轻量验证工具。它接收一条生成的迁移,在临时 SQLite 数据库上执行它,拒绝没有回滚证据的危险语句,并在任何一步失败时以非零退出码退出。重点不在于具体的 SQL 方言,而在于提案和验证保持分离。
一个实用的方案是:用 MonkeyCode 的免费模型起草迁移,用它的免费服务器选项运行 replay 套件。声明:本文作为 MonkeyCode 产品推广的一部分撰写。但这个验证工具本身是提供商无关的。你可以把生成器指向任何你已在用的端点,也可以把验证工具跑在本地、CI 或常驻服务器上。
模型对快速产出初稿很有用,但它不是证明正确性的合适地方。模型可以生成在本地能解析的 SQL,却仍然会删除一个列、锁住一张大表,或违反一个只在生产环境存在的约束。这些失败是确定性的,所以应该由确定性的代码来检查。
保存为 prove_migration.py。
#!/usr/bin/env python3
import subprocess
import sys
import tempfile
from dataclasses import dataclass
@dataclass
class CheckResult:
name: str
passed: bool
detail: str
def run(cmd, cwd, timeout=120):
return subprocess.run(
cmd,
cwd=cwd,
capture_output=True,
text=True,
timeout=timeout,
)
def make_scratch_db(kind='sqlite'):
if kind == 'sqlite':
tmp = tempfile.mkdtemp()
return tmp, f'{tmp}/scratch.db'
raise NotImplementedError('swap in a Postgres container for non-SQLite checks')
def parse_migration(patch):
# Simplified. A production guard should use a real SQL parser.
statements = [s.strip() for s in patch.split(';') if s.strip()]
if not statements:
raise ValueError('no statements found')
return statements
def is_destructive(statement):
lowered = statement.lower()
destructive_markers = ['drop table', 'drop column', 'delete ', 'truncate']
return any(marker in lowered for marker in destructive_markers)
def check_migration(patch):
results = []
db_dir, db_path = make_scratch_db()
baseline_schema = 'CREATE TABLE orders(id INTEGER PRIMARY KEY, status TEXT);'
baseline = run(['sqlite3', db_path, baseline_schema], db_dir)
results.append(CheckResult('baseline_schema', baseline.returncode == 0, baseline.stderr.strip()[:200]))
statements = parse_migration(patch)
for index, statement in enumerate(statements, start=1):
result = run(['sqlite3', db_path, statement], db_dir)
detail = statement[:80]
if result.returncode != 0:
detail = result.stderr.strip()[:300]
results.append(CheckResult(f'statement_{index}', result.returncode == 0, detail))
for statement in statements:
if is_destructive(statement):
results.append(CheckResult(
'rollback_guard',
False,
f'destructive statement has no rollback evidence: {statement[:80]}',
))
return results
def prove_patch(patch):
results = check_migration(patch)
for result in results:
status = 'PASS' if result.passed else 'FAIL'
print(f'[{status}] {result.name}: {result.detail}')
return all(result.passed for result in results)
if __name__ == '__main__':
patch = sys.stdin.read()
ok = prove_patch(patch)
sys.exit(0 if ok else 1)
通过管道把生成的迁移传入标准输入来运行:
python prove_migration.py < candidate_migration.sql
一条添加列的非危险迁移会通过。即使 SQLite 接受了该语句,包含裸 DROP COLUMN 的迁移也会触发回滚守卫而失败。这种"粗暴"是刻意设计的。危险变更永远不应该仅仅因为语法没问题就放行。
如果你的笔记本在 replay 过程中总是进入睡眠,同样的脚本可以在免费服务器上运行。唯一真正的依赖是 Python 运行时和目标数据库引擎。长时间运行的 replay 套件更适合无人值守的服务器,而不是随时可能合上盖子的笔记本。
最重要的设计选择是:模型不参与验证循环。不要让一个模型生成迁移,然后又让一个模型在没有可执行中间步骤的情况下去 review 它。模型可以自信地错两次。验证工具应该是笨拙的、可读的,并且对补丁文本听起来多有说服力漠不关心。
一个有用的起步规则集如下:
表格故意设得保守。在看到误报后再放宽规则,比事后发现危险迁移漏了过去要容易得多。
这个验证工具不是真正的迁移工具的替代品。简单的分号分割器会无法处理字符串、函数或过程化 SQL 内部的冒号。SQLite 临时数据库无法捕获只在 Postgres、MySQL 或特定扩展中才会出现的行为。回滚守卫是启发式的;它拦截明显的危险语句,但不会重建回滚计划或验证时间点恢复。
它也不能保护那些语法正确但语义错误的生成 SQL。模型可能添加了类型错误的列,而临时数据库会接受它。这仍然需要人来将变更与需求意图进行对比。
最后,不要对包含真实客户数据的数据库运行不受信任的生成 SQL。临时数据库应该始终是可丢弃且隔离的。
对每条迁移都有强制数据库管理员 review 的团队,可能会觉得这个工具作为 pre-review 过滤器仅略有帮助。在受监管的 schema 上,或生产迁移需要时间点恢复的团队,应该保留现有的变更管理流程,只把这个工具当作本地合理性检查。如果你已经有了付费 CI runner 和成熟的迁移流水线,可能不再需要单独的免费服务器来执行 replay 步骤,尽管这个验证工具本身仍然值得复制。
模型是可以替换的。验证工具才是持久的产物。一条提议的迁移应该在临时数据库上通过 replay,识别出任何缺少回滚证据的危险语句,并在人工介入 review 之前产生机器可读的成功或失败信号。如果你维护的是迁移密集型的代码库,验证工具是值得复制的部分;模型可以保持廉价且可替换。