Files
MetonaSqlark/tests/v080-unified-validation.test.ts
T
thzxx 50b1864145 chore(docs): 删除 v0.7.4 审计与 v0.7.5 计划共 4 个 md 文档
删除:
- AUDIT-aria-lsm-v0.7.4.md(50KB)
- AUDIT-query-layer-v0.7.4.md(39KB)
- AUDIT-storage-engines-v0.7.4.md(39KB)
- PLAN-v0.7.5.md(98KB,含附录 G/H/I)

删除前先清除引用面,避免留下断链(共 18 处):
- 源码注释 7 处(change-notifier / kvstore index / column-value / expression /
  sql-compare / where-matcher / validation):保留设计意图,引用改为"v0.8.0 审计根因 N"
- 测试注释 9 处(opfs.spec / aria-opfs-backend / faulty-backend / storage-harness /
  v080-b6 / v080-kvstore / v080-query-layer / v080-sql-three-valued /
  v080-unified-validation / parser):同上
- CHANGELOG 3 处:改为不依赖已删除文档的自洽表述(B-6 交付物见各条;门禁订正三处
  按内容重写),并把变异数量同步为 42
- 校验:三个 md 之间无断链;仓库内已无 PLAN-v0.7.5/AUDIT-* 的任何引用
  (git 历史仍可追溯,需要时可 `git show <commit>:PLAN-v0.7.5.md` 找回)

验证:93 套件 / 1985 用例全绿;覆盖率 90.59 / 82.61 / 94.14 / 93.50(阈值 90/82/94/93);
e2e 14/14;lint + 两份 tsc 干净;dist 已重建(注释只影响非压缩产物,min 产物
251,731 B / gzip 63,431 B 不变)。

说明:审查记录的核心内容仍在 CHANGELOG.md("全量回归审查"与"现场失败修复"两节),
随 PLAN 一起删除的是附录 G/H/I 的详细表格(门禁逐条验收、交付物清单、未修复项表)。
2026-09-15 17:17:24 +08:00

236 lines
9.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* v0.8.0 回归套件 —— B-1 统一行校验(工作流 B 第 1 项)
* ============================================================================
* 背景(v0.8.0 审计根因 1 + 缺陷 A12/A17):
* 修复前存在**三份**行校验实现,覆盖面各不相同 —— Memory/KVStore/Hybrid 共用
* 一份"只查类型"的实现(**没有** maxLength/min/max),Aria 走 checkFieldType
* (有约束),schema.ts 是第三份。于是:
*
* A12: `CREATE TABLE t (name STRING(3))` + `INSERT ... 'abcdef'`
* → Aria 抛 VALIDATION_ERRORmemory/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<DatabaseConfig>]> = [
['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_ERRORA12', 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_ERRORA12', 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_FOUNDA17', 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');
}
});
});