Files
MetonaSqlark/tests/v073-fixes.test.ts
T
thzxx 2a109ef933 feat(B-1): 统一行校验 choke point —— 消除三份分叉的校验实现(A12/A17)
背景(PLAN-v0.7.5.md 根因 1):
修复前有**三份**行校验实现,覆盖面各不相同:

  位置                                        类型 required PK非空 maxLength min/max 未知列
  engine/memory.ts(disk/hybrid 共用)          ✓     ✓       ✓      ✗        ✗     静默丢弃
  engine/aria/index.ts → checkFieldType         ✓     ✓       ✓      ✓        ✓     静默丢弃
  table/schema.ts                               ✓     ✓       ✓      ✓        ✓     静默丢弃

后果一(A12):同一份 schema、同一条 INSERT 是否报约束错误取决于引擎选择 ——
`CREATE TABLE t (name STRING(3))` + 插入 'abcdef' 在 Aria 抛错,在
memory/disk/hybrid 静默写入超长值。

后果二(A17):四个引擎对未知列一律静默丢弃。`INSERT INTO t (id, nope) VALUES
('1',2)` 报成功,随后 `SELECT nope` 报 COLUMN_NOT_FOUND —— 同一列名在写路径与
读路径得到**相反结论**。TABLE API 直通路径尤其明显(executor 按 schema 列序
构造行,nope 那个位置根本没有值,所以连"校验 stmt.columns"都拦不住)。

根治方式:
1. 新增 src/table/validation.ts —— 唯一校验定义 `compileValidator(schema)`,
   约束覆盖面取三者并集,并把**规范化**(default 填充、undefined 跳过、
   __proto__ 防污染)与校验放在同一处。
   三种载荷形态刻意分成三个显式入口,不合成带 options 的函数:
     - validateRow(row, knownColumns?)  INSERT 语义(default 生效、缺列合法)
     - validatePartial(row)             UPDATE 语义(只校验出现的列)
     - assertNoUnknownColumns           独立可复用的列名存在性检查
   混成一个函数会让"required 是否生效"取决于调用方参数,重新引入跨路径差异。
2. MemoryEngine / AriaEngine 的私有 validateRow 改为委托;schema.ts 的公开
   validateRow 同样委托(API 不变,实现只剩一份)。
3. 四个引擎新增 validatePayload(table, rows, mode)(IStorageEngine 契约),
   Executor 在**任何副作用之前**调用:多行批量整体判定,错误消息一次列出全部
   未知列与已知列清单。
4. executeInsert 显式校验 stmt.columns 全部存在(A17)。
5. UPDATE 的外键级联写入(applyUpdateCascade)从"直接赋值"改为过
   validatePartial —— 此前 CASCADE 把新主键写进引用列时绕过 maxLength/min/max,
   与 A12 属同一类"校验只在部分写入路径生效"。

连带修正(测试夹具本身不忠实,B-1 使其暴露):
  - tests/engine/aria-cache.test.ts 的 makeRows 无条件返回 {id,name,age},
    部分用例的表只有 {id,name} —— 多余列被静默丢弃所以"通过"。新增 rowsFor()
    按 schema 裁剪,让夹具忠实反映表结构(而不是放宽校验)。
  - tests/v073-fixes.test.ts "schema 外列不持久化" 改为断言写路径即拒绝,
    并保留"合法行落盘后不含额外列"的检查。

验证:
  - 新增 tests/v080-unified-validation.test.ts:8 项 × 4 引擎 + 9 项校验器
    单元契约,共 41 断言;
  - 全量 84 套件 / 1499 测试通过;typecheck(src+tests) 与 lint 零错误。
2026-09-14 23:38:58 +08:00

656 lines
31 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.7.3 回归测试 — 深度审计第六阶段修复
*
* 1. 索引列 IS NULL 恒空(Memory/KVStore/Hybrid
* 2. delete RESTRICT 预检前误删索引
* 3. insert 语句级部分提交(PK/unique 批内重复)
* 4. Aria insert 批内 PK 重复部分提交
* 5. queryStream 子查询静默空结果
* 6. ALTER DROP 索引列残留
* 7. CREATE UNIQUE INDEX 存量重复数据
* 8. SELECT * 混别名列投影
* 9. INSERT hooks 列映射
* 10. KVStore insert 持久化 validated 行
*/
import { MetonaSqlark } from '../src/core';
import { MemoryEngine } from '../src/engine/memory';
import { rows as rowsOf } from './helpers/assertions';
describe('v0.7.3: 索引列 IS NULL(三引擎对齐)', () => {
test.each([
['memory', { mode: 'memory' } as const],
['disk', { mode: 'disk', diskEngine: 'memory' } as const],
['hybrid', { mode: 'hybrid', diskEngine: 'memory' } as const],
])('%s: 索引列 IS NULL 返回 null 行', async (_label, cfg) => {
const db = await MetonaSqlark.create({ name: `v073-isnull-${_label}`, ...cfg });
await db.defineTable('users', {
id: { type: 'string', primaryKey: true },
email: { type: 'string', index: true },
});
await db.query("INSERT INTO users VALUES ('1', NULL), ('2', 'a@b.c')");
const rows = rowsOf<{ id: string }>(await db.query('SELECT * FROM users WHERE email IS NULL'));
expect(rows).toHaveLength(1);
expect(rows[0].id).toBe('1');
await db.close();
});
test.each([
['memory', { mode: 'memory' } as const],
['disk', { mode: 'disk', diskEngine: 'memory' } as const],
])('%s: 唯一索引列 $isNullQuery Builder)不走索引短路', async (_label, cfg) => {
const db = await MetonaSqlark.create({ name: `v073-isnull-qb-${_label}`, ...cfg });
await db.defineTable('users', {
id: { type: 'string', primaryKey: true },
email: { type: 'string', unique: true },
});
await db.query("INSERT INTO users VALUES ('1', NULL), ('2', 'a@b.c')");
// v0.8.0A15):此处原为 `{ $eq: null }` 且期望命中 NULL 行 —— 那是错误的
// SQL 语义。测试的真实目标是"唯一索引上的 NULL 行不被索引短路漏掉",
// 用正确谓词表达后目标不变(见下一条对 `$eq: null` 的语义护栏)。
const rows = await db.table('users').select(['id']).where({ email: { $isNull: true } }).execute();
expect(rows).toHaveLength(1);
await db.close();
});
test.each([
['memory', { mode: 'memory' } as const],
['disk', { mode: 'disk', diskEngine: 'memory' } as const],
])('%s: `$eq: null` 恒 UNKNOWN —— 不再命中 NULL 行', async (_label, cfg) => {
const db = await MetonaSqlark.create({ name: `v073-eqnull-qb-${_label}`, ...cfg });
await db.defineTable('users', {
id: { type: 'string', primaryKey: true },
email: { type: 'string', unique: true },
});
await db.query("INSERT INTO users VALUES ('1', NULL), ('2', 'a@b.c')");
const rows = await db.table('users').select(['id']).where({ email: { $eq: null } }).execute();
expect(rows).toHaveLength(0);
await db.close();
});
test('aria: IS NULL 回归护栏(v0.6.2 已修)', async () => {
const db = await MetonaSqlark.create({ name: 'v073-isnull-aria', mode: 'aria', diskEngine: 'memory' });
await db.defineTable('users', {
id: { type: 'string', primaryKey: true },
email: { type: 'string', index: true },
});
await db.query("INSERT INTO users VALUES ('1', NULL), ('2', 'a@b.c')");
const rows = await db.query('SELECT * FROM users WHERE email IS NULL');
expect(rows).toHaveLength(1);
await db.close();
});
});
describe('v0.7.3: delete RESTRICT 预检不破坏索引', () => {
test('memory: RESTRICT 抛错后唯一约束与索引查询保持有效', async () => {
const db = await MetonaSqlark.create({ name: 'v073-del-restrict', mode: 'memory' });
await db.defineTable('users', {
id: { type: 'string', primaryKey: true },
email: { type: 'string', unique: true, index: true },
});
await db.defineTable('orders', {
id: { type: 'string', primaryKey: true },
user_id: { type: 'string', references: 'users.id', onDelete: 'RESTRICT' },
});
await db.query("INSERT INTO users VALUES ('u1', 'x@x.x')");
await db.query("INSERT INTO orders VALUES ('o1', 'u1')");
let threw = false;
try { await db.query("DELETE FROM users WHERE id = 'u1'"); } catch { threw = true; }
expect(threw).toBe(true);
// 行仍在
expect(await db.query('SELECT * FROM users')).toHaveLength(1);
// 唯一约束仍有效
let uniqueThrew = false;
try { await db.query("INSERT INTO users VALUES ('u2', 'x@x.x')"); } catch { uniqueThrew = true; }
expect(uniqueThrew).toBe(true);
// 索引查询仍能命中
const viaIndex = await db.query("SELECT * FROM users WHERE email = 'x@x.x'");
expect(viaIndex).toHaveLength(1);
await db.close();
});
test('memory: 多行匹配删除 RESTRICT 失败 → 全部行索引完好', async () => {
const db = await MetonaSqlark.create({ name: 'v073-del-restrict-multi', mode: 'memory' });
await db.defineTable('users', {
id: { type: 'string', primaryKey: true },
tag: { type: 'string', index: true },
});
await db.defineTable('orders', {
id: { type: 'string', primaryKey: true },
user_id: { type: 'string', references: 'users.id', onDelete: 'RESTRICT' },
});
await db.query("INSERT INTO users VALUES ('u1', 't1'), ('u2', 't2')");
await db.query("INSERT INTO orders VALUES ('o1', 'u1')");
let threw = false;
try { await db.query('DELETE FROM users'); } catch { threw = true; }
expect(threw).toBe(true);
expect(await db.query('SELECT * FROM users')).toHaveLength(2);
expect(await db.query("SELECT * FROM users WHERE tag = 't1'")).toHaveLength(1);
expect(await db.query("SELECT * FROM users WHERE tag = 't2'")).toHaveLength(1);
await db.close();
});
test('disk: RESTRICT 抛错后索引保持(与 memory 同路径)', async () => {
const db = await MetonaSqlark.create({ name: 'v073-del-restrict-disk', mode: 'disk', diskEngine: 'memory' });
await db.defineTable('users', {
id: { type: 'string', primaryKey: true },
email: { type: 'string', unique: true, index: true },
});
await db.defineTable('orders', {
id: { type: 'string', primaryKey: true },
user_id: { type: 'string', references: 'users.id', onDelete: 'RESTRICT' },
});
await db.query("INSERT INTO users VALUES ('u1', 'x@x.x')");
await db.query("INSERT INTO orders VALUES ('o1', 'u1')");
let threw = false;
try { await db.query("DELETE FROM users WHERE id = 'u1'"); } catch { threw = true; }
expect(threw).toBe(true);
const viaIndex = await db.query("SELECT * FROM users WHERE email = 'x@x.x'");
expect(viaIndex).toHaveLength(1);
await db.close();
});
});
describe('v0.7.3: insert 语句级原子性', () => {
test.each([
['memory', { mode: 'memory' } as const],
['disk', { mode: 'disk', diskEngine: 'memory' } as const],
['hybrid', { mode: 'hybrid', diskEngine: 'memory' } as const],
])('%s: 批内主键重复 → 整句不执行', async (_label, cfg) => {
const db = await MetonaSqlark.create({ name: `v073-ins-atomic-pk-${_label}`, ...cfg });
await db.defineTable('users', { id: { type: 'string', primaryKey: true } });
let threw = false;
try { await db.query("INSERT INTO users VALUES ('a'), ('a')"); } catch { threw = true; }
expect(threw).toBe(true);
expect(await db.query('SELECT * FROM users')).toHaveLength(0);
await db.close();
});
test.each([
['memory', { mode: 'memory' } as const],
['disk', { mode: 'disk', diskEngine: 'memory' } as const],
['hybrid', { mode: 'hybrid', diskEngine: 'memory' } as const],
])('%s: 批内唯一冲突 → 整句不执行', async (_label, cfg) => {
const db = await MetonaSqlark.create({ name: `v073-ins-atomic-uq-${_label}`, ...cfg });
await db.defineTable('users', {
id: { type: 'string', primaryKey: true },
email: { type: 'string', unique: true },
});
let threw = false;
try {
await db.query("INSERT INTO users VALUES ('1', 'a@b.c'), ('2', 'a@b.c')");
} catch { threw = true; }
expect(threw).toBe(true);
expect(await db.query('SELECT * FROM users')).toHaveLength(0);
await db.close();
});
test.each([
['memory', { mode: 'memory' } as const],
['disk', { mode: 'disk', diskEngine: 'memory' } as const],
])('%s: 第 N 行撞已有主键 → 整句不执行(含前 N-1 行)', async (_label, cfg) => {
const db = await MetonaSqlark.create({ name: `v073-ins-atomic-exist-${_label}`, ...cfg });
await db.defineTable('users', { id: { type: 'string', primaryKey: true } });
await db.query("INSERT INTO users VALUES ('a')");
let threw = false;
try { await db.query("INSERT INTO users VALUES ('b'), ('a')"); } catch { threw = true; }
expect(threw).toBe(true);
const rows = rowsOf<{ id: string }>(await db.query('SELECT * FROM users'));
expect(rows).toHaveLength(1);
expect(rows[0].id).toBe('a');
await db.close();
});
test('aria: 批内主键重复 → 整句不执行(此前部分提交 + WAL 不一致)', async () => {
const db = await MetonaSqlark.create({ name: 'v073-ins-atomic-aria', mode: 'aria', diskEngine: 'memory' });
await db.defineTable('users', { id: { type: 'string', primaryKey: true } });
let threw = false;
try { await db.query("INSERT INTO users VALUES ('a'), ('a')"); } catch { threw = true; }
expect(threw).toBe(true);
expect(await db.query('SELECT * FROM users')).toHaveLength(0);
await db.close();
});
test('aria: 事务内批内主键重复 → 整句不执行且快照干净', async () => {
const db = await MetonaSqlark.create({ name: 'v073-ins-atomic-aria-tx', mode: 'aria', diskEngine: 'memory' });
await db.defineTable('users', { id: { type: 'string', primaryKey: true } });
await db.query('BEGIN');
let threw = false;
try { await db.query("INSERT INTO users VALUES ('a'), ('a')"); } catch { threw = true; }
expect(threw).toBe(true);
await db.query('COMMIT');
expect(await db.query('SELECT * FROM users')).toHaveLength(0);
await db.close();
});
test('aria: 批内唯一冲突整批不落库回归护栏(v0.6.2)', async () => {
const db = await MetonaSqlark.create({ name: 'v073-ins-atomic-aria-uq', mode: 'aria', diskEngine: 'memory' });
await db.defineTable('users', {
id: { type: 'string', primaryKey: true },
email: { type: 'string', unique: true },
});
let threw = false;
try {
await db.query("INSERT INTO users VALUES ('1', 'a@b.c'), ('2', 'a@b.c')");
} catch { threw = true; }
expect(threw).toBe(true);
expect(await db.query('SELECT * FROM users')).toHaveLength(0);
await db.close();
});
});
describe('v0.7.3: queryStream 子查询回退物化', () => {
test.each([
['memory', { mode: 'memory' } as const],
['aria', { mode: 'aria', diskEngine: 'memory' } as const],
])('%s: IN 子查询流式查询返回正确结果(回退物化)', async (_label, cfg) => {
const db = await MetonaSqlark.create({ name: `v073-stream-sub-${_label}`, ...cfg });
await db.defineTable('users', { id: { type: 'string', primaryKey: true } });
await db.defineTable('orders', { id: { type: 'string', primaryKey: true }, user_id: { type: 'string' } });
await db.query("INSERT INTO users VALUES ('1'), ('2')");
await db.query("INSERT INTO orders VALUES ('o1', '1')");
const collected: Record<string, unknown>[] = [];
const n = await db.queryStream('SELECT * FROM users WHERE id IN (SELECT user_id FROM orders)', (r) => collected.push(r));
expect(n).toBe(1);
expect(collected).toHaveLength(1);
expect(collected[0].id).toBe('1');
await db.close();
});
test('memory: EXISTS 关联子查询流式查询回退物化', async () => {
const db = await MetonaSqlark.create({ name: 'v073-stream-exists', mode: 'memory' });
await db.defineTable('users', { id: { type: 'string', primaryKey: true } });
await db.defineTable('orders', { id: { type: 'string', primaryKey: true }, user_id: { type: 'string' } });
await db.query("INSERT INTO users VALUES ('1'), ('2')");
await db.query("INSERT INTO orders VALUES ('o1', '1')");
const collected: Record<string, unknown>[] = [];
await db.queryStream(
'SELECT * FROM users u WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id)',
(r) => collected.push(r),
);
expect(collected).toHaveLength(1);
await db.close();
});
test('memory: 简单查询仍走引擎流式路径(未误回退)', async () => {
const db = await MetonaSqlark.create({ name: 'v073-stream-simple', mode: 'memory' });
await db.defineTable('users', { id: { type: 'string', primaryKey: true } });
await db.query("INSERT INTO users VALUES ('1'), ('2')");
const collected: Record<string, unknown>[] = [];
const n = await db.queryStream("SELECT * FROM users WHERE id = '1'", (r) => collected.push(r));
expect(n).toBe(1);
expect(collected).toHaveLength(1);
await db.close();
});
test('memory: WHERE 列引用($col)流式查询回退物化且不抛错', async () => {
const db = await MetonaSqlark.create({ name: 'v073-stream-colref', mode: 'memory' });
await db.defineTable('t1', { id: { type: 'string', primaryKey: true }, x: { type: 'number' }, y: { type: 'number' } });
await db.query("INSERT INTO t1 VALUES ('1', 5, 5), ('2', 10, 3)");
// t1.x = t1.y 解析为 $col 列引用 —— 引擎层 matchWhere 无 $col 匹配分支,
// 此前流式路径会抛 QUERY_ERROR/静默过滤;v0.7.3 回退物化,结果与 query() 一致
const viaQuery = await db.query('SELECT * FROM t1 WHERE t1.x = t1.y');
const collected: Record<string, unknown>[] = [];
await db.queryStream('SELECT * FROM t1 WHERE t1.x = t1.y', (r) => collected.push(r));
expect(collected).toHaveLength((viaQuery as Record<string, unknown>[]).length);
await db.close();
});
});
describe('v0.7.3: ALTER DROP 索引列清理', () => {
test.each([
['memory', { mode: 'memory' } as const],
['disk', { mode: 'disk', diskEngine: 'memory' } as const],
])('%s: DROP 索引列后无旧索引短路(新增行可见)', async (_label, cfg) => {
const db = await MetonaSqlark.create({ name: `v073-alter-drop-idx-${_label}`, ...cfg });
await db.defineTable('users', {
id: { type: 'string', primaryKey: true },
email: { type: 'string', index: true },
});
await db.query("INSERT INTO users VALUES ('1', 'a@b.c')");
await db.query('ALTER TABLE users DROP COLUMN email');
await db.query("INSERT INTO users VALUES ('2')");
expect(await db.query('SELECT * FROM users')).toHaveLength(2);
// 已删列不再存在于 schema;查询该列应报错而非走旧索引(executor 层不会到达)
const rows = await db.query('SELECT * FROM users WHERE id = \'2\'');
expect(rows).toHaveLength(1);
await db.close();
});
test('memory: DROP 非索引列不影响其他列索引', async () => {
const db = await MetonaSqlark.create({ name: 'v073-alter-drop-plain', mode: 'memory' });
await db.defineTable('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string' },
email: { type: 'string', index: true },
});
await db.query("INSERT INTO users VALUES ('1', 'Alice', 'a@b.c')");
await db.query('ALTER TABLE users DROP COLUMN name');
const viaIndex = await db.query("SELECT * FROM users WHERE email = 'a@b.c'");
expect(viaIndex).toHaveLength(1);
await db.close();
});
});
describe('v0.7.3: CREATE UNIQUE INDEX 存量唯一性', () => {
test.each([
['memory', { mode: 'memory' } as const],
['aria', { mode: 'aria', diskEngine: 'memory' } as const],
])('%s: 存量重复数据 → 抛 UNIQUE_VIOLATION 且无半初始化索引', async (_label, cfg) => {
const db = await MetonaSqlark.create({ name: `v073-uqidx-dup-${_label}`, ...cfg });
await db.defineTable('users', { id: { type: 'string', primaryKey: true }, email: { type: 'string' } });
await db.query("INSERT INTO users VALUES ('1', 'a@b.c'), ('2', 'a@b.c')");
let threw = false;
let code = '';
try { await db.query('CREATE UNIQUE INDEX idx_e ON users (email)'); } catch (e) {
threw = true;
code = (e as { code?: string }).code ?? '';
}
expect(threw).toBe(true);
expect(code).toBe('UNIQUE_VIOLATION');
// 失败后列标志未落:普通 CREATE INDEX 仍可建立
await db.query('CREATE INDEX idx_e ON users (email)');
const viaIndex = await db.query("SELECT * FROM users WHERE email = 'a@b.c'");
expect(viaIndex).toHaveLength(2);
await db.close();
});
test.each([
['memory', { mode: 'memory' } as const],
['aria', { mode: 'aria', diskEngine: 'memory' } as const],
])('%s: 存量数据唯一 → 建索引成功且唯一约束生效', async (_label, cfg) => {
const db = await MetonaSqlark.create({ name: `v073-uqidx-ok-${_label}`, ...cfg });
await db.defineTable('users', { id: { type: 'string', primaryKey: true }, email: { type: 'string' } });
await db.query("INSERT INTO users VALUES ('1', 'a@b.c'), ('2', 'b@b.c')");
await db.query('CREATE UNIQUE INDEX idx_e ON users (email)');
let threw = false;
try { await db.query("INSERT INTO users VALUES ('3', 'a@b.c')"); } catch { threw = true; }
expect(threw).toBe(true);
expect(await db.query('SELECT * FROM users')).toHaveLength(2);
await db.close();
});
});
describe('v0.7.3: SELECT * 混别名列投影', () => {
test('memory: SELECT *, name AS nick 保留全部列 + 别名列', async () => {
const db = await MetonaSqlark.create({ name: 'v073-star-alias', mode: 'memory' });
await db.defineTable('users', { id: { type: 'string', primaryKey: true }, name: { type: 'string' }, age: { type: 'number' } });
await db.query("INSERT INTO users VALUES ('1', 'Alice', 30)");
const rows = rowsOf<Record<string, unknown>>(await db.query('SELECT *, name AS nick FROM users'));
expect(rows).toHaveLength(1);
const row = rows[0];
expect(row.id).toBe('1');
expect(row.name).toBe('Alice');
expect(row.age).toBe(30);
expect(row.nick).toBe('Alice');
await db.close();
});
test('memory: 纯 SELECT * 行为不变(原行引用键集合)', async () => {
const db = await MetonaSqlark.create({ name: 'v073-star-only', mode: 'memory' });
await db.defineTable('users', { id: { type: 'string', primaryKey: true }, name: { type: 'string' } });
await db.query("INSERT INTO users VALUES ('1', 'Alice')");
const rows = rowsOf<Record<string, unknown>>(await db.query('SELECT * FROM users'));
expect(Object.keys(rows[0]).sort()).toEqual(['id', 'name']);
await db.close();
});
test('memory: SELECT *, 常量列混合', async () => {
const db = await MetonaSqlark.create({ name: 'v073-star-const', mode: 'memory' });
await db.defineTable('users', { id: { type: 'string', primaryKey: true } });
await db.query("INSERT INTO users VALUES ('1')");
const rows = rowsOf<Record<string, unknown>>(await db.query("SELECT *, 'lit' AS c FROM users"));
const row = rows[0];
expect(row.id).toBe('1');
expect(row.c).toBe('lit');
await db.close();
});
});
describe('v0.7.3: INSERT hooks 列映射', () => {
test('SQL 省略列名时 beforeInsert 收到 schema 列名映射', async () => {
const db = await MetonaSqlark.create({ name: 'v073-hooks-insert', mode: 'memory' });
await db.defineTable('users', { id: { type: 'string', primaryKey: true }, name: { type: 'string' } });
let seenRows: unknown = null;
db.on('beforeInsert', async (rows: unknown) => { seenRows = rows; });
await db.query("INSERT INTO users VALUES ('1', 'Alice')");
const rows = seenRows as Record<string, unknown>[];
expect(rows).toHaveLength(1);
expect(rows[0].id).toBe('1');
expect(rows[0].name).toBe('Alice');
await db.close();
});
test('SQL 显式列名时 hooks 行键按显式列名', async () => {
const db = await MetonaSqlark.create({ name: 'v073-hooks-insert-cols', mode: 'memory' });
await db.defineTable('users', { id: { type: 'string', primaryKey: true }, name: { type: 'string' } });
let seenRows: unknown = null;
db.on('beforeInsert', async (rows: unknown) => { seenRows = rows; });
await db.query("INSERT INTO users (name, id) VALUES ('Alice', '1')");
const rows = seenRows as Record<string, unknown>[];
expect(rows[0].id).toBe('1');
expect(rows[0].name).toBe('Alice');
await db.close();
});
});
describe('v0.7.3: KVStore insert 持久化 validated 行', () => {
test('default 值与列投影落盘(跨实例恢复一致)', async () => {
const db = await MetonaSqlark.create({ name: 'v073-kv-validated', mode: 'disk', diskEngine: 'memory' });
await db.defineTable('users', { id: { type: 'string', primaryKey: true }, age: { type: 'number', default: 18 } });
await db.query("INSERT INTO users VALUES ('1')");
await db.close();
const db2 = await MetonaSqlark.create({ name: 'v073-kv-validated', mode: 'disk', diskEngine: 'memory' });
const rows = rowsOf<Record<string, unknown>>(await db2.query('SELECT * FROM users'));
expect(rows).toHaveLength(1);
expect(rows[0]).toEqual({ id: '1', age: 18 });
await db2.close();
});
test('schema 外列在写路径即被拒绝(v0.8.0 B-1 由静默丢弃改为报错)', async () => {
const db = await MetonaSqlark.create({ name: 'v073-kv-extra-col', mode: 'disk', diskEngine: 'memory' });
await db.defineTable('users', { id: { type: 'string', primaryKey: true } });
// v0.8.0B-1 / 缺陷 A17):此断言此前写作"junk 被静默丢弃、插入成功"。
// 静默丢弃是错的:INSERT 报成功、`SELECT junk` 又报 COLUMN_NOT_FOUND ——
// 同一列名在写/读路径得到相反结论,用户以为写进去了。
// 现在写路径直接抛 COLUMN_NOT_FOUND(与读路径同一错误码),
// 因此"不持久化"这一目的以更强的形式成立(连内存表都不会有脏列)。
await expect(db.table('users').insert({ id: '1', junk: 'x' } as never)).rejects.toMatchObject({
code: 'COLUMN_NOT_FOUND',
});
// 合法的行仍可写入,且落盘后不含任何额外列
await db.table('users').insert({ id: '1' } as never);
await db.close();
const db2 = await MetonaSqlark.create({ name: 'v073-kv-extra-col', mode: 'disk', diskEngine: 'memory' });
const rows = rowsOf<Record<string, unknown>>(await db2.query('SELECT * FROM users'));
expect(Object.keys(rows[0]).sort()).toEqual(['id']);
await db2.close();
});
test('MemoryEngine.getRow 暴露 validated 行', async () => {
const engine = new MemoryEngine();
await engine.open('v073-getrow', 1);
await engine.createTable({ name: 'users', columns: { id: { type: 'string', primaryKey: true }, age: { type: 'number', default: 18 } } });
await engine.insert('users', [{ id: '1' }]);
const row = engine.getRow('users', '1');
expect(row).toEqual({ id: '1', age: 18 });
expect(engine.getRow('users', 'nope')).toBeNull();
expect(engine.getRow('missing', '1')).toBeNull();
await engine.close();
});
});
describe('v0.7.3: WAL BEGIN/ROLLBACK 写失败窗口', () => {
async function createAria(name: string): Promise<{ db: MetonaSqlark; walStore: { append: (d: Uint8Array) => Promise<void> } }> {
const db = await MetonaSqlark.create({ name, mode: 'aria', diskEngine: 'memory' });
await db.defineTable('users', { id: { type: 'string', primaryKey: true } });
const engine = db.getEngine() as unknown as { wal: { store: { append: (d: Uint8Array) => Promise<void> } } };
return { db, walStore: engine.wal.store };
}
test('BEGIN 记录写失败 → 事务状态不泄漏(可重试)', async () => {
const { db, walStore } = await createAria('v073-wal-begin-fail');
const orig = walStore.append.bind(walStore);
walStore.append = async () => { throw new Error('wal boom'); };
await expect(db.query('BEGIN')).rejects.toThrow();
walStore.append = orig;
// 事务状态未泄漏:可正常开始并提交新事务
await db.query('BEGIN');
await db.query("INSERT INTO users VALUES ('1')");
await db.query('COMMIT');
expect(await db.query('SELECT * FROM users')).toHaveLength(1);
await db.close();
});
test('ROLLBACK 记录写失败 → 事务仍活跃(可重试回滚,不复活数据)', async () => {
const { db, walStore } = await createAria('v073-wal-rollback-fail');
await db.query('BEGIN');
await db.query("INSERT INTO users VALUES ('1')");
const orig = walStore.append.bind(walStore);
walStore.append = async () => { throw new Error('wal boom'); };
// ROLLBACK WAL 记录先写失败 → 内存未回滚、事务仍活跃
await expect(db.query('ROLLBACK')).rejects.toThrow();
walStore.append = orig;
// 重试回滚成功,数据未提交
await db.query('ROLLBACK');
expect(await db.query('SELECT * FROM users')).toHaveLength(0);
await db.close();
});
});
describe('v0.7.3: aria $in 批级预加载', () => {
test('多值 IN(含重复值/未命中值)索引查询正确', async () => {
const db = await MetonaSqlark.create({ name: 'v073-in-batch', mode: 'aria', diskEngine: 'memory' });
await db.defineTable('users', {
id: { type: 'string', primaryKey: true },
tag: { type: 'string', index: true },
});
for (let i = 0; i < 50; i++) {
await db.query('INSERT INTO users VALUES (?, ?)', [String(i), `t${i % 5}`]);
}
// flush 使索引/主数据 SSTable 化(预加载路径真实生效)
const engine = db.getEngine() as unknown as { lsm: { flush(): Promise<void> }; secondaryIndexes: Map<string, { flush(): Promise<void> }> };
await engine.lsm.flush();
for (const idxLsm of engine.secondaryIndexes.values()) await idxLsm.flush();
const rows = await db.query("SELECT * FROM users WHERE tag IN ('t1', 't2', 't1', 't9')");
expect(rows).toHaveLength(20);
await db.close();
});
test('IN 与 $and 组合条件结果正确(索引子集 + 全条件过滤)', async () => {
const db = await MetonaSqlark.create({ name: 'v073-in-and', mode: 'aria', diskEngine: 'memory' });
await db.defineTable('users', {
id: { type: 'string', primaryKey: true },
tag: { type: 'string', index: true },
age: { type: 'number' },
});
for (let i = 0; i < 20; i++) {
await db.query('INSERT INTO users VALUES (?, ?, ?)', [String(i), `t${i % 4}`, i]);
}
const rows = await db.query("SELECT * FROM users WHERE tag IN ('t1', 't2') AND age >= 10");
expect(rows).toHaveLength(5);
await db.close();
});
});
describe('v0.7.3: $and 等值条件下推', () => {
test.each([
['memory', { mode: 'memory' } as const],
['aria', { mode: 'aria', diskEngine: 'memory' } as const],
])('%s: 多条件 AND 查询结果正确', async (_label, cfg) => {
const db = await MetonaSqlark.create({ name: `v073-and-push-${_label}`, ...cfg });
await db.defineTable('users', {
id: { type: 'string', primaryKey: true },
tag: { type: 'string', index: true },
age: { type: 'number' },
});
await db.query("INSERT INTO users VALUES ('1', 'a', 10), ('2', 'a', 20), ('3', 'b', 10)");
const rows = rowsOf<{ id: string }>(await db.query("SELECT * FROM users WHERE tag = 'a' AND age = 10"));
expect(rows).toHaveLength(1);
expect(rows[0].id).toBe('1');
await db.close();
});
test.each([
['memory', { mode: 'memory' } as const],
['aria', { mode: 'aria', diskEngine: 'memory' } as const],
])('%s: EXPLAIN 识别 $and 嵌套索引条件', async (_label, cfg) => {
const db = await MetonaSqlark.create({ name: `v073-and-explain-${_label}`, ...cfg });
await db.defineTable('users', {
id: { type: 'string', primaryKey: true },
tag: { type: 'string', index: true },
age: { type: 'number' },
});
await db.query("INSERT INTO users VALUES ('1', 'a', 10)");
// 索引列在 $and 嵌套中 → 递归识别 index:tag
const plan1 = await db.query("EXPLAIN SELECT * FROM users WHERE age > 5 AND tag = 'a'");
expect((plan1 as Record<string, unknown>).usingIndex).toBe('index:tag');
// 主键在 $and 嵌套中 → pk
const plan2 = await db.query("EXPLAIN SELECT * FROM users WHERE age > 5 AND id = '1'");
expect((plan2 as Record<string, unknown>).usingIndex).toBe('pk');
await db.close();
});
});
describe('v0.7.3: SELECT 常量列 SQL 标准转义', () => {
test("SELECT 'O''Brien' 还原为 O'Brien", async () => {
const db = await MetonaSqlark.create({ name: 'v073-escape', mode: 'memory' });
await db.defineTable('users', { id: { type: 'string', primaryKey: true } });
await db.query("INSERT INTO users VALUES ('1')");
const rows = rowsOf<{ name: string }>(await db.query("SELECT 'O''Brien' AS name FROM users"));
expect(rows[0].name).toBe("O'Brien");
await db.close();
});
test("无表查询 SELECT 'a''b' AS x 转义还原", async () => {
const db = await MetonaSqlark.create({ name: 'v073-escape-notable', mode: 'memory' });
const rows = rowsOf<{ x: string }>(await db.query("SELECT 'a''b' AS x"));
expect(rows[0].x).toBe("a'b");
await db.close();
});
test("SELECT *, 'x''y' AS c 混合投影转义", async () => {
const db = await MetonaSqlark.create({ name: 'v073-escape-star', mode: 'memory' });
await db.defineTable('users', { id: { type: 'string', primaryKey: true } });
await db.query("INSERT INTO users VALUES ('1')");
const rows = rowsOf<Record<string, unknown>>(await db.query("SELECT *, 'x''y' AS c FROM users"));
const row = rows[0];
expect(row.id).toBe('1');
expect(row.c).toBe("x'y");
await db.close();
});
});
describe('v0.7.3: ANALYZE 统计二级索引', () => {
test('aria: 统计含二级索引 LSMsstableCount/memtableSize 汇总)', async () => {
const db = await MetonaSqlark.create({ name: 'v073-analyze', mode: 'aria', diskEngine: 'memory' });
await db.defineTable('users', {
id: { type: 'string', primaryKey: true },
email: { type: 'string', index: true },
tag: { type: 'string', index: true },
});
for (let i = 0; i < 30; i++) {
await db.query('INSERT INTO users VALUES (?, ?, ?)', [String(i), `e${i}@x.x`, `t${i % 3}`]);
}
const engine = db.getEngine() as unknown as {
lsm: { flush(): Promise<void> };
secondaryIndexes: Map<string, { flush(): Promise<void> }>;
};
await engine.lsm.flush();
for (const idxLsm of engine.secondaryIndexes.values()) await idxLsm.flush();
const stats = await db.query('ANALYZE users');
expect((stats as Record<string, unknown>).rowCount).toBe(30);
expect(typeof (stats as Record<string, unknown>).indexDepth).toBe('number');
expect((stats as Record<string, unknown>).sstableCount).toBeGreaterThanOrEqual(1);
expect(typeof (stats as Record<string, unknown>).memtableSize).toBe('number');
await db.close();
});
test('memory: ANALYZE 不支持抛 NOT_SUPPORTED(护栏)', async () => {
const db = await MetonaSqlark.create({ name: 'v073-analyze-memory', mode: 'memory' });
await db.defineTable('users', { id: { type: 'string', primaryKey: true } });
await expect(db.query('ANALYZE users')).rejects.toMatchObject({ code: 'NOT_SUPPORTED' });
await db.close();
});
});