每项都先用可执行探针复现出**错误的实际输出**,再修根因、补永久回归套件
(tests/v080-query-layer.test.ts,8 项 × 4 引擎 + 跨引擎项,共 111 断言)。
A22 GROUP BY 引用 SELECT 别名
修复前:`SELECT g AS grp, COUNT(*) FROM t GROUP BY grp` 抛
`COLUMN_NOT_FOUND Unknown column "g" in SELECT list` —— 错误信息与真正原因
(GROUP BY 用了别名)无关,因为 grp 取到 undefined 使全表并成一组,
投影阶段又发现 g 不在输出行里。
修复:GROUP BY 项先解析回**基列**(别名 → 源表达式)再分组,输出键与
"按基列分组"完全一致。
A23 HAVING 引用未出现在 SELECT 里的聚合
修复前:`SELECT g FROM t GROUP BY g HAVING SUM(n) > 25` → `[]`
(SUM(n) 从未被求值 → HAVING 的键在分组行里不存在 → UNKNOWN)。
修复:需要计算的聚合 = SELECT 列 ∪ HAVING 中的聚合(并集),
并在 HAVING **之后**才把行收缩为 SELECT 输出键(否则又变回 [];
顺序错了会双向出错:先投影 → 空结果,不投影 → 泄漏内部聚合列)。
A25 带表前缀的聚合参数恒 0
修复前:`COUNT(t.n)` → 0、`SUM(t.n)` → null(行键是 n,直接取 row['t.n']
得 undefined 再被"过滤 NULL"剔除,**且不报错**)。
修复:新增唯一列引用解析 resolveColumnValue(前缀剥离 → 精确 → 唯一后缀),
聚合识别统一为 parseAggregateExpression —— 此前"是否聚合"与"如何求值"
用两条不同的正则。取不到列改为抛 COLUMN_NOT_FOUND,不再静默计 0。
A26 UNION 尾部 ORDER BY/LIMIT 归属错误
修复前:`A UNION B ORDER BY id DESC` 只排 B;`... LIMIT 3` 返回 4 行
(parser 把子句挂在右侧 SELECT 上,AST 没有复合查询级字段)。
修复:SelectUnionStatement 增加 orderBy/limit/offset,parser 把子句**上移**
(移动而非复制,否则 LIMIT 应用两次),executor 在合并+去重后统一排序/切片。
A27 DISTINCT 作用在投影前
修复前:`SELECT DISTINCT g AS d FROM t` 返回 4 行 a,a,b,b
(对 {id,g,n} 原始行去重),而 `SELECT DISTINCT g` 返回 2 行。
修复:DISTINCT 移到投影后(作用于输出列);ORDER BY 的应用时机随之拆成
"引用输出列 → 投影后" / "引用非输出列 → 投影前",两者互为因果必须一起改。
A29 maxRowsPerQuery 静默截断写入
修复前:maxRowsPerQuery=2 时 `INSERT INTO dst SELECT id FROM src`(4 行源)
只写 2 行并报成功 —— 不是"限制查询规模"而是**静默丢数据**。
修复:行源不截断(executeSelect 增加 purpose='source'),写路径显式报错。
A30 INSERT 值多于目标列静默丢弃
修复前:`INSERT INTO t (id,g) VALUES ('9','z','LOST')` 报成功、'LOST' 消失。
修复:显式列名时解析期拦截(PARSE_ERROR),未给列名时 executor 对照 schema
拦截(VALIDATION_ERROR)——两种情况都需要,因为前者无需 schema。
A36 派生表别名引用
修复前:`SELECT d.id FROM (SELECT id, g FROM t) AS d` 返回 `[]`,
而同义的 `SELECT id FROM (...) AS d` 正确。
修复:抽出 normalizeUnprefixedReferences(WHERE/ORDER BY/GROUP BY/SELECT
四类引用统一剥离别名前缀),非 JOIN 单表路径与派生表路径共用同一规则。
连带根治(修复过程中发现的两个更底层问题):
1. **同步抛错穿过 async 边界**:`executor.execute()` 里 `return this.executeXxx(stmt)`
的同步前导段若抛错(arity/校验),异常成为**同步抛出** ——
`await expect(db.query(...)).rejects...` 的断言不生效、`.catch()` 永不执行。
现统一包一层 try/catch,保证任何错误都是 rejected promise。
2. **缺列的行形状不一致**:validateRow 此前"值为 undefined 就不落键",
于是 `INSERT INTO t (id,g) VALUES ('9','z')` 的行里没有 n 键 →
`SELECT id,g,n FROM t` 抛 COLUMN_NOT_FOUND: n,而 `SELECT * FROM t` 正常。
现在缺列且无 default → 显式补 null(SQL 语义),行始终含全部 schema 列;
ALTER ADD 同步在已有行上物化 null,使"内存视图"与"重启后视图"一致。
验证:全量 83 套件 / 1589 测试通过(含 Aria 5 万行索引竞态、KVStore 持久化);
typecheck(src+tests) 与 lint 零错误。
517 lines
18 KiB
TypeScript
517 lines
18 KiB
TypeScript
/**
|
||
* 介质:SharedMemoryBackend(跨实例共享,模拟磁盘持久化)
|
||
*/
|
||
|
||
import { KVStoreEngine } from '../../src/engine/kvstore_engine';
|
||
import { SharedMemoryBackend } from '../../src/engine/kvstore/shared_memory_medium';
|
||
import { createSchema } from '../../src/table/schema';
|
||
|
||
let dbCounter = 0;
|
||
|
||
describe('KVStoreEngine', () => {
|
||
let engine: KVStoreEngine;
|
||
let dbName: string;
|
||
|
||
const userSchema = createSchema('users', {
|
||
id: { type: 'string', primaryKey: true },
|
||
name: { type: 'string', required: true },
|
||
age: { type: 'number', default: 0 },
|
||
email: { type: 'string', unique: true },
|
||
});
|
||
|
||
beforeEach(async () => {
|
||
dbName = `test-kv-${++dbCounter}-${Math.random().toString(36).slice(2, 6)}`;
|
||
engine = new KVStoreEngine(new SharedMemoryBackend());
|
||
await engine.open(dbName, 1);
|
||
});
|
||
|
||
afterEach(async () => {
|
||
if (engine.isOpen()) {
|
||
await engine.close();
|
||
}
|
||
});
|
||
|
||
// ---- 生命周期 ----
|
||
|
||
describe('生命周期', () => {
|
||
it('打开和关闭', async () => {
|
||
expect(engine.isOpen()).toBe(true);
|
||
await engine.close();
|
||
expect(engine.isOpen()).toBe(false);
|
||
});
|
||
|
||
it('未打开时操作抛出错误', async () => {
|
||
await engine.close();
|
||
await expect(engine.count('users')).rejects.toThrow('not opened');
|
||
});
|
||
|
||
it('可以重新打开', async () => {
|
||
await engine.close();
|
||
await engine.open(dbName, 2);
|
||
expect(engine.isOpen()).toBe(true);
|
||
});
|
||
});
|
||
|
||
// ---- 表管理 ----
|
||
|
||
describe('表管理', () => {
|
||
it('创建表', async () => {
|
||
await engine.createTable(userSchema);
|
||
expect(await engine.hasTable('users')).toBe(true);
|
||
const names = await engine.getTableNames();
|
||
expect(names).toContain('users');
|
||
});
|
||
|
||
it('重复创建表抛错', async () => {
|
||
await engine.createTable(userSchema);
|
||
await expect(engine.createTable(userSchema)).rejects.toThrow('already exists');
|
||
});
|
||
|
||
it('删除表', async () => {
|
||
await engine.createTable(userSchema);
|
||
await engine.insert('users', [{ id: '1', name: 'Alice' }]);
|
||
await engine.dropTable('users');
|
||
expect(await engine.hasTable('users')).toBe(false);
|
||
await expect(engine.count('users')).rejects.toThrow('does not exist');
|
||
});
|
||
|
||
it('获取表结构', async () => {
|
||
await engine.createTable(userSchema);
|
||
const schema = await engine.getTableSchema('users');
|
||
expect(schema).not.toBeNull();
|
||
expect(schema!.columns.id.primaryKey).toBe(true);
|
||
});
|
||
|
||
it('schema 持久化:重启后表结构保留', async () => {
|
||
await engine.createTable(userSchema);
|
||
await engine.close();
|
||
|
||
const engine2 = new KVStoreEngine(new SharedMemoryBackend());
|
||
await engine2.open(dbName, 1);
|
||
expect(await engine2.hasTable('users')).toBe(true);
|
||
const schema = await engine2.getTableSchema('users');
|
||
expect(schema!.columns.email.unique).toBe(true);
|
||
await engine2.close();
|
||
});
|
||
});
|
||
|
||
// ---- 插入 ----
|
||
|
||
describe('插入', () => {
|
||
beforeEach(async () => { await engine.createTable(userSchema); });
|
||
|
||
it('插入单行并返回主键', async () => {
|
||
const pks = await engine.insert('users', [{ id: '1', name: 'Alice', age: 30 }]);
|
||
expect(pks).toEqual(['1']);
|
||
expect(await engine.count('users')).toBe(1);
|
||
});
|
||
|
||
it('默认值生效', async () => {
|
||
await engine.insert('users', [{ id: '1', name: 'Alice' }]);
|
||
const rows = await engine.find('users', { table: 'users' });
|
||
expect(rows[0].age).toBe(0);
|
||
});
|
||
|
||
it('重复主键抛错', async () => {
|
||
await engine.insert('users', [{ id: '1', name: 'A' }]);
|
||
await expect(engine.insert('users', [{ id: '1', name: 'B' }])).rejects.toThrow('Duplicate primary key');
|
||
});
|
||
|
||
it('唯一约束冲突抛错', async () => {
|
||
await engine.insert('users', [{ id: '1', name: 'A', email: 'a@x.com' }]);
|
||
await expect(engine.insert('users', [{ id: '2', name: 'B', email: 'a@x.com' }]))
|
||
.rejects.toThrow('Unique constraint violation');
|
||
});
|
||
|
||
it('必填字段校验', async () => {
|
||
await expect(engine.insert('users', [{ id: '1' }])).rejects.toThrow('is required');
|
||
});
|
||
|
||
it('批量插入', async () => {
|
||
const pks = await engine.insert('users', [
|
||
{ id: '1', name: 'A' }, { id: '2', name: 'B' }, { id: '3', name: 'C' },
|
||
]);
|
||
expect(pks).toEqual(['1', '2', '3']);
|
||
});
|
||
});
|
||
|
||
// ---- 查询 ----
|
||
|
||
describe('查询', () => {
|
||
beforeEach(async () => {
|
||
await engine.createTable(userSchema);
|
||
await engine.insert('users', [
|
||
{ id: '1', name: 'Alice', age: 30 },
|
||
{ id: '2', name: 'Bob', age: 25 },
|
||
{ id: '3', name: 'Carol', age: 35 },
|
||
]);
|
||
});
|
||
|
||
it('全表查询', async () => {
|
||
const rows = await engine.find('users', { table: 'users' });
|
||
expect(rows).toHaveLength(3);
|
||
});
|
||
|
||
it('WHERE 等值过滤', async () => {
|
||
const rows = await engine.find('users', { table: 'users', where: { name: 'Bob' } });
|
||
expect(rows).toHaveLength(1);
|
||
expect(rows[0].id).toBe('2');
|
||
});
|
||
|
||
it('WHERE 操作符', async () => {
|
||
const rows = await engine.find('users', { table: 'users', where: { age: { $gt: 26 } } });
|
||
expect(rows.map((r) => r.id).sort()).toEqual(['1', '3']);
|
||
});
|
||
|
||
it('ORDER BY', async () => {
|
||
const rows = await engine.find('users', { table: 'users', orderBy: [{ column: 'age', direction: 'desc' }] });
|
||
expect(rows[0].id).toBe('3');
|
||
expect(rows[2].id).toBe('2');
|
||
});
|
||
|
||
it('LIMIT / OFFSET', async () => {
|
||
const rows = await engine.find('users', { table: 'users', limit: 2, offset: 1 });
|
||
expect(rows).toHaveLength(2);
|
||
});
|
||
|
||
it('列投影', async () => {
|
||
const rows = await engine.find('users', { table: 'users', columns: ['name'] });
|
||
expect(Object.keys(rows[0])).toEqual(['name']);
|
||
});
|
||
});
|
||
|
||
// ---- 更新 ----
|
||
|
||
describe('更新', () => {
|
||
beforeEach(async () => {
|
||
await engine.createTable(userSchema);
|
||
await engine.insert('users', [
|
||
{ id: '1', name: 'Alice', age: 30 },
|
||
{ id: '2', name: 'Bob', age: 25 },
|
||
]);
|
||
});
|
||
|
||
it('按条件更新', async () => {
|
||
const count = await engine.update('users', { table: 'users', where: { id: '1' } }, { age: 31 });
|
||
expect(count).toBe(1);
|
||
const rows = await engine.find('users', { table: 'users', where: { id: '1' } });
|
||
expect(rows[0].age).toBe(31);
|
||
});
|
||
|
||
it('无匹配行影响 0 行', async () => {
|
||
const count = await engine.update('users', { table: 'users', where: { id: 'nope' } }, { age: 1 });
|
||
expect(count).toBe(0);
|
||
});
|
||
|
||
it('更新主键(旧键删除 + 新键写入)', async () => {
|
||
const count = await engine.update('users', { table: 'users', where: { id: '1' } }, { id: '10', age: 99 });
|
||
expect(count).toBe(1);
|
||
expect(await engine.count('users')).toBe(2);
|
||
const rows = await engine.find('users', { table: 'users', where: { id: '10' } });
|
||
expect(rows[0].age).toBe(99);
|
||
expect(await engine.find('users', { table: 'users', where: { id: '1' } })).toHaveLength(0);
|
||
});
|
||
|
||
it('更新持久化:重启后保留', async () => {
|
||
await engine.update('users', { table: 'users', where: { id: '1' } }, { age: 99 });
|
||
await engine.close();
|
||
|
||
const engine2 = new KVStoreEngine(new SharedMemoryBackend());
|
||
await engine2.open(dbName, 1);
|
||
const rows = await engine2.find('users', { table: 'users', where: { id: '1' } });
|
||
expect(rows[0].age).toBe(99);
|
||
await engine2.close();
|
||
});
|
||
});
|
||
|
||
// ---- 删除 ----
|
||
|
||
describe('删除', () => {
|
||
beforeEach(async () => {
|
||
await engine.createTable(userSchema);
|
||
await engine.insert('users', [
|
||
{ id: '1', name: 'Alice' }, { id: '2', name: 'Bob' }, { id: '3', name: 'Carol' },
|
||
]);
|
||
});
|
||
|
||
it('按条件删除', async () => {
|
||
const count = await engine.delete('users', { table: 'users', where: { id: '1' } });
|
||
expect(count).toBe(1);
|
||
expect(await engine.count('users')).toBe(2);
|
||
});
|
||
|
||
it('删除持久化:重启后保留', async () => {
|
||
await engine.delete('users', { table: 'users', where: { id: '1' } });
|
||
await engine.close();
|
||
|
||
const engine2 = new KVStoreEngine(new SharedMemoryBackend());
|
||
await engine2.open(dbName, 1);
|
||
expect(await engine2.count('users')).toBe(2);
|
||
await engine2.close();
|
||
});
|
||
|
||
it('级联删除(CASCADE)', async () => {
|
||
const ordersSchema = createSchema('orders', {
|
||
id: { type: 'string', primaryKey: true },
|
||
user_id: { type: 'string', references: 'users.id', onDelete: 'CASCADE' },
|
||
});
|
||
await engine.createTable(ordersSchema);
|
||
await engine.insert('orders', [{ id: 'o1', user_id: '1' }, { id: 'o2', user_id: '2' }]);
|
||
|
||
await engine.delete('users', { table: 'users', where: { id: '1' } });
|
||
// 级联:orders 中 user_id=1 的行被删
|
||
expect(await engine.count('orders')).toBe(1);
|
||
|
||
// 重启后级联结果持久化
|
||
await engine.close();
|
||
const engine2 = new KVStoreEngine(new SharedMemoryBackend());
|
||
await engine2.open(dbName, 1);
|
||
expect(await engine2.count('users')).toBe(2);
|
||
expect(await engine2.count('orders')).toBe(1);
|
||
await engine2.close();
|
||
});
|
||
});
|
||
|
||
// ---- Count ----
|
||
|
||
describe('Count', () => {
|
||
beforeEach(async () => {
|
||
await engine.createTable(userSchema);
|
||
await engine.insert('users', [
|
||
{ id: '1', name: 'A', age: 10 }, { id: '2', name: 'B', age: 20 }, { id: '3', name: 'C', age: 30 },
|
||
]);
|
||
});
|
||
|
||
it('总数', async () => {
|
||
expect(await engine.count('users')).toBe(3);
|
||
});
|
||
|
||
it('条件计数', async () => {
|
||
expect(await engine.count('users', { table: 'users', where: { age: { $gte: 20 } } })).toBe(2);
|
||
});
|
||
});
|
||
|
||
// ---- 事务 ----
|
||
|
||
describe('事务', () => {
|
||
beforeEach(async () => { await engine.createTable(userSchema); });
|
||
|
||
it('commit 后数据可见且持久化', async () => {
|
||
await engine.beginTransaction();
|
||
await engine.insert('users', [{ id: '1', name: 'Alice' }]);
|
||
await engine.commitTransaction();
|
||
expect(await engine.count('users')).toBe(1);
|
||
|
||
await engine.close();
|
||
const engine2 = new KVStoreEngine(new SharedMemoryBackend());
|
||
await engine2.open(dbName, 1);
|
||
expect(await engine2.count('users')).toBe(1);
|
||
await engine2.close();
|
||
});
|
||
|
||
it('rollback 后数据消失', async () => {
|
||
await engine.beginTransaction();
|
||
await engine.insert('users', [{ id: '1', name: 'Alice' }]);
|
||
await engine.rollbackTransaction();
|
||
expect(await engine.count('users')).toBe(0);
|
||
});
|
||
|
||
it('事务内读到自己写入的行', async () => {
|
||
await engine.beginTransaction();
|
||
await engine.insert('users', [{ id: '1', name: 'Alice' }]);
|
||
const rows = await engine.find('users', { table: 'users' });
|
||
expect(rows).toHaveLength(1);
|
||
await engine.rollbackTransaction();
|
||
});
|
||
|
||
it('commit 多表原子(全部生效或全部不生效)', async () => {
|
||
await engine.createTable(createSchema('orders', {
|
||
id: { type: 'string', primaryKey: true },
|
||
user_id: { type: 'string' },
|
||
}));
|
||
await engine.beginTransaction();
|
||
await engine.insert('users', [{ id: '1', name: 'Alice' }]);
|
||
await engine.insert('orders', [{ id: 'o1', user_id: '1' }]);
|
||
await engine.commitTransaction();
|
||
|
||
await engine.close();
|
||
const engine2 = new KVStoreEngine(new SharedMemoryBackend());
|
||
await engine2.open(dbName, 1);
|
||
expect(await engine2.count('users')).toBe(1);
|
||
expect(await engine2.count('orders')).toBe(1);
|
||
await engine2.close();
|
||
});
|
||
|
||
it('事务中 DDL 支持(create/drop 随 commit 生效)', async () => {
|
||
const txSchema = createSchema('tx_users', {
|
||
id: { type: 'string', primaryKey: true },
|
||
name: { type: 'string' },
|
||
});
|
||
await engine.beginTransaction();
|
||
await engine.createTable(txSchema);
|
||
await engine.insert('tx_users', [{ id: '1', name: 'A' }]);
|
||
await engine.commitTransaction();
|
||
expect(await engine.hasTable('tx_users')).toBe(true);
|
||
expect(await engine.count('tx_users')).toBe(1);
|
||
|
||
// 事务中 drop 表
|
||
await engine.beginTransaction();
|
||
await engine.dropTable('tx_users');
|
||
await engine.commitTransaction();
|
||
expect(await engine.hasTable('tx_users')).toBe(false);
|
||
|
||
// 重启后 drop 生效(KV 残留清理)
|
||
await engine.close();
|
||
const engine2 = new KVStoreEngine(new SharedMemoryBackend());
|
||
await engine2.open(dbName, 1);
|
||
expect(await engine2.hasTable('tx_users')).toBe(false);
|
||
await engine2.close();
|
||
});
|
||
});
|
||
|
||
// ---- 持久化与崩溃恢复 ----
|
||
|
||
describe('持久化与崩溃恢复', () => {
|
||
it('未 checkpoint 数据重启后经日志重放恢复', async () => {
|
||
await engine.createTable(userSchema);
|
||
await engine.insert('users', [
|
||
{ id: '1', name: 'Alice', age: 30 },
|
||
{ id: '2', name: 'Bob', age: 25 },
|
||
]);
|
||
await engine.close(); // 模拟"崩溃后重开"(未 checkpoint,数据在 KVStore 日志)
|
||
|
||
const engine2 = new KVStoreEngine(new SharedMemoryBackend());
|
||
await engine2.open(dbName, 1);
|
||
expect(await engine2.count('users')).toBe(2);
|
||
const rows = await engine2.find('users', { table: 'users' });
|
||
expect(rows.map((r) => r.id).sort()).toEqual(['1', '2']);
|
||
await engine2.close();
|
||
});
|
||
|
||
it('checkpoint 后重启(快照恢复)', async () => {
|
||
await engine.createTable(userSchema);
|
||
await engine.insert('users', [{ id: '1', name: 'Alice' }]);
|
||
await (engine as any).kv.checkpoint();
|
||
await engine.insert('users', [{ id: '2', name: 'Bob' }]);
|
||
await engine.close();
|
||
|
||
const engine2 = new KVStoreEngine(new SharedMemoryBackend());
|
||
await engine2.open(dbName, 1);
|
||
expect(await engine2.count('users')).toBe(2);
|
||
await engine2.close();
|
||
});
|
||
|
||
it('索引列跨重启恢复(查询走索引)', async () => {
|
||
const idxSchema = createSchema('items', {
|
||
id: { type: 'string', primaryKey: true },
|
||
tag: { type: 'string', index: true },
|
||
});
|
||
await engine.createTable(idxSchema);
|
||
await engine.insert('items', [
|
||
{ id: '1', tag: 'a' }, { id: '2', tag: 'b' }, { id: '3', tag: 'a' },
|
||
]);
|
||
await engine.close();
|
||
|
||
const engine2 = new KVStoreEngine(new SharedMemoryBackend());
|
||
await engine2.open(dbName, 1);
|
||
const rows = await engine2.find('items', { table: 'items', where: { tag: 'a' } });
|
||
expect(rows).toHaveLength(2);
|
||
await engine2.close();
|
||
});
|
||
|
||
it('CREATE INDEX 持久化(重启后索引保留)', async () => {
|
||
await engine.createTable(userSchema);
|
||
await engine.insert('users', [{ id: '1', name: 'Alice', age: 30 }]);
|
||
await engine.createIndex('users', 'name');
|
||
await engine.close();
|
||
|
||
const engine2 = new KVStoreEngine(new SharedMemoryBackend());
|
||
await engine2.open(dbName, 1);
|
||
const schema = await engine2.getTableSchema('users');
|
||
expect(schema!.columns.name.index).toBe(true);
|
||
const rows = await engine2.find('users', { table: 'users', where: { name: 'Alice' } });
|
||
expect(rows).toHaveLength(1);
|
||
await engine2.close();
|
||
});
|
||
|
||
it('ALTER TABLE ADD/DROP 持久化', async () => {
|
||
await engine.createTable(userSchema);
|
||
await engine.insert('users', [{ id: '1', name: 'Alice', age: 30 }]);
|
||
await engine.alterTable('users', 'ADD', { name: 'phone', type: 'string' });
|
||
await engine.alterTable('users', 'DROP', { name: 'age', type: 'number' });
|
||
await engine.close();
|
||
|
||
const engine2 = new KVStoreEngine(new SharedMemoryBackend());
|
||
await engine2.open(dbName, 1);
|
||
const schema = await engine2.getTableSchema('users');
|
||
expect(schema!.columns.phone).toBeDefined();
|
||
expect(schema!.columns.age).toBeUndefined();
|
||
const rows = await engine2.find('users', { table: 'users' });
|
||
// v0.8.0(B-1):ADD 出来的列在已有行上物化为 NULL。
|
||
//
|
||
// 行校验契约保证"存储行含全部 schema 列",此前 ADD 不补齐,于是同一行
|
||
// 在内存里没有 phone 键、落盘再读回(恢复路径走 validateRow)却有
|
||
// `phone: null` —— 行的形状取决于是否重启过。此断言按新契约改为 null。
|
||
expect(rows[0].phone).toBeNull();
|
||
// DROP 掉的列必须真正消失(不是变 null):它是**不在 schema 里**的列,
|
||
// 若保留为 null 会让"已删列"看起来仍然存在。
|
||
expect(rows[0].age).toBeUndefined();
|
||
expect('age' in rows[0]).toBe(false);
|
||
await engine2.close();
|
||
});
|
||
|
||
it('元数据 getMeta/setMeta 持久化', async () => {
|
||
await engine.setMeta('__metona_version', '3');
|
||
await engine.close();
|
||
|
||
const engine2 = new KVStoreEngine(new SharedMemoryBackend());
|
||
await engine2.open(dbName, 1);
|
||
expect(await engine2.getMeta('__metona_version')).toBe('3');
|
||
await engine2.close();
|
||
});
|
||
|
||
it('数据损坏自愈:日志损坏 → open 不崩,repair 截断', async () => {
|
||
await engine.createTable(userSchema);
|
||
await engine.insert('users', [{ id: '1', name: 'Alice' }]);
|
||
await engine.close();
|
||
|
||
// 篡改日志尾部 CRC(模拟最后一条记录写入中断)
|
||
const medium = new SharedMemoryBackend();
|
||
await medium.open(dbName);
|
||
const log = await medium.read('__kv_log');
|
||
expect(log).not.toBeNull();
|
||
const corrupted = new Uint8Array(log as ArrayBuffer);
|
||
corrupted[corrupted.byteLength - 5] ^= 0xff;
|
||
await medium.write('__kv_log', corrupted.buffer as ArrayBuffer);
|
||
|
||
const engine2 = new KVStoreEngine(new SharedMemoryBackend());
|
||
await engine2.open(dbName, 1); // 损坏尾部截断,打开不崩
|
||
expect(await engine2.hasTable('users')).toBe(true);
|
||
await engine2.repair();
|
||
expect(await engine2.hasTable('users')).toBe(true);
|
||
// repair 后新写入正常
|
||
await engine2.insert('users', [{ id: '2', name: 'Bob' }]);
|
||
expect(await engine2.count('users')).toBeGreaterThanOrEqual(1);
|
||
await engine2.close();
|
||
});
|
||
});
|
||
|
||
// ---- 流式查询 ----
|
||
|
||
describe('流式查询', () => {
|
||
beforeEach(async () => {
|
||
await engine.createTable(userSchema);
|
||
await engine.insert('users', [
|
||
{ id: '1', name: 'A' }, { id: '2', name: 'B' }, { id: '3', name: 'C' },
|
||
]);
|
||
});
|
||
|
||
it('findStream 逐行回调', async () => {
|
||
const rows: Record<string, unknown>[] = [];
|
||
const count = await engine.findStream!('users', { table: 'users' }, (r) => rows.push(r));
|
||
expect(count).toBe(3);
|
||
expect(rows.map((r) => r.id).sort()).toEqual(['1', '2', '3']);
|
||
});
|
||
});
|
||
});
|