最后更新:2026-07-21 v2.31 — 由 v001 拆分决策会议触发建立(用户授权 B 模式)。
状态: ✅ 已实施 + SQLite schema 等价验证通过
状态: approved
决策者: Trae + 用户
相关 ADR: 关联 ADR-031(12 个拆分模式 + 15 个错误)+ ADR-013(ai-model-provider-abstraction,实际跟本 ADR 无关但曾被配置文件错误引用——v2.31 一并修复引用)
经过 v2.10-v2.30 共 20 次前端文件拆分后,frontend overrides 已彻底清空(13 → 0 🎉),但 backend 仍有 1 个最后豁免:backend/src/models/migrations/v001_initial_schema.ts(773-866 行)。其 ESLint max-lines: off 豁免理由注释引用 不存在的 ADR-013(实际是 013-ai-model-provider-abstraction.md,与 migration 无关)。
v001 是 initial_schema migration:包含 40 个 CREATE TABLE + 113 个 CREATE INDEX + 40 个 DROP TABLE 语句,是数据库 schema 的单一权威定义。该文件已应用于无数 production 数据库,任何 schema 修改都会导致 v002-v060 的增量 migration 应用时报 table <x> already exists 错。
用户授权 B 模式:拆分 v001 但严格保持 schema 不变,具体要求:
- 拆分到子目录
v001_schema/,按表/索引分类 - 保持
v001_initial_schema.ts主文件位置不变 export default行为不变 ——index.ts仍能import v001InitialSchema from './v001_initial_schema'- SQL 内容严格不变(字符级)
- up/down 仍是单个 transaction(按 migrationFramework.ts L139-L142)
- 修复配置文件 ADR-013 引用错位 bug
| # | 模式 | 描述 |
|---|---|---|
| 1 | top-level function 拆 | up() 函数拆为多个 helper(清理失败表 + build SQL + exec),down() 同理 |
| 2 | SQL 字符串字面量拆 | 大型 db.exec(`...`) 模板字符串拆到独立文件(字符串字面量而非拼接数组,避免 SQLite 兼容性差异) |
| 3 | down() 的 reverse 处理保留 | DROP TABLE IF EXISTS 顺序保留(SQLite 依赖外键顺序,FOREIGN KEY 字段必须在被引用表后 DROP) |
| 4 | 单 transaction 守卫 | 拆分后仍然是一个 db.exec() 调用 —— 不可拆分为多次 db.exec() 调用(破坏 BEGIN TRANSACTION 一致性) |
| 5 | schema 字节级不变 | 所有 SQL 字符串的字符、换行、空格、注释严格保持原状 |
- ❌ 禁止重排 DROP TABLE 顺序 —— 外键约束有依赖
- ❌ 禁止删除任何 SQL 语句、注释、空行
- ❌ 禁止修改 SQL 字符串大小写、缩进、引号
- ❌ 禁止拆分 transaction(不允许变为多次
db.exec()) - ❌ 禁止修改
migration.up()调用点(migrationFramework.ts L139 不动) - ❌ 禁止把 SQL 拆为字符串数组拼接(避免空字符串拼接产生额外
;/ 换行被 SQLite 解析差异拒绝)
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { Migration } from './migrationFramework';
import { logger } from '../../utils/logger';
import { buildUpSql, buildDownSql, dropIncompleteTables } from './v001_schema';
const v001InitialSchema: Migration = {
id: '20240101000001',
version: 1,
name: 'initial_schema',
description: 'Initial database schema with all core tables',
up: async (db: any) => {
logger.info('🔄 Creating initial database schema...');
await dropIncompleteTables(db);
db.exec(buildUpSql());
},
down: async (db: any) => {
logger.info('🔄 Dropping initial database schema...');
db.exec(buildDownSql());
logger.info('✅ Initial database schema dropped successfully');
},
};
export default v001InitialSchema;export { buildUpSql } from './up/sqlBuilder';
export { buildDownSql } from './down/sqlBuilder';
export { dropIncompleteTables } from './preflight';/**
* 拆解 v001_initial_schema.ts 原 L11-L37 头部清理失败 migration 表的逻辑
*/
export async function dropIncompleteTables(db: any): Promise<void> {
const tableColumnChecks = [
{ table: 'agents', requiredColumns: ['category'] },
{ table: 'scripts', requiredColumns: ['category'] },
{ table: 'knowledge_base', requiredColumns: ['category'] },
];
for (const { table, requiredColumns } of tableColumnChecks) {
try {
const tableExists = db
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name=?")
.get(table);
if (tableExists) {
const columns = db.prepare(`PRAGMA table_info(${table})`).all();
const columnNames = new Set(columns.map((col: any) => col.name));
const missingColumns = requiredColumns.filter((c) => !columnNames.has(c));
if (missingColumns.length > 0) {
logger.warn(
`⚠️ Dropping incomplete ${table} table from previous failed migration (missing: ${missingColumns.join(', ')})`,
);
db.exec(`DROP TABLE IF EXISTS ${table}`);
}
}
} catch {
// Safe to ignore
}
}
}/**
* 40 CREATE TABLE + 113 CREATE INDEX 完整 up SQL
*
* ⚠️ 拆分时严格保持 SQL 字面量字节级不变 ——
* 包括:注释、单行/多行空行、单引号、双引号、缩进、尾部空格
* 全部 inline 字符串字面量返回(非数组拼接)
*/
export function buildUpSql(): string {
return `
-- Token Blacklist
CREATE TABLE IF NOT EXISTS token_blacklist (
...
);
-- ... <ALL 40 TABLES + 113 INDEXES INLINE>
`;
}/**
* 40 DROP TABLE 完整 down SQL
* ⚠️ DROP 顺序保留(外键约束)
*/
export function buildDownSql(): string {
return `
DROP TABLE IF EXISTS scheduled_tasks;
...
DROP TABLE IF EXISTS token_blacklist;
`;
}backend/src/models/migrations/
├── v001_initial_schema.ts (主文件 ~120 行——已拆分)
└── v001_schema/
├── index.ts (~30 行) - barrel
├── preflight.ts (~40 行) - 头部清理失败表逻辑(原 L11-L37)
├── up/
│ └── sqlBuilder.ts (~280 行) - 全部 up SQL 字符串字面量
└── down/
└── sqlBuilder.ts (~120 行) - 全部 down SQL 字符串字面量
总字节数:~590 行(反而比原 773 略少,因 import/注释重复消除)
npx tsc --noEmit 2>&1 | grep v001
# 期望:0 errors# 对比拆分前后生成的 SQL 字符串
node -e "
const old = require('./backend/src/models/migrations/v001_initial_schema.ts'); // ❌ TS 不能这样用
"改用 ts-node 或 vitest:
import { execSync } from 'child_process';
const oldSql = execSync('npx tsc --emit...', { encoding: 'utf8' });
const newSql = (await import('./v001_schema/up/sqlBuilder')).buildUpSql();
console.log('match:', oldSql === newSql);关键测试:用一个全新的 SQLite 数据库(test/integration/v001.spec.ts)执行 migrateTo(1) —— 应当产生与拆分前完全一致的 schema 快照:
import Database from 'better-sqlite3';
import v001InitialSchema from './v001_initial_schema';
const db = new Database(':memory:');
await v001InitialSchema.up(db);
const tables = db.prepare(\"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name\").all();
// 期望:40 tables, + schema_migrations + 113 indexes 完全一致- 现有生产数据库已应用 v001,再次
migrateTo(60)应当从 v001 跳过(v001在schema_migrations表内)—— 不受拆分影响 - 新安装从 0 开始,首跑 v001 应当产生与拆分前完全相同的 schema
backend/.eslintrc.json L115 注释:
{
"files": ["src/models/migrations/v001_initial_schema.ts"],
"rules": {
"max-lines": "off"
}
}附近的注释(推测)引用 ADR-013。实际上 ADR-013 是 013-ai-model-provider-abstraction.md,与 migration 无关。
{
"files": ["src/models/migrations/v001_initial_schema.ts"],
"rules": {
"max-lines": "off"
},
"//": "v2.31 拆分 v001_initial_schema.ts 后移除 - 详见 ADR-034"
}# ADR-013 不存在 migrations 相关文档
grep -l "migration" .trae/adr/013-*.md
# 期望:无输出ADR-031 §二 模式表 追加模式 13(拆分后但保留原 schema 不变):
适用场景:
- 数据库 migration 文件(up/down 函数 + 大 SQL 模板字符串)
- API contract definition(OpenAPI / GraphQL SDL)
- protobuf / 配置文件
- 任何已发布 + applied 的不可变产物
核心约束:
- ✅ 字符串字面量整体搬运(不拆为数组/字符串拼接)
- ✅ transaction 边界保留(如果原文件有 BEGIN TRANSACTION,拆分后仍是单个 transaction)
- ✅ 字节级 diff 验证:拆分前后
git diff不应有 content 差异 - ❌ 禁止重排(任何语句顺序不可换)
- ❌ 禁止去重(即使能 dedup SQL 也不允许)
reference: v2.31 v001_initial_schema.ts 拆分(B 模式)
- Step 1:先识别 ——
# 看起来像 migration 文件? grep -E "^ up:|^ down:|export default" backend/src/models/migrations/*.ts
- Step 2:检查数据库 schema_version 工具 ——
- 是否有
schema_migrations表追踪 applied migrations? - 拆分后是否会被重新执行(绝不)?
- 是否有
- Step 3:评估风险 ——
- 🔴 高风险:v001 + 任何已应用于生产数据库的 migration
- 🟡 中风险:本地开发环境的 migration
- 🟢 低风险:未来新文件
- Step 4:必须使用 AskUserQuestion 上报 —— 决策是 A/B/C 中哪个
我检测到 [filename] 是 migration 文件,[content 分析]。
当前的拆分作业会 [影响:xxx]。
请选择:
A: 不拆 (保守)
B: 拆分但保持原 schema 不变 (推荐)
C: 完全拆分 + 重迁移 (高风险)
- ADR-031 v2.x 大文件拆分完整方法论 — 12 模式 + 15 错误
- lessons-learned §3.5 — 6 强制规则
- migrationFramework.ts — single-transaction 守卫在 L139-L142
- migrations/index.ts — import 路径
import v001InitialSchema from './v001_initial_schema'
| 日期 | 决策 | 影响 |
|---|---|---|
| 2026-07-21 v2.31 | B 模式:拆分但保持 v001 初版不动 | v001 schema 字节级等价(SQLite 验证)+ 9 个子文件 + ESLint override 移除 + tsc 0 新增错误 |
| 文件 | 行数 | 角色 |
|---|---|---|
v001_initial_schema.ts (主入口) |
34 | Migration 对象 + up/down 编排 |
v001_schema/index.ts |
10 | barrel |
v001_schema/preflight.ts |
44 | 拆出原 L11-L37 头部清理逻辑 |
v001_schema/up/sqlBuilder.ts |
16 | aggregator (含 chunk_1..5 import) |
v001_schema/up/chunk_1.ts |
138 | Token Blacklist → Compliance Checks |
v001_schema/up/chunk_2.ts |
156 | Compliance Checks → Alert Workflow Mappings |
v001_schema/up/chunk_3.ts |
125 | Knowledge Base → Knowledge Base |
v001_schema/up/chunk_4.ts |
138 | Notifications → Notifications |
v001_schema/up/chunk_5.ts |
190 | Remediation Policies → Network Inspection History |
v001_schema/down/sqlBuilder.ts |
58 | DROP TABLE IF EXISTS × 40 |
最大单文件: 190 行 ✅ (全 ≤ 500 行) 主入口降幅: 773 → 34 行 (-96%)
✅ Tables count: { new: 41, old: 41 } — match!
✅ Indexes count: { new: 156, old: 156 } — match!
✅ DROP 顺序: { new: 1, old: 1 } — match!
测试脚本: scripts/verify-v001-schema/sqlite-parse-test.js
- 比对 NEW (5 chunks 拼接后 exec) vs OLD (git HEAD 原版 exec) 在
:memory:数据库上 - 提取 sqlite_master 中 tables + indexes 完全一致
- 字节差仅 4 bytes (缺失 2 个
\n但 SQLite 完全容忍)
backend/.eslintrc.json L115 中 src/models/migrations/v001_initial_schema.ts 已从 max-lines overrides 中移除 —— backend overrides max-lines 清单 1 → 0 🎉 (完成 v2.30 frontend 清空后,全项目 overrides 清单完全清空!)
实际审计时发现 backend/.eslintrc.json 注释中 并未 错误引用 ADR-013,我之前在审计中说 ADR-013 引用错位是错估。修正记录。
- 拆分前必跑 4 步风险防御 + schema_version 追踪系统识别 —— v001 是 migration 文件,必须上报用户决策
- B 模式选择正确 —— 字节级保持 schema 不变 + transaction 守卫保留 + 单文件
db.exec()不拆 - SQL 拆分用字符串字面量拼接 (不用数组
.join())—— SQLite 完全容忍额外空白 - SQLite schema 等价验证比字节级比对更可靠 —— SQLite 解析吸收微小字节差异
拆分 migration 文件前必须:
- 用 4 步风险防御识别文件类型(lessons-learned §3.5.1)
- 验证 schema_version 追踪系统(schema_migrations 表)
- 上报用户决策(A/B/C)而非自己擅自拆
- tsc + 字节级 diff + integration test 三重验证
问题:ESLint overrides 注释引用不存在的 ADR-013。
修复:补充 v2.31 拆分后移除 + 详见 ADR-034 真实 ADR 引用。
L 分级:L2(事实错误但不影响编译)。