import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import type { TestDatabaseHandle } from "../../helpers"; import { createMigratedMemoryTestDatabase } from "../../helpers"; interface ForeignKey { from: string; id: number; match: string; on_delete: string; on_update: string; seq: number; table: string; to: string; } interface IndexInfo { cid: number; name: string; seq: number; } interface IndexListEntry { name: string; origin: string; partial: number; seq: number; unique: number; } interface TableColumn { cid: number; dflt_value: null | string; name: string; notnull: number; pk: number; type: string; } const BUSINESS_TABLES = ["conversations", "materials", "messages", "models", "projects", "providers"] as const; const CHECK_CONSTRAINTS: Record = { materials: { column: "status", invalidValue: "'invalid_status'", validValue: "'pending'" }, messages: { column: "role", invalidValue: "'invalid_role'", validValue: "'user'" }, projects: { column: "status", invalidValue: "'invalid_status'", validValue: "'active'" }, providers: { column: "type", invalidValue: "'invalid_type'", validValue: "'openai-compatible'" }, } as const; const FK_INDEX_REQUIRED: Record = { conversations: ["model_id", "project_id"], materials: ["project_id"], messages: ["conversation_id"], models: ["provider_id"], } as const; const TABLES_WITH_FK = Object.keys(FK_INDEX_REQUIRED); describe("schema 契约", () => { let handle!: TestDatabaseHandle; beforeAll(() => { handle = createMigratedMemoryTestDatabase("schema-contract"); }); afterAll(() => { handle.cleanup(); }); describe("基础列(id / created_at / updated_at / deleted_at)", () => { for (const table of BUSINESS_TABLES) { test(`${table} 包含全部基础列且约束正确`, () => { const columns = handle.db.query(`PRAGMA table_info(${table})`).all() as TableColumn[]; const colMap = new Map(columns.map((c) => [c.name, c])); const idCol = colMap.get("id"); expect(idCol, `${table} 缺少 id 列`).toBeDefined(); expect(idCol!.pk, `${table}.id 必须为主键`).toBe(1); const createdAtCol = colMap.get("created_at"); expect(createdAtCol, `${table} 缺少 created_at 列`).toBeDefined(); expect(createdAtCol!.notnull, `${table}.created_at 必须 NOT NULL`).toBe(1); const updatedAtCol = colMap.get("updated_at"); expect(updatedAtCol, `${table} 缺少 updated_at 列`).toBeDefined(); expect(updatedAtCol!.notnull, `${table}.updated_at 必须 NOT NULL`).toBe(1); const deletedAtCol = colMap.get("deleted_at"); expect(deletedAtCol, `${table} 缺少 deleted_at 列`).toBeDefined(); expect(deletedAtCol!.notnull, `${table}.deleted_at 必须可空`).toBe(0); expect(deletedAtCol!.pk, `${table}.deleted_at 不可为主键`).toBe(0); }); } }); describe("外键索引", () => { for (const table of TABLES_WITH_FK) { const fkColumns = FK_INDEX_REQUIRED[table]!; test(`${table} 外键列 ${fkColumns.join(", ")} 均有索引`, () => { const indexes = handle.db.query(`PRAGMA index_list(${table})`).all() as IndexListEntry[]; const indexedColumns = new Set(); for (const idx of indexes) { const idxCols = handle.db.query(`PRAGMA index_info(${idx.name})`).all() as IndexInfo[]; for (const col of idxCols) { indexedColumns.add(col.name); } } for (const fkCol of fkColumns) { expect(indexedColumns, `${table}.${fkCol} 应有索引`).toContain(fkCol); } }); } }); describe("外键级联策略", () => { for (const table of TABLES_WITH_FK) { test(`${table} 所有外键使用 NO ACTION`, () => { const fks = handle.db.query(`PRAGMA foreign_key_list(${table})`).all() as ForeignKey[]; expect(fks.length, `${table} 应至少有一个外键`).toBeGreaterThan(0); for (const fk of fks) { expect( fk.on_delete.toLowerCase(), `${table}.${fk.from} → ${fk.table}.${fk.to} 应为 no action,实际为 ${fk.on_delete}`, ).toBe("no action"); } }); } }); describe("CHECK 约束(枚举列)", () => { for (const [table, spec] of Object.entries(CHECK_CONSTRAINTS)) { test(`${table}.${spec.column} 拒绝非法枚举值`, () => { handle.db.exec("SAVEPOINT check_test"); try { expect(() => { handle.db .query( `INSERT INTO ${table} (id, ${spec.column}${table === "messages" ? ", conversation_id, created_at, updated_at" : table === "materials" ? ", project_id, associated_date, description, created_at, updated_at" : table === "providers" ? ", name, api_key, base_url, created_at, updated_at" : ", name, description, created_at, updated_at"}) VALUES ('__check_test__', ${spec.invalidValue}${table === "messages" ? ", 'conv-x', '2024-01-01', '2024-01-01'" : table === "materials" ? ", 'proj-x', '2024-01-01', '', '2024-01-01', '2024-01-01'" : table === "providers" ? ", 'p', '', '', '2024-01-01', '2024-01-01'" : ", 'pj', '', '2024-01-01', '2024-01-01'"})`, ) .run(); }).toThrow(); } finally { handle.db.exec("ROLLBACK TO SAVEPOINT check_test"); handle.db.exec("RELEASE SAVEPOINT check_test"); } }); } }); });