/** * v0.8.0 回归套件 —— B-1 统一行校验(工作流 B 第 1 项) * ============================================================================ * 背景(PLAN-v0.7.5.md 根因 1 + 缺陷 A12/A17): * 修复前存在**三份**行校验实现,覆盖面各不相同 —— Memory/KVStore/Hybrid 共用 * 一份"只查类型"的实现(**没有** maxLength/min/max),Aria 走 checkFieldType * (有约束),schema.ts 是第三份。于是: * * A12: `CREATE TABLE t (name STRING(3))` + `INSERT ... 'abcdef'` * → Aria 抛 VALIDATION_ERROR,memory/disk/hybrid **静默写入超长值**。 * 用户的约束是否生效取决于选了哪个引擎。 * * A17: `INSERT INTO t (id, nope) VALUES ('1', 2)` * → 四个引擎全部**静默丢弃 nope**、INSERT 报成功;随后 `SELECT nope` * 又抛 COLUMN_NOT_FOUND —— 同一列名在写路径与读路径得到相反结论。 * * 本套件对四引擎逐一断言相同结论,锁定"校验只有一份实现"。 */ import { MetonaSqlark } from '../src/core'; import { compileValidator } from '../src/table/validation'; import { createSchema } from '../src/table/schema'; import { rows as rowsOf, expectCode } from './helpers/assertions'; import type { DatabaseConfig } from '../src/constants'; const ENGINES: Array<[string, DatabaseConfig['mode'], Partial]> = [ ['memory', 'memory', {}], ['disk', 'disk', {}], ['hybrid', 'hybrid', {}], ['aria', 'aria', { diskEngine: 'memory' }], ]; describe('[v0.8.0] B-1 统一行校验 —— 四引擎行为必须一致', () => { describe.each(ENGINES)('%s 引擎', (label, mode, extra) => { let db: MetonaSqlark; beforeEach(async () => { db = await MetonaSqlark.create({ name: `b1-${label}-${Math.random().toString(36).slice(2)}`, mode, ...extra }); }); afterEach(async () => { await db.close(); }); // ---- A12: maxLength / min / max ---- it('maxLength 超长 → VALIDATION_ERROR(A12)', async () => { await db.defineTable('t', { id: { type: 'string', primaryKey: true }, name: { type: 'string', maxLength: 3 }, }); await expectCode(db.query("INSERT INTO t VALUES ('1', 'abcd')"), 'VALIDATION_ERROR'); // 边界值恰好等于上限必须通过 await db.query("INSERT INTO t VALUES ('2', 'abc')"); expect(rowsOf(await db.query('SELECT id FROM t'))).toHaveLength(1); }); it('min / max 越界 → VALIDATION_ERROR(A12)', async () => { await db.defineTable('t', { id: { type: 'string', primaryKey: true }, n: { type: 'number', min: 1, max: 10 }, }); await expectCode(db.query("INSERT INTO t VALUES ('1', 0)"), 'VALIDATION_ERROR'); await expectCode(db.query("INSERT INTO t VALUES ('2', 11)"), 'VALIDATION_ERROR'); await db.query("INSERT INTO t VALUES ('3', 1), ('4', 10)"); expect(rowsOf(await db.query('SELECT id FROM t'))).toHaveLength(2); }); it('UPDATE 同样受 maxLength / min / max 约束(A12)', async () => { await db.defineTable('t', { id: { type: 'string', primaryKey: true }, name: { type: 'string', maxLength: 3 }, n: { type: 'number', min: 0 }, }); await db.query("INSERT INTO t VALUES ('1', 'abc', 5)"); await expectCode(db.query("UPDATE t SET name = 'abcd' WHERE id = '1'"), 'VALIDATION_ERROR'); await expectCode(db.query('UPDATE t SET n = -1 WHERE id = \'1\''), 'VALIDATION_ERROR'); // 失败的更新不得留下任何部分修改 const after = rowsOf<{ name: string; n: number }>(await db.query('SELECT name, n FROM t')); expect(after).toEqual([{ name: 'abc', n: 5 }]); }); // ---- A17: 未知列 ---- it('INSERT 未知列 → COLUMN_NOT_FOUND(A17)', async () => { await db.defineTable('t', { id: { type: 'string', primaryKey: true }, n: { type: 'number' }, }); await expectCode(db.query("INSERT INTO t (id, nope) VALUES ('1', 2)"), 'COLUMN_NOT_FOUND'); // 关键:不能"部分插入"——报错后表必须仍是空的 expect(rowsOf(await db.query('SELECT id FROM t'))).toHaveLength(0); }); it('INSERT 未知列的错误消息列出已知列(便于自查)', async () => { await db.defineTable('t', { id: { type: 'string', primaryKey: true }, n: { type: 'number' }, }); await expect(db.query("INSERT INTO t (id, nope) VALUES ('1', 2)")).rejects.toThrow( /Unknown column "nope" in table "t"\. Known columns: id, n/, ); }); it('TABLE API 直通路径同样拒绝未知列(A17 的真实形态)', async () => { await db.defineTable('t', { id: { type: 'string', primaryKey: true }, n: { type: 'number' }, }); // db.table().insert() 不经 Executor —— 校验必须在引擎边界生效, // 否则这条路径仍然静默丢列 await expectCode( db.table('t').insert({ id: '1', nope: 2 } as never), 'COLUMN_NOT_FOUND', ); expect(rowsOf(await db.query('SELECT id FROM t'))).toHaveLength(0); }); it('UPDATE 未知列 → COLUMN_NOT_FOUND(原有行为,回归护栏)', async () => { await db.defineTable('t', { id: { type: 'string', primaryKey: true }, n: { type: 'number' }, }); await db.query("INSERT INTO t VALUES ('1', 2)"); await expectCode(db.query('UPDATE t SET nope = 3 WHERE id = \'1\''), 'COLUMN_NOT_FOUND'); }); // ---- 批量原子性 ---- it('多行 INSERT 中任一行违反约束 → 整条语句不写入', async () => { await db.defineTable('t', { id: { type: 'string', primaryKey: true }, name: { type: 'string', maxLength: 3 }, }); await expectCode( db.query("INSERT INTO t VALUES ('1', 'ok'), ('2', 'toolong')"), 'VALIDATION_ERROR', ); // 第 1 行合法,但语句级原子性要求整批拒绝 expect(rowsOf(await db.query('SELECT id FROM t'))).toHaveLength(0); }); }); }); describe('[v0.8.0] B-1 校验器单元契约(compileValidator)', () => { const schema = createSchema('t', { id: { type: 'string', primaryKey: true }, name: { type: 'string', maxLength: 3 }, n: { type: 'number', min: 0, max: 10 }, tag: { type: 'string', required: true }, flag: { type: 'boolean', default: false }, }); it('validateRow 填充 default,未提供且无 default 的列补 NULL', () => { const validator = compileValidator(schema); const row = validator.validateRow({ id: '1', tag: 'a' }); // v0.8.0:未提供且无 default 的列**显式补 null**。此前该键被整个删掉, // 于是 `SELECT id, tag, name FROM t` 对刚插入的行抛 COLUMN_NOT_FOUND: name, // 而 `SELECT * FROM t` 又能返回 —— 同一行"有没有 name 列"两种结论。 expect(row).toEqual({ id: '1', tag: 'a', flag: false, name: null, n: null }); // 行必须包含**全部** schema 列(投影/排序/三值比较的统一前提) expect(Object.keys(row).sort()).toEqual(['flag', 'id', 'n', 'name', 'tag']); }); it('validateRow 拒绝未知列并一次列出全部', () => { const validator = compileValidator(schema); expect(() => validator.validateRow({ id: '1', tag: 'a', x: 1, y: 2 })).toThrow( /Unknown columns "x", "y"/, ); }); it('validateRow 的 knownColumns 收窄允许集合(INSERT 显式列清单)', () => { const validator = compileValidator(schema); // 只允许 id/tag 出现 → name 虽在 schema 里也算未知列 expect(() => validator.validateRow({ id: '1', tag: 'a', name: 'x' }, ['id', 'tag'])).toThrow( /Unknown column "name"/, ); expect(validator.validateRow({ id: '1', tag: 'a' }, ['id', 'tag'])).toEqual({ id: '1', tag: 'a', flag: false, name: null, n: null, }); }); it('validatePartial 只校验出现的列(undefined 不触发 required)', () => { const validator = compileValidator(schema); const { values, unknown } = validator.validatePartial({ n: 5, ghost: 1 }); expect(values).toEqual({ n: 5 }); expect(unknown).toEqual(['ghost']); // required 列未出现 → 不报错(UPDATE 语义) expect(validator.validatePartial({ name: 'ab' }).values).toEqual({ name: 'ab' }); // 但出现且越界 → 报错 expect(() => validator.validatePartial({ name: 'abcd' })).toThrow('exceeds max length'); }); it('validatePartial 拒绝 __proto__ 列名(原型污染防护)', () => { const validator = compileValidator(schema); expect(() => validator.validatePartial(JSON.parse('{"__proto__": {"polluted": 1}}'))).toThrow( /__proto__/, ); }); it('null 通过 required 之外的约束(NULL 不是类型错误)', () => { const validator = compileValidator(schema); expect(validator.validateRow({ id: '1', tag: 'a', name: null, n: null })).toEqual({ id: '1', tag: 'a', name: null, n: null, flag: false, }); }); it('primaryKey 隐含 NOT NULL', () => { const validator = compileValidator(schema); expect(() => validator.validateRow({ id: null, tag: 'a' })).toThrow(/Primary key column "id"/); expect(() => validator.validateRow({ tag: 'a' })).toThrow(/Primary key column "id"/); }); it('required 与 primaryKey 各自给出稳定错误消息', () => { const validator = compileValidator(schema); expect(() => validator.validateRow({ id: '1', tag: null })).toThrow('Column "tag" is required in table "t"'); }); it('类型错误使用 TYPE_ERROR 码(可被调用方分类处理)', () => { const validator = compileValidator(schema); try { validator.validateRow({ id: 1, tag: 'a' }); throw new Error('should have thrown'); } catch (error) { expect((error as { code?: string }).code).toBe('TYPE_ERROR'); } }); });