Files
MetonaSqlark/tests/v070-features.test.ts
thzxx 57415975ea
CI / test (22.x) (push) Successful in 17m29s
CI / test (24.x) (push) Failing after 17m32s
CI / e2e (push) Successful in 9m53s
CI / test (18.x) (push) Successful in 19m17s
CI / test (20.x) (push) Successful in 18m15s
feat: v0.7.0 参数化查询 + 事务增量 flush + 复合主键语义硬化 + EXPLAIN 真实索引信息
- 参数化查询 db.query(sql, params):词法层 ? 绑定 + SQL 字面量安全编码
  ('' 转义/注入防护);参数计数不匹配 PARAM_ERROR;对象参数显式拒绝
- KVStoreEngine 事务增量 flush:行级变更追踪,commit 仅写改动行
  (1000 行表改 1 行:日志 1 条目 vs 整表 1000 条目);级联影响表漏写修复
  (txFullTables 同步加入 txDirtyTables);移除每次 commit 全量 checkpoint
  (阈值自动 checkpoint + close 统一截断)
- 复合主键显式拒绝:createSchema 校验期 SCHEMA_ERROR + ALTER ADD 主键列防护
  (此前静默取第一个主键,其余标记失效)
- EXPLAIN usingIndex 真实命中信息:pk / index:col / none

测试 1126 → 1147(72 套件);行覆盖率 89.8%;版本 0.7.0
2026-08-13 10:57:26 +08:00

404 lines
15 KiB
TypeScript
Raw Permalink 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.0 功能与性能回归测试
*
* 覆盖:
* - 复合主键显式拒绝(defineTable / SQL / ALTER ADD 主键列)
* - 参数化查询 db.query(sql, params)(安全编码/参数计数/注入防护/多语句)
* - KVStoreEngine 事务增量 flush(大表事务 O(改动行),非整表重写)
* - EXPLAIN 真实索引命中信息(pk / index:col / none
*/
import { MetonaSqlark } from '../src/core';
import { KVStoreEngine } from '../src/engine/kvstore_engine';
import { createSchema } from '../src/table/schema';
import { DatabaseError } from '../src/constants';
import { parseLogRecords, KVLogOp } from '../src/engine/kvstore/log';
import { SharedMemoryBackend } from '../src/engine/kvstore/shared_memory_medium';
import type { KVStore } from '../src/engine/kvstore/index';
function uniqueDB(): string {
return `v070-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
}
async function expectCode(promise: Promise<unknown>, code: string): Promise<void> {
try {
await promise;
} catch (e) {
expect((e as { code?: string }).code).toBe(code);
return;
}
throw new Error(`Expected rejection with code "${code}", but promise resolved`);
}
describe('v0.7.0 — 复合主键显式拒绝', () => {
it('defineTable 多主键抛 SCHEMA_ERROR(不再静默取第一个)', () => {
expect(() => createSchema('t', {
a: { type: 'string', primaryKey: true },
b: { type: 'string', primaryKey: true },
})).toThrow('Composite primary keys are not supported');
});
it('SQL CREATE TABLE 多主键抛 SCHEMA_ERROR', async () => {
const db = new MetonaSqlark({ name: uniqueDB(), mode: 'memory' });
await db.init();
await expectCode(
db.query('CREATE TABLE t (a STRING PRIMARY KEY, b STRING PRIMARY KEY)'),
'SCHEMA_ERROR',
);
await db.close();
});
it('ALTER TABLE ADD 主键列(已有主键)抛 SCHEMA_ERROR', async () => {
const db = new MetonaSqlark({ name: uniqueDB(), mode: 'memory' });
await db.init();
await db.defineTable('t', { id: { type: 'string', primaryKey: true } });
await expectCode(
db.query('ALTER TABLE t ADD COLUMN other STRING PRIMARY KEY'),
'SCHEMA_ERROR',
);
await db.close();
});
});
describe('v0.7.0 — 参数化查询', () => {
async function setup(): Promise<MetonaSqlark> {
const db = new MetonaSqlark({ name: uniqueDB(), mode: 'memory' });
await db.init();
await db.defineTable('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string' },
age: { type: 'number' },
active: { type: 'boolean' },
profile: { type: 'json' },
});
return db;
}
it('位置参数绑定(字符串/数字/布尔/null)', async () => {
const db = await setup();
await db.query("INSERT INTO users VALUES ('1', 'Alice', 30, TRUE, NULL)");
await db.query('INSERT INTO users VALUES (?, ?, ?, ?, ?)', ['2', "O'Brien", 25, false, null]);
const rows = await db.query('SELECT * FROM users WHERE id = ?', ['2']) as Record<string, unknown>[];
expect(rows).toHaveLength(1);
expect(rows[0].name).toBe("O'Brien");
expect(rows[0].age).toBe(25);
expect(rows[0].active).toBe(false);
expect(rows[0].profile).toBeNull();
const byName = await db.query("SELECT * FROM users WHERE name = ?", ["O'Brien"]) as Record<string, unknown>[];
expect(byName).toHaveLength(1);
await db.close();
});
it('对象参数显式拒绝(PARAM_ERROR,防静默错配)', async () => {
const db = await setup();
await expectCode(
db.query('INSERT INTO users VALUES (?, ?, ?, ?, ?)', ['1', 'N', 1, true, { city: 'BJ' }]),
'PARAM_ERROR',
);
await db.close();
});
it('注入防护:字符串参数含 SQL 片段被当作字面量', async () => {
const db = await setup();
await db.query('INSERT INTO users VALUES (?, ?, ?, ?, NULL)', ['1', "x'; DROP TABLE users; --", 1, true]);
const rows = await db.query('SELECT * FROM users') as Record<string, unknown>[];
expect(rows).toHaveLength(1);
expect(rows[0].name).toBe("x'; DROP TABLE users; --");
await db.close();
});
it('字符串字面量内的 ? 不被替换', async () => {
const db = await setup();
await db.query("INSERT INTO users VALUES ('1', 'what?', 1, TRUE, NULL)");
const rows = await db.query("SELECT * FROM users WHERE name = 'what?'") as Record<string, unknown>[];
expect(rows).toHaveLength(1);
await db.close();
});
it('参数数量不匹配抛 PARAM_ERROR', async () => {
const db = await setup();
await expectCode(db.query('SELECT * FROM users WHERE id = ?', []), 'PARAM_ERROR');
await expectCode(db.query('SELECT * FROM users WHERE id = ?', ['1', '2']), 'PARAM_ERROR');
await db.close();
});
it('多语句 + 参数', async () => {
const db = await setup();
await db.query("INSERT INTO users VALUES (?, 'A', 1, TRUE, NULL); INSERT INTO users VALUES (?, 'B', 2, TRUE, NULL)", ['1', '2']);
const rows = await db.query('SELECT * FROM users') as Record<string, unknown>[];
expect(rows).toHaveLength(2);
await db.close();
});
it('NaN/Infinity 参数编码为 NULL', async () => {
const db = await setup();
await db.query('INSERT INTO users VALUES (?, ?, ?, ?, NULL)', ['1', 'N', Number.NaN, true]);
const rows = await db.query('SELECT * FROM users WHERE id = ?', ['1']) as Record<string, unknown>[];
expect(rows).toHaveLength(1);
expect(rows[0].age).toBeNull();
await db.close();
});
});
describe('v0.7.0 — KVStoreEngine 事务增量 flush', () => {
it('大表事务改 1 行:日志仅含该行(非整表重写)', async () => {
const name = uniqueDB();
const eng = new KVStoreEngine();
await eng.open(name, 1);
await eng.createTable(createSchema('t', {
id: { type: 'number', primaryKey: true },
v: { type: 'string' },
}));
const rows = Array.from({ length: 1000 }, (_, i) => ({ id: i, v: `v${i}` }));
await eng.insert('t', rows);
// 基线 checkpoint(日志清空)
const kv0 = (eng as any).kv as KVStore;
await kv0.checkpoint();
await eng.beginTransaction();
await eng.update('t', { table: 't', where: { id: 500 } }, { v: 'CHANGED' });
await eng.commitTransaction();
// commit 后日志应只有 1 条记录、1 个 PUT 条目(增量 flush,非 1000 行重写)
const medium = (eng as any).kv.medium as SharedMemoryBackend;
const raw = await medium.read('__kv_log');
const records: { entries: { op: KVLogOp }[] }[] = [];
parseLogRecords(new Uint8Array(raw!), (r) => records.push(r as never));
expect(records).toHaveLength(1);
expect(records[0].entries).toHaveLength(1);
expect(records[0].entries[0].op).toBe(KVLogOp.PUT);
await eng.close();
// 重开后数据完整
const eng2 = new KVStoreEngine();
await eng2.open(name, 1);
const all = await eng2.find('t', { table: 't' });
expect(all).toHaveLength(1000);
expect(all.find((r) => r.id === 500)!.v).toBe('CHANGED');
await eng2.close();
});
it('事务混合操作(insert/update/delete/clear 语义)持久化正确', async () => {
const name = uniqueDB();
const eng = new KVStoreEngine();
await eng.open(name, 1);
await eng.createTable(createSchema('t', {
id: { type: 'string', primaryKey: true },
v: { type: 'string' },
}));
await eng.insert('t', [{ id: '1', v: 'a' }, { id: '2', v: 'b' }, { id: '3', v: 'c' }]);
await (eng as any).kv.checkpoint();
await eng.beginTransaction();
await eng.insert('t', [{ id: '4', v: 'd' }]);
await eng.update('t', { table: 't', where: { id: '1' } }, { v: 'A' });
await eng.delete('t', { table: 't', where: { id: '3' } });
await eng.commitTransaction();
await eng.close();
const eng2 = new KVStoreEngine();
await eng2.open(name, 1);
const rows = await eng2.find('t', { table: 't' });
expect(rows.map((r) => r.id).sort()).toEqual(['1', '2', '4']);
expect(rows.find((r) => r.id === '1')!.v).toBe('A');
await eng2.close();
});
it('事务内 delete 后 reinsert 同主键 → put 最终态', async () => {
const name = uniqueDB();
const eng = new KVStoreEngine();
await eng.open(name, 1);
await eng.createTable(createSchema('t', {
id: { type: 'string', primaryKey: true },
v: { type: 'string' },
}));
await eng.insert('t', [{ id: '1', v: 'old' }]);
await (eng as any).kv.checkpoint();
await eng.beginTransaction();
await eng.delete('t', { table: 't', where: { id: '1' } });
await eng.insert('t', [{ id: '1', v: 'new' }]);
await eng.commitTransaction();
await eng.close();
const eng2 = new KVStoreEngine();
await eng2.open(name, 1);
const rows = await eng2.find('t', { table: 't' });
expect(rows).toHaveLength(1);
expect(rows[0].v).toBe('new');
await eng2.close();
});
it('事务内 clear 后 insert → 最终态仅新行', async () => {
const name = uniqueDB();
const eng = new KVStoreEngine();
await eng.open(name, 1);
await eng.createTable(createSchema('t', {
id: { type: 'string', primaryKey: true },
}));
await eng.insert('t', [{ id: '1' }, { id: '2' }]);
await (eng as any).kv.checkpoint();
await eng.beginTransaction();
await eng.clear('t');
await eng.insert('t', [{ id: '9' }]);
await eng.commitTransaction();
await eng.close();
const eng2 = new KVStoreEngine();
await eng2.open(name, 1);
const rows = await eng2.find('t', { table: 't' });
expect(rows).toHaveLength(1);
expect(rows[0].id).toBe('9');
await eng2.close();
});
it('事务内主键变更 → 整表 diff 兜底路径正确', async () => {
const name = uniqueDB();
const eng = new KVStoreEngine();
await eng.open(name, 1);
await eng.createTable(createSchema('t', {
id: { type: 'string', primaryKey: true },
}));
await eng.insert('t', [{ id: '1' }, { id: '2' }]);
await (eng as any).kv.checkpoint();
await eng.beginTransaction();
await eng.update('t', { table: 't', where: { id: '1' } }, { id: '10' });
await eng.commitTransaction();
await eng.close();
const eng2 = new KVStoreEngine();
await eng2.open(name, 1);
const rows = await eng2.find('t', { table: 't' });
expect(rows.map((r) => r.id).sort()).toEqual(['10', '2']);
await eng2.close();
});
it('多表事务 + 级联(SET NULL)持久化正确', async () => {
const name = uniqueDB();
const eng = new KVStoreEngine();
await eng.open(name, 1);
await eng.createTable(createSchema('users', {
id: { type: 'string', primaryKey: true },
}));
await eng.createTable(createSchema('orders', {
id: { type: 'string', primaryKey: true },
user_id: { type: 'string', references: 'users.id', onDelete: 'SET NULL' },
}));
await eng.insert('users', [{ id: 'u1' }]);
await eng.insert('orders', [{ id: 'o1', user_id: 'u1' }]);
await (eng as any).kv.checkpoint();
await eng.beginTransaction();
await eng.delete('users', { table: 'users', where: { id: 'u1' } });
await eng.insert('users', [{ id: 'u2' }]);
await eng.commitTransaction();
await eng.close();
const eng2 = new KVStoreEngine();
await eng2.open(name, 1);
const users = await eng2.find('users', { table: 'users' });
expect(users.map((r) => r.id)).toEqual(['u2']);
const orders = await eng2.find('orders', { table: 'orders' });
expect(orders).toHaveLength(1);
expect(orders[0].user_id).toBeNull();
await eng2.close();
});
it('事务回滚后不落盘(增量追踪状态重置)', async () => {
const name = uniqueDB();
const eng = new KVStoreEngine();
await eng.open(name, 1);
await eng.createTable(createSchema('t', {
id: { type: 'string', primaryKey: true },
}));
await eng.insert('t', [{ id: '1' }]);
await (eng as any).kv.checkpoint();
await eng.beginTransaction();
await eng.insert('t', [{ id: '2' }]);
await eng.rollbackTransaction();
await eng.close();
const eng2 = new KVStoreEngine();
await eng2.open(name, 1);
const rows = await eng2.find('t', { table: 't' });
expect(rows).toHaveLength(1);
await eng2.close();
});
it('连续事务互不串扰(第二事务增量正常)', async () => {
const name = uniqueDB();
const eng = new KVStoreEngine();
await eng.open(name, 1);
await eng.createTable(createSchema('t', {
id: { type: 'string', primaryKey: true },
v: { type: 'string' },
}));
await eng.insert('t', [{ id: '1', v: 'a' }, { id: '2', v: 'b' }]);
await (eng as any).kv.checkpoint();
await eng.beginTransaction();
await eng.update('t', { table: 't', where: { id: '1' } }, { v: 'A' });
await eng.commitTransaction();
await eng.beginTransaction();
await eng.update('t', { table: 't', where: { id: '2' } }, { v: 'B' });
await eng.commitTransaction();
await eng.close();
const eng2 = new KVStoreEngine();
await eng2.open(name, 1);
const rows = await eng2.find('t', { table: 't' });
expect(rows.find((r) => r.id === '1')!.v).toBe('A');
expect(rows.find((r) => r.id === '2')!.v).toBe('B');
await eng2.close();
});
});
describe('v0.7.0 — EXPLAIN 真实索引信息', () => {
it('主键/索引/无索引条件分别报告 pk/index:col/none', async () => {
const db = new MetonaSqlark({ name: uniqueDB(), mode: 'aria', diskEngine: 'memory' });
await db.init();
await db.defineTable('t', {
id: { type: 'string', primaryKey: true },
tag: { type: 'string', index: true },
other: { type: 'string' },
});
const byPk = await db.query("EXPLAIN SELECT * FROM t WHERE id = '1'") as Record<string, unknown>;
expect(byPk.usingIndex).toBe('pk');
const byIdx = await db.query("EXPLAIN SELECT * FROM t WHERE tag = 'x'") as Record<string, unknown>;
expect(byIdx.usingIndex).toBe('index:tag');
const none = await db.query("EXPLAIN SELECT * FROM t WHERE other = 'x'") as Record<string, unknown>;
expect(none.usingIndex).toBe('none');
await db.close();
});
it('EXPLAIN DELETE 同样报告索引信息(不执行)', async () => {
const db = new MetonaSqlark({ name: uniqueDB(), mode: 'memory' });
await db.init();
await db.defineTable('t', { id: { type: 'string', primaryKey: true } });
await db.query('INSERT INTO t VALUES (\'1\'), (\'2\')');
const plan = await db.query('EXPLAIN DELETE FROM t WHERE id = \'1\'') as Record<string, unknown>;
expect(plan.usingIndex).toBe('pk');
expect(plan.estimatedRows).toBe(1);
const rows = await db.query('SELECT * FROM t') as Record<string, unknown>[];
expect(rows).toHaveLength(2);
await db.close();
});
});
describe('v0.7.0 — 错误码导出检查', () => {
it('DatabaseError code 可访问', () => {
const e = new DatabaseError('m', 'X');
expect(e.code).toBe('X');
});
});