/** * 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[]; 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[]; expect(visible).toHaveLength(5); // 事务内可见 await db.query('COMMIT'); const after = await db.query('SELECT * FROM users') as Record[]; 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[]; 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[]; 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[]; 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[]; 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[]; 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[]; // 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[]; 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[]; // 左右各 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[]; // 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[]; 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[]; 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[]; 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[]; 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[]; 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[]; 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[]; 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[]; // 有订单的用户: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[]; // 无订单用户: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[]; // 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[]; expect(rows).toHaveLength(2); await db.close(); }); });