Files
MetonaSqlark/tests/v073-fixes.test.ts
T
thzxx 4ab04df882 fix(A15): 三值逻辑求值器统一 —— 消除 WHERE 的第二套语义(静默错值根治)
背景(PLAN-v0.7.5.md 根因 1/7、缺陷 A15):
项目里 `matchWhere`(布尔版,自带 matchField)与三值求值器并存。同一条 SQL 的
语义取决于走哪个函数,实测三类静默错值:
  - `WHERE n = NULL` 命中 NULL 行、`WHERE n != NULL` 返回所有非 NULL 行;
  - `WHERE s NOT LIKE 'x'` 会把 NULL 行判真(布尔取反);
  - `WHERE n NOT BETWEEN 1 AND 2` 恒空集 —— parser 生成的字段级
    `{ n: { $or: [ {$lt:1}, {$gt:2} ] } }` 递归进了 where 子句级求值器,
    子项 `{ $lt: 1 }` 被当成"查询列 `$lt`" → 每行 UNKNOWN。

根治方式(不是打补丁,而是取消第二套实现):
1. where-matcher.ts 重写为**唯一一个递归求值器**,同时理解 where 子句级
   (键是列名/逻辑连接词)与操作符级(键是 `$gt` …),位置由上下文承载而非
   由另一个函数承载;`matchWhere` 退化为"三值结果是否恰为 TRUE"。
   行上下文随求值上下文下传,`$col` 在任意嵌套深度都能解析。
2. parser:`IS NULL` / `IS NOT NULL` 生成 `$isNull` / `$isNotNull` **谓词**
   (此前与 `= NULL` / `!= NULL` 共用 `$eq: null` / `$ne: null`,两者语义无法区分);
   `BETWEEN` 生成真正的范围条件(此前把同一对象同时当操作符对象与操作数);
   `NOT BETWEEN` 展开为 `$or: [{$lt}, {$gt}]`。
3. executor 新增 enginePreFilter:逐行求值谓词(`$col` / `$exists` / CASE 键)
   必须整体移出引擎层 —— 引擎无外层行上下文,会把它们判 UNKNOWN 并把**所有行**
   过滤掉,逐行求值再正确也无行可算。粒度按连接词决定:`$and` 成员可单独移除,
   `$or`/`$not` 成员一移除就改变结果集(漏行/多行),故整条下推放弃。

契约变更(旧测试编码了错误语义,已按 SQL 标准改正并注明理由):
  - a) `{ $eq: null }` 不再命中 NULL 行(`= NULL` 恒 UNKNOWN)→ 用 `$isNull`;
  - b) `IN` 列表含 NULL:`x IN (NULL, 'a')` 只命中 'a'(`null = NULL` 为 UNKNOWN),
       未命中的行仍因 UNKNOWN 不保留。
  - c) 引擎层 `$in: [null, ...]` 与 `$eq: null` 的断言同步修正。

验证:
  - 新增 tests/v080-sql-three-valued.test.ts:26 条 SQL 语义矩阵 × 4 引擎
    (memory/disk/hybrid/aria)+ UPDATE/DELETE 写路径,共 104 断言;
  - 全量 83 套件 / 1458 测试通过(含 Aria 生产负载 10 万行);
  - typecheck(src+tests) 与 lint 零错误。
2026-09-14 23:22:26 +08:00

647 lines
30 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 外列不持久化', async () => {
const db = await MetonaSqlark.create({ name: 'v073-kv-extra-col', mode: 'disk', diskEngine: 'memory' });
await db.defineTable('users', { id: { type: 'string', primaryKey: true } });
await db.table('users').insert({ id: '1', junk: 'x' } 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();
});
});