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% 覆盖率
This commit is contained in:
@@ -0,0 +1,372 @@
|
||||
/**
|
||||
* 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, Carol(Alice 同时 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 属于 u1(Beijing)
|
||||
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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user