Files
MetonaSqlark/tests/sql-ext.test.ts
T
thzxx d544501e1c
CI / test (18.x) (push) Failing after 5m11s
CI / test (20.x) (push) Failing after 5m8s
CI / test (22.x) (push) Successful in 9m58s
CI / test (24.x) (push) Successful in 9m56s
release: v0.3.2 — 质量加固 + SQL扩展 + 表达式 + 并发同步
v0.2.6 质量加固:
- 修复 AriaEngine 二级索引 SSTable 互相覆盖(命名空间隔离)
- 修复 LSM 多版本读取顺序错误 + MergeIterator 取最新来源
- 重写 LZ4 压缩器(往返一致性 + 缓冲区溢出)
- sstableCache LRU 上限 + 预加载兜底(BufferPool 配置生效)
- 修复 React/Vue 集成 import type 运行时 bug + exports 子路径
- 新增 38 个测试(LZ4往返/Crypto/集成), 删除伪测试

v0.3.0 SQL 功能扩展:
- 多语句 parseAll + 事务语句 BEGIN/COMMIT/ROLLBACK
- INSERT INTO ... SELECT + UNION/UNION ALL + EXISTS 关联子查询
- CREATE/DROP INDEX 五引擎实现 + 别名 WHERE 修复
- benchmark 页面 + 36 个新测试

v0.3.1 表达式与性能:
- CASE WHEN 表达式(SELECT 列/WHERE/聚合)
- JOIN + 关联子查询逐行绑定
- WAL 批量组提交(写放大 O(N)→O(1))
- 修复 pending frozen 可见性 + flush 缓存竞争

v0.3.2 并发:
- CASE WHEN 用于 WHERE/聚合 + JOIN 哈希连接
- 多标签页同步(multiTabSync + BroadcastChannel)
- IndexedDB schema 持久化(reopen 后表结构恢复)
- 修复 where-matcher 顶层 $not
- 修复 CJS 产物 .js 被 ESM 解析(exports 空) — .cjs 后缀 + exports 修正
- 836 测试 / 44 套件 / 81.0% 覆盖率
2026-08-08 10:41:30 +08:00

373 lines
15 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.3.0 SQL 功能扩展测试
* @module tests/sql-ext
*
* 覆盖:多语句 / 事务语句 / INSERT...SELECT / UNION / CREATE INDEX / EXISTS
*/
import 'fake-indexeddb/auto';
import { MetonaSqlark } from '../src/core';
import '../src/connection-manager';
import { parse, parseAll } from '../src/sql/parser';
async function createDb(mode: 'memory' | 'disk' | 'aria' = 'memory') {
const db = new MetonaSqlark({ name: `sql-ext-${mode}-${Date.now()}-${Math.random()}`, mode, diskEngine: 'indexeddb' });
await db.init();
await db.defineTable('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string' },
age: { type: 'number' },
city: { 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 ('u1', 'Alice', 30, 'Beijing')`);
await db.query(`INSERT INTO users VALUES ('u2', 'Bob', 25, 'Shanghai')`);
await db.query(`INSERT INTO users VALUES ('u3', 'Carol', 35, 'Beijing')`);
await db.query(`INSERT INTO users VALUES ('u4', 'Dave', 28, 'Shenzhen')`);
await db.query(`INSERT INTO orders VALUES ('o1', 'u1', 100)`);
await db.query(`INSERT INTO orders VALUES ('o2', 'u1', 200)`);
await db.query(`INSERT INTO orders VALUES ('o3', 'u2', 50)`);
return db;
}
// ===================================================================
// 多语句 parseAll
// ===================================================================
describe('[v0.3.0] 多语句支持', () => {
test('parseAll 解析分号分隔的多条语句', () => {
const stmts = parseAll('SELECT * FROM a; INSERT INTO b VALUES (1); DELETE FROM c WHERE id = 1');
expect(stmts).toHaveLength(3);
expect(stmts[0].type).toBe('SELECT');
expect(stmts[1].type).toBe('INSERT');
expect(stmts[2].type).toBe('DELETE');
});
test('parseAll 忽略多余分号与尾部空语句', () => {
const stmts = parseAll(';;SELECT * FROM a;;;');
expect(stmts).toHaveLength(1);
expect(stmts[0].type).toBe('SELECT');
});
test('parseAll 支持事务语句', () => {
const stmts = parseAll('BEGIN; INSERT INTO a VALUES (1); COMMIT');
expect(stmts.map((s) => s.type)).toEqual(['BEGIN', 'INSERT', 'COMMIT']);
});
test('parse 保持单语句兼容', () => {
expect(parse('SELECT * FROM a').type).toBe('SELECT');
});
test('parseAll 语句间缺分号报错', () => {
expect(() => parseAll('SELECT * FROM a SELECT * FROM b')).toThrow(/Expected ';'/);
});
test('core.query 顺序执行多语句', async () => {
const db = await createDb();
await db.query('CREATE TABLE logs (id STRING PRIMARY KEY, msg STRING); INSERT INTO logs VALUES (\'l1\', \'hello\'); INSERT INTO logs VALUES (\'l2\', \'world\')');
const rows = await db.query('SELECT * FROM logs') as Record<string, unknown>[];
expect(rows).toHaveLength(2);
await db.close();
});
});
// ===================================================================
// 事务语句 BEGIN / COMMIT / ROLLBACK
// ===================================================================
describe('[v0.3.0] 事务语句', () => {
test('BEGIN + INSERT + COMMIT 持久化', async () => {
const db = await createDb();
await db.query('BEGIN');
await db.query(`INSERT INTO users VALUES ('u5', 'Eve', 22, 'Beijing')`);
const visible = await db.query('SELECT * FROM users') as Record<string, unknown>[];
expect(visible).toHaveLength(5); // 事务内可见
await db.query('COMMIT');
const after = await db.query('SELECT * FROM users') as Record<string, unknown>[];
expect(after).toHaveLength(5);
await db.close();
});
test('BEGIN + INSERT + ROLLBACK 回滚', async () => {
const db = await createDb();
await db.query('BEGIN');
await db.query(`INSERT INTO users VALUES ('u5', 'Eve', 22, 'Beijing')`);
await db.query('ROLLBACK');
const after = await db.query('SELECT * FROM users') as Record<string, unknown>[];
expect(after).toHaveLength(4);
await db.close();
});
test('BEGIN 嵌套报错', async () => {
const db = await createDb();
await db.query('BEGIN');
await expect(db.query('BEGIN')).rejects.toThrow(/TX_ACTIVE|Transaction already/);
await db.query('ROLLBACK');
await db.close();
});
test('无事务时 COMMIT 报错', async () => {
const db = await createDb();
await expect(db.query('COMMIT')).rejects.toThrow(/TX_NONE|No active transaction/);
await db.close();
});
test('事务语句在 Aria 引擎上可用', async () => {
const db = await createDb('aria');
await db.query('BEGIN');
await db.query(`INSERT INTO users VALUES ('u5', 'Eve', 22, 'Beijing')`);
await db.query('ROLLBACK');
const after = await db.query('SELECT * FROM users') as Record<string, unknown>[];
expect(after).toHaveLength(4);
await db.close();
});
});
// ===================================================================
// INSERT INTO ... SELECT
// ===================================================================
describe('[v0.3.0] INSERT INTO ... SELECT', () => {
test('INSERT SELECT 全列复制', async () => {
const db = await createDb();
await db.defineTable('users_backup', {
id: { type: 'string', primaryKey: true },
name: { type: 'string' },
age: { type: 'number' },
city: { type: 'string' },
});
await db.query('INSERT INTO users_backup SELECT * FROM users');
const rows = await db.query('SELECT * FROM users_backup') as Record<string, unknown>[];
expect(rows).toHaveLength(4);
expect(rows[0].name).toBe('Alice');
await db.close();
});
test('INSERT SELECT 带 WHERE 过滤', async () => {
const db = await createDb();
await db.defineTable('beijing_users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string' },
age: { type: 'number' },
city: { type: 'string' },
});
await db.query(`INSERT INTO beijing_users SELECT * FROM users WHERE city = 'Beijing'`);
const rows = await db.query('SELECT * FROM beijing_users') as Record<string, unknown>[];
expect(rows).toHaveLength(2);
await db.close();
});
test('INSERT SELECT 指定列映射', async () => {
const db = await createDb();
await db.defineTable('names', { id: { type: 'string', primaryKey: true }, n: { type: 'string' } });
await db.query('INSERT INTO names (id, n) SELECT id, name FROM users');
const rows = await db.query('SELECT * FROM names') as Record<string, unknown>[];
expect(rows).toHaveLength(4);
expect(rows[0].n).toBe('Alice');
await db.close();
});
test('INSERT SELECT 语法解析', () => {
const stmt = parse('INSERT INTO a (x, y) SELECT p, q FROM b WHERE r > 1') as any;
expect(stmt.type).toBe('INSERT');
expect(stmt.select).toBeDefined();
expect(stmt.select.type).toBe('SELECT');
expect(stmt.columns).toEqual(['x', 'y']);
});
});
// ===================================================================
// UNION / UNION ALL
// ===================================================================
describe('[v0.3.0] UNION / UNION ALL', () => {
test('UNION 去重合并', async () => {
const db = await createDb();
const rows = await db.query(`SELECT name FROM users WHERE city = 'Beijing' UNION SELECT name FROM users WHERE age < 30`) as Record<string, unknown>[];
// Beijing: Alice, CarolAlice 同时 age30 不重复);age<30: Bob, Dave
expect(rows).toHaveLength(4);
await db.close();
});
test('UNION ALL 不去重', async () => {
const db = await createDb();
const rows = await db.query(`SELECT city FROM users WHERE city = 'Beijing' UNION ALL SELECT city FROM users WHERE city = 'Beijing'`) as Record<string, unknown>[];
expect(rows).toHaveLength(4); // 2 + 2
await db.close();
});
test('UNION 对相同行去重', async () => {
const db = await createDb();
const rows = await db.query(`SELECT city FROM users WHERE city = 'Beijing' UNION SELECT city FROM users WHERE city = 'Beijing'`) as Record<string, unknown>[];
// 左右各 2 行同值 → 去重后 1 行
expect(rows).toHaveLength(1);
expect(rows[0].city).toBe('Beijing');
await db.close();
});
test('UNION 链式(三表合并)', async () => {
const db = await createDb();
await db.query(`INSERT INTO users VALUES ('u5', 'Eve', 22, 'Beijing')`);
const rows = await db.query(`SELECT city FROM users WHERE city = 'Beijing' UNION SELECT city FROM users WHERE city = 'Shanghai' UNION SELECT city FROM users WHERE city = 'Beijing'`) as Record<string, unknown>[];
// Beijing(3) + Shanghai(1) + Beijing(3 重复去重) → 2 个城市
expect(rows).toHaveLength(2);
await db.close();
});
test('UNION 语法解析', () => {
const stmt = parse('SELECT a FROM t1 UNION SELECT b FROM t2') as any;
expect(stmt.type).toBe('SELECT_UNION');
expect(stmt.all).toBeUndefined();
const stmtAll = parse('SELECT a FROM t1 UNION ALL SELECT b FROM t2') as any;
expect(stmtAll.type).toBe('SELECT_UNION');
expect(stmtAll.all).toBe(true);
});
test('UNION 在 Aria 引擎上可用', async () => {
const db = await createDb('aria');
const rows = await db.query(`SELECT city FROM users WHERE city = 'Beijing' UNION SELECT city FROM users WHERE city = 'Beijing'`) as Record<string, unknown>[];
expect(rows).toHaveLength(1);
await db.close();
});
});
// ===================================================================
// CREATE INDEX / DROP INDEX
// ===================================================================
describe('[v0.3.0] CREATE INDEX / DROP INDEX', () => {
test('CREATE INDEX 后索引查询可用(Memory', async () => {
const db = await createDb();
await db.query('CREATE INDEX idx_users_city ON users (city)');
const rows = await db.query(`SELECT * FROM users WHERE city = 'Beijing'`) as Record<string, unknown>[];
expect(rows).toHaveLength(2);
await db.close();
});
test('CREATE INDEX 后索引查询可用(Aria', async () => {
const db = await createDb('aria');
await db.query('CREATE INDEX idx_users_city ON users (city)');
const rows = await db.query(`SELECT * FROM users WHERE city = 'Beijing'`) as Record<string, unknown>[];
expect(rows).toHaveLength(2);
await db.close();
});
test('CREATE INDEX 对已有数据立即生效(索引填充)', async () => {
const db = await createDb();
await db.query('INSERT INTO users VALUES (\'u5\', \'Eve\', 22, \'Beijing\')');
await db.query('CREATE INDEX idx_users_age ON users (age)');
const rows = await db.query(`SELECT * FROM users WHERE age = 22`) as Record<string, unknown>[];
expect(rows).toHaveLength(1);
expect(rows[0].id).toBe('u5');
await db.close();
});
test('DROP INDEX 后查询回退全表扫描', async () => {
const db = await createDb();
await db.query('CREATE INDEX idx_users_city ON users (city)');
await db.query('DROP INDEX idx_users_city ON users (city)');
const rows = await db.query(`SELECT * FROM users WHERE city = 'Beijing'`) as Record<string, unknown>[];
expect(rows).toHaveLength(2);
await db.close();
});
test('CREATE INDEX 重复创建幂等', async () => {
const db = await createDb();
await db.query('CREATE INDEX idx_users_city ON users (city)');
await db.query('CREATE INDEX idx_users_city ON users (city)'); // 不抛错
const rows = await db.query(`SELECT * FROM users WHERE city = 'Beijing'`) as Record<string, unknown>[];
expect(rows).toHaveLength(2);
await db.close();
});
test('CREATE INDEX 不存在列报错', async () => {
const db = await createDb();
const err = await db.query('CREATE INDEX idx_bad ON users (nonexistent)').catch((e: any) => e);
expect(err).toBeDefined();
expect(err.code).toBe('COLUMN_NOT_FOUND');
await db.close();
});
test('CREATE INDEX 后插入新数据索引同步更新', async () => {
const db = await createDb();
await db.query('CREATE INDEX idx_users_city ON users (city)');
await db.query('INSERT INTO users VALUES (\'u5\', \'Eve\', 22, \'Beijing\')');
const rows = await db.query(`SELECT * FROM users WHERE city = 'Beijing'`) as Record<string, unknown>[];
expect(rows).toHaveLength(3);
await db.close();
});
test('DROP INDEX Aria 主键索引受保护', async () => {
const db = await createDb('aria');
await expect(db.query('DROP INDEX pk ON users (id)')).rejects.toThrow(/Cannot drop primary key/);
await db.close();
});
test('CREATE UNIQUE INDEX 语法解析', () => {
const stmt = parse('CREATE UNIQUE INDEX idx_u ON users (email)') as any;
expect(stmt.type).toBe('CREATE_INDEX');
expect(stmt.unique).toBe(true);
expect(stmt.column).toBe('email');
});
test('CREATE INDEX / DROP INDEX 语法解析', () => {
const stmt = parse('CREATE INDEX idx_c ON users (city)') as any;
expect(stmt).toMatchObject({ type: 'CREATE_INDEX', name: 'idx_c', table: 'users', column: 'city' });
const drop = parse('DROP INDEX idx_c ON users (city)') as any;
expect(drop).toMatchObject({ type: 'DROP_INDEX', name: 'idx_c', table: 'users', column: 'city' });
});
});
// ===================================================================
// EXISTS / NOT EXISTS
// ===================================================================
describe('[v0.3.0] EXISTS / NOT EXISTS', () => {
test('EXISTS 子查询为真返回行', async () => {
const db = await createDb();
const rows = await db.query(`SELECT * FROM users u WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id)`) as Record<string, unknown>[];
// 有订单的用户:u1, u2
expect(rows).toHaveLength(2);
expect(rows.map((r) => r.id).sort()).toEqual(['u1', 'u2']);
await db.close();
});
test('NOT EXISTS 返回无关联行', async () => {
const db = await createDb();
const rows = await db.query(`SELECT * FROM users u WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id)`) as Record<string, unknown>[];
// 无订单用户:u3, u4
expect(rows).toHaveLength(2);
expect(rows.map((r) => r.id).sort()).toEqual(['u3', 'u4']);
await db.close();
});
test('EXISTS 与 AND 组合', async () => {
const db = await createDb();
const rows = await db.query(`SELECT * FROM users u WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id AND o.amount > 150) AND u.city = 'Beijing'`) as Record<string, unknown>[];
// o2 金额 200 > 150 属于 u1Beijing
expect(rows).toHaveLength(1);
expect(rows[0].id).toBe('u1');
await db.close();
});
test('EXISTS 语法解析', () => {
const stmt = parse(`SELECT * FROM users WHERE EXISTS (SELECT 1 FROM orders)`) as any;
expect(stmt.where.$exists).toBeDefined();
expect(stmt.where.$exists.$subquery.type).toBe('SELECT');
const notStmt = parse(`SELECT * FROM users WHERE NOT EXISTS (SELECT 1 FROM orders)`) as any;
expect(notStmt.where.$exists).toBeDefined();
expect(notStmt.where.$exists.$negate).toBe(true);
});
test('EXISTS 在 Aria 引擎上可用', async () => {
const db = await createDb('aria');
const rows = await db.query(`SELECT * FROM users u WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id)`) as Record<string, unknown>[];
expect(rows).toHaveLength(2);
await db.close();
});
});