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% 覆盖率
This commit is contained in:
@@ -0,0 +1,382 @@
|
||||
/**
|
||||
* 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 匹配 b3(t1/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 EXISTS(SELECT 列仅含 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(...)):均值 364,Alice 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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user