Files
MetonaSqlark/tests/v040-features.test.ts
T
thzxx a1e4f5071c
CI / test (20.x) (push) Successful in 10m4s
CI / test (22.x) (push) Successful in 10m8s
CI / test (24.x) (push) Successful in 9m55s
CI / test (18.x) (push) Successful in 10m9s
release: v0.4.1 — Aria 级联/ALTER/clearAll + 流式查询/派生表 + 正确性加固
新增:
- AriaEngine 外键级联(CASCADE/SET NULL/RESTRICT)+ clearAll() 重置 API
- 引擎级 alterTable:Aria DROP COLUMN 重写存储行 + schema 持久化
- 流式查询 queryStream / findStream(LSM 惰性扫描不物化)
- FROM 派生表 / 多列 ON 哈希连接 / COUNT(DISTINCT) / NULLS FIRST/LAST
- 普通列别名 + ORDER BY 别名 + 无表查询 + 字符串常量列
- 演示页引擎切换器(Memory/Aria)+ 预设自动重置

修复:
- Aria WAL DROP_TABLE 崩溃恢复(删表复活)+ 恢复后 WAL 截断
- Memory update/delete 索引维护(unique 约束绕过)
- 关联 EXISTS 绑定失效 / HAVING 标量子查询 / INSERT SELECT 位置错位
- 裸布尔列条件(WHERE done / CASE WHEN done)
- Aria $in 重复行 / JOIN 主表 WHERE 下推 / DROP INDEX 报错
- ORDER BY/GROUP BY/SELECT 表前缀列 + SQL '' 标准转义

质量:894 测试 · 47 套件 · 81.5% 覆盖率
2026-08-08 13:36:34 +08:00

383 lines
20 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.4.0 功能扩展测试
* - B-1: 流式查询 queryStream / findStream
* - B-2: 多列 ON 哈希连接
* - B-3: FROM 子查询(派生表)
* - B-4: COUNT(DISTINCT) + NULLS FIRST/LAST
*/
import 'fake-indexeddb/auto';
import { MetonaSqlark } from '../src/core';
import { AriaEngine } from '../src/engine/aria/index';
const uniqueName = (prefix: string): string =>
`${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
describe('[v0.4.0] B-1: 流式查询', () => {
test('Aria findStream 逐行回调 + limit 生效', async () => {
const e = new AriaEngine({ storageBackend: 'memory', walSyncMode: 'full' });
await e.open('stream1', 1);
await e.createTable({ name: 'logs', columns: { id: { type: 'string', primaryKey: true }, level: { type: 'string' } } });
const rows: Record<string, unknown>[] = [];
for (let i = 0; i < 5000; i++) rows.push({ id: `${i}`, level: i % 2 ? 'error' : 'info' });
await e.insert('logs', rows);
let count = 0;
let sum = 0;
const n = await e.findStream('logs', { table: 'logs', where: { level: 'error' }, limit: 100 }, (r) => {
count++;
sum += Number(r.id);
});
expect(n).toBe(100);
expect(count).toBe(100);
expect(sum).toBeGreaterThan(0);
await e.close();
});
test('db.queryStream 端到端(WHERE + 投影)', async () => {
const db = new MetonaSqlark({ name: uniqueName('qs'), mode: 'memory' });
await db.init();
await db.defineTable('users', { id: { type: 'string', primaryKey: true }, age: { type: 'number' } });
await db.query("INSERT INTO users VALUES ('1', 20), ('2', 30), ('3', 40), ('4', 30)");
const seen: Record<string, unknown>[] = [];
const total = await db.queryStream('SELECT id FROM users WHERE age >= 30', (row) => seen.push(row));
expect(total).toBe(3);
expect(seen.map((r) => r.id).sort()).toEqual(['2', '3', '4']);
await db.close();
});
test('queryStream 对 async 回调回退物化(不吞 Promise', async () => {
const db = new MetonaSqlark({ name: uniqueName('qa'), mode: 'memory' });
await db.init();
await db.defineTable('t', { id: { type: 'string', primaryKey: true } });
await db.query("INSERT INTO t VALUES ('1'), ('2')");
const seen: string[] = [];
const total = await db.queryStream('SELECT * FROM t', async (row) => {
await new Promise((r) => setTimeout(r, 1));
seen.push(String(row.id));
});
expect(total).toBe(2);
expect(seen.sort()).toEqual(['1', '2']);
await db.close();
});
test('queryStream 不支持非 SELECT 语句', async () => {
const db = new MetonaSqlark({ name: uniqueName('qi'), mode: 'memory' });
await db.init();
await expect(db.queryStream('INSERT INTO x VALUES (1)', () => {})).rejects.toThrow('only supports SELECT');
await db.close();
});
test('db.table().stream() 逐行回调', async () => {
const db = new MetonaSqlark({ name: uniqueName('qt'), mode: 'memory' });
await db.init();
await db.defineTable('t', { id: { type: 'string', primaryKey: true }, v: { type: 'number' } });
await db.query("INSERT INTO t VALUES ('1', 10), ('2', 20), ('3', 30)");
const seen: number[] = [];
const total = await db.table('t').stream((row) => seen.push(Number(row.v)), { where: { v: { $gte: 20 } } });
expect(total).toBe(2);
expect(seen.sort((a, b) => a - b)).toEqual([20, 30]);
await db.close();
});
});
describe('[v0.4.0] B-2: 多列 ON 哈希连接', () => {
test('复合键 INNER JOIN 结果正确', async () => {
const db = new MetonaSqlark({ name: uniqueName('hj'), mode: 'memory' });
await db.init();
await db.defineTable('a', {
id: { type: 'string', primaryKey: true },
tenant: { type: 'string', index: true },
key: { type: 'string' },
});
await db.defineTable('b', {
id: { type: 'string', primaryKey: true },
tenant: { type: 'string', index: true },
key: { type: 'string' },
val: { type: 'number' },
});
await db.query("INSERT INTO a VALUES ('a1', 't1', 'k1'), ('a2', 't2', 'k2'), ('a3', 't1', 'k9')");
await db.query("INSERT INTO b VALUES ('b1', 't1', 'k1', 100), ('b2', 't1', 'k2', 200), ('b3', 't2', 'k2', 300)");
// tenant + key 复合等值连接
const rows = await db.query(
"SELECT a.id AS aid, b.val FROM a INNER JOIN b ON a.tenant = b.tenant AND a.key = b.key",
) as Record<string, unknown>[];
expect(rows.length).toBe(2);
const byAid = Object.fromEntries(rows.map((r) => [r.aid, r['b.val']]));
expect(byAid['a1']).toBe(100); // t1/k1 匹配 b1
expect(byAid['a2']).toBe(300); // t2/k2 匹配 b3t1/k2 的 b2 因 tenant 不同被排除)
expect(byAid['a3']).toBeUndefined(); // t1/k9 无匹配
await db.close();
});
test('复合键 LEFT JOIN 无匹配行置 null', async () => {
const db = new MetonaSqlark({ name: uniqueName('hl'), mode: 'memory' });
await db.init();
await db.defineTable('a', { id: { type: 'string', primaryKey: true }, k1: { type: 'string', index: true }, k2: { type: 'string' } });
await db.defineTable('b', { id: { type: 'string', primaryKey: true }, k1: { type: 'string', index: true }, k2: { type: 'string' }, v: { type: 'number' } });
await db.query("INSERT INTO a VALUES ('a1', 'x', 'y'), ('a2', 'x', 'z')");
await db.query("INSERT INTO b VALUES ('b1', 'x', 'y', 5)");
const rows = await db.query(
"SELECT a.id AS aid, b.v FROM a LEFT JOIN b ON a.k1 = b.k1 AND a.k2 = b.k2",
) as Record<string, unknown>[];
expect(rows.length).toBe(2);
expect(rows.find((r) => r.aid === 'a1')?.['b.v']).toBe(5);
expect(rows.find((r) => r.aid === 'a2')?.['b.v']).toBeNull();
await db.close();
});
});
describe('[v0.4.0] B-3: FROM 子查询(派生表)', () => {
test('FROM (SELECT ...) 派生表 + WHERE + ORDER BY', async () => {
const db = new MetonaSqlark({ name: uniqueName('sub'), mode: 'memory' });
await db.init();
await db.defineTable('emp', {
id: { type: 'string', primaryKey: true },
dept: { type: 'string' },
salary: { type: 'number' },
});
await db.query("INSERT INTO emp VALUES ('1', 'eng', 100), ('2', 'eng', 300), ('3', 'ops', 200)");
const rows = await db.query(
"SELECT dept, total FROM (SELECT dept, SUM(salary) AS total FROM emp GROUP BY dept) AS t WHERE total > 150 ORDER BY total DESC",
) as Record<string, unknown>[];
expect(rows).toEqual([{ dept: 'eng', total: 400 }, { dept: 'ops', total: 200 }]);
await db.close();
});
test('派生表无别名也可用', async () => {
const db = new MetonaSqlark({ name: uniqueName('sub2'), mode: 'memory' });
await db.init();
await db.defineTable('t', { id: { type: 'string', primaryKey: true }, v: { type: 'number' } });
await db.query("INSERT INTO t VALUES ('1', 1), ('2', 2)");
const rows = await db.query("SELECT v FROM (SELECT * FROM t WHERE v > 1) WHERE v < 10") as Record<string, unknown>[];
expect(rows).toEqual([{ v: 2 }]);
await db.close();
});
});
describe('[v0.4.0] B-4: COUNT(DISTINCT) + NULLS FIRST/LAST', () => {
test('COUNT(DISTINCT col)', async () => {
const db = new MetonaSqlark({ name: uniqueName('cd'), mode: 'memory' });
await db.init();
await db.defineTable('t', { id: { type: 'string', primaryKey: true }, dept: { type: 'string' } });
await db.query("INSERT INTO t VALUES ('1', 'a'), ('2', 'b'), ('3', 'a'), ('4', 'c')");
const rows = await db.query('SELECT COUNT(DISTINCT dept) AS n FROM t') as Record<string, unknown>[];
expect(rows[0].n).toBe(3);
await db.close();
});
test('SUM(DISTINCT col) 与 GROUP BY 组合', async () => {
const db = new MetonaSqlark({ name: uniqueName('sd'), mode: 'memory' });
await db.init();
await db.defineTable('t', { id: { type: 'string', primaryKey: true }, g: { type: 'string' }, v: { type: 'number' } });
await db.query("INSERT INTO t VALUES ('1', 'x', 10), ('2', 'x', 10), ('3', 'y', 20)");
const rows = await db.query('SELECT g, SUM(DISTINCT v) AS s FROM t GROUP BY g ORDER BY g') as Record<string, unknown>[];
expect(rows).toEqual([{ g: 'x', s: 10 }, { g: 'y', s: 20 }]);
await db.close();
});
test('NULLS FIRST 排序', async () => {
const db = new MetonaSqlark({ name: uniqueName('nf'), mode: 'memory' });
await db.init();
await db.defineTable('t', { id: { type: 'string', primaryKey: true }, v: { type: 'number' } });
await db.query("INSERT INTO t VALUES ('1', 10), ('2', NULL), ('3', 20)");
const rows = await db.query('SELECT v FROM t ORDER BY v ASC NULLS FIRST') as Record<string, unknown>[];
expect(rows).toEqual([{ v: null }, { v: 10 }, { v: 20 }]);
await db.close();
});
test('NULLS LAST 排序(降序时 NULL 排最后)', async () => {
const db = new MetonaSqlark({ name: uniqueName('nl'), mode: 'memory' });
await db.init();
await db.defineTable('t', { id: { type: 'string', primaryKey: true }, v: { type: 'number' } });
await db.query("INSERT INTO t VALUES ('1', 10), ('2', NULL), ('3', 20)");
const rows = await db.query('SELECT v FROM t ORDER BY v DESC NULLS LAST') as Record<string, unknown>[];
expect(rows).toEqual([{ v: 20 }, { v: 10 }, { v: null }]);
await db.close();
});
test('字符串常量列 + \'\' 转义(无 FROM 查询)', async () => {
const db = new MetonaSqlark({ name: uniqueName('lit'), mode: 'memory' });
await db.init();
const rows = await db.query("SELECT 'it''s a test' AS escaped") as Record<string, unknown>[];
expect(rows).toEqual([{ escaped: "it's a test" }]);
const mixed = await db.query("SELECT 'hello' AS greeting, 'world' AS subject") as Record<string, unknown>[];
expect(mixed).toEqual([{ greeting: 'hello', subject: 'world' }]);
await db.close();
});
test('ORDER BY / GROUP BY 支持表前缀列(u.name / o.amount', async () => {
const db = new MetonaSqlark({ name: uniqueName('prefix'), mode: 'memory' });
await db.init();
await db.defineTable('users', { id: { type: 'string', primaryKey: true }, name: { type: 'string' }, age: { type: 'number' } });
await db.defineTable('orders', { id: { type: 'string', primaryKey: true }, user_id: { type: 'string' }, amount: { type: 'number' } });
await db.query("INSERT INTO users VALUES ('1', 'Alice', 30), ('2', 'Bob', 25)");
await db.query("INSERT INTO orders VALUES ('o1', '1', 1200), ('o2', '2', 50)");
// JOIN + ORDER BY 表前缀(演示页报错场景)
const join = await db.query(
'SELECT u.name, o.amount FROM users u INNER JOIN orders o ON u.id = o.user_id ORDER BY o.amount DESC',
) as Record<string, unknown>[];
expect(join.map((r) => r['u.name'])).toEqual(['Alice', 'Bob']);
// JOIN + GROUP BY 表前缀
const grouped = await db.query(
'SELECT u.name, SUM(o.amount) AS total FROM users u INNER JOIN orders o ON u.id = o.user_id GROUP BY u.name',
) as Record<string, unknown>[];
expect(grouped.map((r) => r.total).sort((a, b) => Number(b) - Number(a))).toEqual([1200, 50]);
// 非 JOIN + ORDER BY / GROUP BY 表前缀
const plain = await db.query('SELECT name FROM users ORDER BY users.age DESC') as Record<string, unknown>[];
expect(plain.map((r) => r.name)).toEqual(['Alice', 'Bob']);
const byAge = await db.query('SELECT users.age, COUNT(*) AS n FROM users GROUP BY users.age') as Record<string, unknown>[];
expect(byAge.length).toBe(2);
await db.close();
});
test('INSERT ... SELECT 按源表列顺序映射(缺列不填,不报类型错)', async () => {
const db = new MetonaSqlark({ name: uniqueName('insel'), mode: 'memory' });
await db.init();
await db.defineTable('users', { id: { type: 'string', primaryKey: true }, name: { type: 'string', required: true }, email: { type: 'string' }, age: { type: 'number', default: 0 } });
await db.table('users').insertMany([{ id: '1', name: 'Alice', email: 'a@x.com', age: 30 }]);
// alter 预设:ADD 列 → 显式列名插入(无 email)→ DROP 列,产生行键缺 email 的行
await db.query('ALTER TABLE users ADD COLUMN phone STRING');
await db.query("INSERT INTO users (id, name, age, phone) VALUES ('6', 'Frank', 33, '123')");
await db.query('ALTER TABLE users DROP COLUMN phone');
await db.query('CREATE TABLE users_backup (id STRING PRIMARY KEY, name STRING, email STRING, age NUMBER)');
await db.query('INSERT INTO users_backup SELECT * FROM users');
const rows = await db.query('SELECT * FROM users_backup ORDER BY id') as Record<string, unknown>[];
expect(rows).toHaveLength(2);
expect(rows[0]).toEqual({ id: '1', name: 'Alice', email: 'a@x.com', age: 30 });
expect(rows[1].id).toBe('6');
expect(rows[1].name).toBe('Frank');
expect(rows[1].age).toBe(33);
// SELECT 指定列映射
await db.query('CREATE TABLE names_only (id STRING PRIMARY KEY, name STRING)');
await db.query('INSERT INTO names_only SELECT id, name FROM users');
const names = await db.query('SELECT * FROM names_only') as Record<string, unknown>[];
expect(names.map((r) => r.name).sort()).toEqual(['Alice', 'Frank']);
await db.close();
});
test('关联 EXISTS / NOT EXISTSSELECT 列仅含 name 时绑定主键仍生效)', async () => {
const db = new MetonaSqlark({ name: uniqueName('exists'), mode: 'memory' });
await db.init();
await db.defineTable('users', { id: { type: 'string', primaryKey: true }, name: { type: 'string' }, age: { type: 'number' } });
await db.defineTable('orders', { id: { type: 'string', primaryKey: true }, user_id: { type: 'string' }, amount: { type: 'number' } });
await db.query("INSERT INTO users VALUES ('1', 'Alice', 30), ('2', 'Bob', 25), ('3', 'Eve', 22)");
await db.query("INSERT INTO orders VALUES ('o1', '1', 100), ('o2', '1', 200), ('o3', '2', 50)");
// SELECT 只投影 name(不含 id),EXISTS 绑定 u.id 必须仍工作
const withOrders = await db.query(
'SELECT u.name FROM users u WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id)',
) as Record<string, unknown>[];
expect(withOrders.map((r) => r.name).sort()).toEqual(['Alice', 'Bob']);
const without = await db.query(
'SELECT u.name FROM users u WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id)',
) as Record<string, unknown>[];
expect(without.map((r) => r.name)).toEqual(['Eve']);
// SELECT 列带表前缀 + EXISTS 组合
const prefixed = await db.query(
'SELECT u.name FROM users u WHERE u.age > 21 AND EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id)',
) as Record<string, unknown>[];
expect(prefixed.map((r) => r.name).sort()).toEqual(['Alice', 'Bob']);
await db.close();
});
test('HAVING 引用聚合表达式 + 标量子查询', async () => {
const db = new MetonaSqlark({ name: uniqueName('having'), mode: 'memory' });
await db.init();
await db.defineTable('users', { id: { type: 'string', primaryKey: true }, name: { type: 'string' } });
await db.defineTable('orders', { id: { type: 'string', primaryKey: true }, user_id: { type: 'string' }, amount: { type: 'number' } });
await db.query("INSERT INTO users VALUES ('1', 'Alice'), ('2', 'Bob'), ('3', 'Charlie')");
await db.query("INSERT INTO orders VALUES ('o1', '1', 1200), ('o2', '1', 50), ('o3', '2', 150), ('o4', '3', 400), ('o5', '3', 20)");
// HAVING SUM(...) > (SELECT AVG(...)):均值 364Alice 1250 / Charlie 420 达标
const rows = await db.query(
'SELECT u.name, SUM(o.amount) AS spent FROM users u INNER JOIN orders o ON u.id = o.user_id GROUP BY u.name HAVING SUM(o.amount) > (SELECT AVG(amount) FROM orders)',
) as Record<string, unknown>[];
expect(rows.map((r) => [r['u.name'], r.spent])).toEqual([['Alice', 1250], ['Charlie', 420]]);
// HAVING 引用别名也生效
const byAlias = await db.query(
'SELECT u.name, SUM(o.amount) AS spent FROM users u INNER JOIN orders o ON u.id = o.user_id GROUP BY u.name HAVING spent > 400',
) as Record<string, unknown>[];
expect(byAlias.map((r) => r['u.name']).sort()).toEqual(['Alice', 'Charlie']);
await db.close();
});
test('JOIN 主表 WHERE 条件下推走索引 + DROP INDEX 报错', async () => {
const db = new MetonaSqlark({ name: uniqueName('idx'), mode: 'memory' });
await db.init();
await db.defineTable('users', { id: { type: 'string', primaryKey: true }, name: { type: 'string' } });
await db.defineTable('orders', { id: { type: 'string', primaryKey: true }, user_id: { type: 'string' }, product: { type: 'string' } });
await db.query("INSERT INTO users VALUES ('1', 'Alice'), ('2', 'Bob')");
await db.query("INSERT INTO orders VALUES ('o1', '1', 'Laptop'), ('o2', '1', 'Mouse'), ('o3', '2', 'Keyboard')");
await db.query('CREATE INDEX idx_orders_user ON orders (user_id)');
// WHERE 主表前缀条件下推:引擎收到的 orders 查询 where 为 { user_id: { $eq: '1' } }
const eng = db.getEngine() as { find: (...a: unknown[]) => Promise<unknown> };
const origFind = eng.find.bind(eng);
let pushed: unknown = null;
eng.find = async (t: string, q: { where?: unknown }) => {
if (t === 'orders' && q?.where) pushed = q.where;
return origFind(t, q);
};
const rows = await db.query(
"SELECT u.name, o.product FROM orders o JOIN users u ON u.id = o.user_id WHERE o.user_id = '1'",
) as Record<string, unknown>[];
expect(rows.map((r) => [r['u.name'], r['o.product']])).toEqual([['Alice', 'Laptop'], ['Alice', 'Mouse']]);
expect(pushed).toEqual({ user_id: { $eq: '1' } });
// DROP 存在的索引成功;再次 DROP(列已无索引)报 INDEX_NOT_FOUND
await db.query('DROP INDEX idx_orders_user ON orders (user_id)');
await expect(db.query('DROP INDEX idx_nonexist ON orders (user_id)')).rejects.toThrow('Index on column');
// DROP 后回退全表扫描,结果不变
const after = await db.query("SELECT product FROM orders WHERE user_id = '2'") as Record<string, unknown>[];
expect(after.map((r) => r.product)).toEqual(['Keyboard']);
await db.close();
});
test('裸布尔列条件:WHERE done / CASE WHEN done(真值判断)', async () => {
const db = new MetonaSqlark({ name: uniqueName('bool'), mode: 'memory' });
await db.init();
await db.defineTable('tasks', { id: { type: 'string', primaryKey: true }, title: { type: 'string' }, done: { type: 'boolean', default: false } });
await db.query("INSERT INTO tasks VALUES ('t1', 'A', false), ('t2', 'B', true), ('t3', 'C', false)");
// WHERE 裸列
const done = await db.query('SELECT id FROM tasks WHERE done') as Record<string, unknown>[];
expect(done.map((r) => r.id)).toEqual(['t2']);
// CASE WHEN 裸列(演示页 aria 预设)
const labeled = await db.query(
"SELECT title, CASE WHEN done THEN 'done' ELSE 'pending' END AS status FROM tasks",
) as Record<string, unknown>[];
expect(labeled.find((r) => r.title === 'B')?.status).toBe('done');
expect(labeled.find((r) => r.title === 'A')?.status).toBe('pending');
await db.close();
});
test('Aria $in 查询去重(IN 子查询含重复值不返回重复行)', async () => {
const db = new MetonaSqlark({ name: uniqueName('indup'), mode: 'aria', diskEngine: 'indexeddb' });
await db.init();
await db.getEngine().clearAll();
await db.defineTable('users', { id: { type: 'string', primaryKey: true }, name: { type: 'string' } });
await db.defineTable('orders', { id: { type: 'string', primaryKey: true }, user_id: { type: 'string' } });
await db.query("INSERT INTO users VALUES ('1', 'Alice'), ('2', 'Bob')");
await db.query("INSERT INTO orders VALUES ('o1', '1'), ('o2', '1')");
const rows = await db.query('SELECT name FROM users WHERE id IN (SELECT user_id FROM orders)') as Record<string, unknown>[];
expect(rows).toHaveLength(1);
expect(rows[0].name).toBe('Alice');
await db.close();
});
});