Files
MetonaSqlark/tests/engine/kvstore-engine.test.ts
T
thzxx 841db2e049 fix(A22/A23/A25/A26/A27/A29/A30/A36): 查询层 8 项缺陷根治 + 单一语义收敛
每项都先用可执行探针复现出**错误的实际输出**,再修根因、补永久回归套件
(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 零错误。
2026-09-15 00:00:08 +08:00

517 lines
18 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.
/**
* 介质: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.0B-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']);
});
});
});