Files
MetonaSqlark/tests/aria-cascade.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

200 lines
9.8 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.1 测试 — AriaEngine 外键级联 + clearAll 重置
*/
import 'fake-indexeddb/auto';
import { AriaEngine } from '../src/engine/aria/index';
const mkEngine = async (name: string): Promise<AriaEngine> => {
const e = new AriaEngine({ storageBackend: 'indexeddb', walSyncMode: 'full' });
await e.open(`cascade-${name}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, 1);
return e;
};
describe('[v0.4.1] AriaEngine 外键级联', () => {
test('CASCADE:删除主表行时级联删除引用行', async () => {
const e = await mkEngine('cas');
await e.createTable({ name: 'users', columns: { id: { type: 'string', primaryKey: true } } });
await e.createTable({ name: 'orders', columns: { id: { type: 'string', primaryKey: true }, user_id: { type: 'string', references: 'users.id', onDelete: 'CASCADE' } } });
await e.insert('users', [{ id: 'u1' }, { id: 'u2' }]);
await e.insert('orders', [{ id: 'o1', user_id: 'u1' }, { id: 'o2', user_id: 'u1' }, { id: 'o3', user_id: 'u2' }]);
const count = await e.delete('users', { table: 'users', where: { id: 'u1' } });
expect(count).toBe(3); // u1 + o1 + o2
expect(await e.count('users')).toBe(1);
expect(await e.count('orders')).toBe(1);
const rest = await e.find('orders', { table: 'orders' });
expect(rest[0].user_id).toBe('u2');
await e.close();
});
test('CASCADE:多层递归(users → orders → order_items', async () => {
const e = await mkEngine('cas2');
await e.createTable({ name: 'users', columns: { id: { type: 'string', primaryKey: true } } });
await e.createTable({ name: 'orders', columns: { id: { type: 'string', primaryKey: true }, user_id: { type: 'string', references: 'users.id', onDelete: 'CASCADE' } } });
await e.createTable({ name: 'order_items', columns: { id: { type: 'string', primaryKey: true }, order_id: { type: 'string', references: 'orders.id', onDelete: 'CASCADE' } } });
await e.insert('users', [{ id: 'u1' }]);
await e.insert('orders', [{ id: 'o1', user_id: 'u1' }]);
await e.insert('order_items', [{ id: 'i1', order_id: 'o1' }, { id: 'i2', order_id: 'o1' }]);
const count = await e.delete('users', { table: 'users', where: { id: 'u1' } });
expect(count).toBe(4); // u1 + o1 + i1 + i2
expect(await e.count('order_items')).toBe(0);
await e.close();
});
test('SET NULL:删除主表行时引用列置 null', async () => {
const e = await mkEngine('sn');
await e.createTable({ name: 'users', columns: { id: { type: 'string', primaryKey: true } } });
await e.createTable({ name: 'orders', columns: { id: { type: 'string', primaryKey: true }, user_id: { type: 'string', references: 'users.id', onDelete: 'SET NULL' } } });
await e.insert('users', [{ id: 'u1' }]);
await e.insert('orders', [{ id: 'o1', user_id: 'u1' }]);
const count = await e.delete('users', { table: 'users', where: { id: 'u1' } });
expect(count).toBe(1); // 仅 u1,引用行保留
const rows = await e.find('orders', { table: 'orders' });
expect(rows).toHaveLength(1);
expect(rows[0].user_id).toBeNull();
await e.close();
});
test('RESTRICT:存在引用行时禁止删除', async () => {
const e = await mkEngine('res');
await e.createTable({ name: 'users', columns: { id: { type: 'string', primaryKey: true } } });
await e.createTable({ name: 'orders', columns: { id: { type: 'string', primaryKey: true }, user_id: { type: 'string', references: 'users.id', onDelete: 'RESTRICT' } } });
await e.insert('users', [{ id: 'u1' }]);
await e.insert('orders', [{ id: 'o1', user_id: 'u1' }]);
await expect(e.delete('users', { table: 'users', where: { id: 'u1' } })).rejects.toThrow('Cannot delete');
// 数据未被删除
expect(await e.count('users')).toBe(1);
expect(await e.count('orders')).toBe(1);
await e.close();
});
test('CASCADE 同时清理二级索引(删除后按索引查不到)', async () => {
const e = await mkEngine('cidx');
await e.createTable({ name: 'users', columns: { id: { type: 'string', primaryKey: true } } });
await e.createTable({ name: 'orders', columns: { id: { type: 'string', primaryKey: true }, user_id: { type: 'string', index: true, references: 'users.id', onDelete: 'CASCADE' } } });
await e.insert('users', [{ id: 'u1' }]);
await e.insert('orders', [{ id: 'o1', user_id: 'u1' }]);
await e.delete('users', { table: 'users', where: { id: 'u1' } });
const rows = await e.find('orders', { table: 'orders', where: { user_id: 'u1' } });
expect(rows).toHaveLength(0);
await e.close();
});
test('事务内级联删除可提交/回滚', async () => {
const e = await mkEngine('ctx');
await e.createTable({ name: 'users', columns: { id: { type: 'string', primaryKey: true } } });
await e.createTable({ name: 'orders', columns: { id: { type: 'string', primaryKey: true }, user_id: { type: 'string', references: 'users.id', onDelete: 'CASCADE' } } });
await e.insert('users', [{ id: 'u1' }]);
await e.insert('orders', [{ id: 'o1', user_id: 'u1' }]);
// rollback:级联删除被回滚
await e.beginTransaction();
await e.delete('users', { table: 'users', where: { id: 'u1' } });
await e.rollbackTransaction();
expect(await e.count('users')).toBe(1);
expect(await e.count('orders')).toBe(1);
// commit:级联删除生效
await e.beginTransaction();
await e.delete('users', { table: 'users', where: { id: 'u1' } });
await e.commitTransaction();
expect(await e.count('users')).toBe(0);
expect(await e.count('orders')).toBe(0);
await e.close();
});
});
describe('[v0.4.1] AriaEngine clearAll', () => {
test('clearAll 清空全部表与数据,实例可继续使用', async () => {
const e = await mkEngine('clr');
await e.createTable({ name: 'users', columns: { id: { type: 'string', primaryKey: true }, name: { type: 'string', index: true } } });
await e.insert('users', [{ id: '1', name: 'Alice' }]);
await e.clearAll();
expect(await e.getTableNames()).toHaveLength(0);
// 实例可继续建表使用
await e.createTable({ name: 't2', columns: { id: { type: 'string', primaryKey: true } } });
await e.insert('t2', [{ id: 'x' }]);
expect(await e.count('t2')).toBe(1);
await e.close();
});
test('clearAll 后重启(模拟刷新)无残留数据', async () => {
const name = `clr2-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
let e = new AriaEngine({ storageBackend: 'indexeddb', walSyncMode: 'full' });
await e.open(name, 1);
await e.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true } } });
await e.insert('t', [{ id: '1' }]);
await e.clearAll();
await e.close();
// 重新打开(模拟页面刷新):不应有残留表
e = new AriaEngine({ storageBackend: 'indexeddb', walSyncMode: 'full' });
await e.open(name, 1);
expect(await e.getTableNames()).toHaveLength(0);
await e.close();
});
});
describe('[v0.4.1] AriaEngine ALTER TABLE', () => {
test('DROP COLUMN 真正清除存储中的列值(find 副本不再残留)', async () => {
const e = await mkEngine('alt');
await e.createTable({ name: 'users', columns: { id: { type: 'string', primaryKey: true }, name: { type: 'string' }, email: { type: 'string' }, age: { type: 'number', default: 0 } } });
await e.insert('users', [{ id: '1', name: 'Alice', email: 'a@x.com', age: 30 }]);
await e.alterTable('users', 'ADD', { name: 'phone', type: 'string' });
await e.insert('users', [{ id: '2', name: 'Frank', age: 33, phone: '123' }]);
await e.alterTable('users', 'DROP', { name: 'phone', type: 'string' });
const rows = await e.find('users', { table: 'users' });
for (const row of rows) {
expect('phone' in row).toBe(false);
}
await e.close();
});
test('ALTER 持久化:重启后 schema 与行一致', async () => {
const name = `alt2-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
let e = new AriaEngine({ storageBackend: 'indexeddb', walSyncMode: 'full' });
await e.open(name, 1);
await e.createTable({ name: 'users', columns: { id: { type: 'string', primaryKey: true }, name: { type: 'string' } } });
await e.insert('users', [{ id: '1', name: 'Alice' }]);
await e.alterTable('users', 'ADD', { name: 'phone', type: 'string' });
await e.insert('users', [{ id: '2', name: 'Frank', phone: '123' }]);
await e.alterTable('users', 'DROP', { name: 'phone', type: 'string' });
await e.close();
e = new AriaEngine({ storageBackend: 'indexeddb', walSyncMode: 'full' });
await e.open(name, 1);
const schema = await e.getTableSchema('users');
expect(Object.keys(schema!.columns)).not.toContain('phone');
const rows = await e.find('users', { table: 'users' });
for (const row of rows) expect('phone' in row).toBe(false);
await e.close();
});
test('ALTER 模拟崩溃(不 close)重启:schema 与行一致', async () => {
const name = `alt3-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
let e = new AriaEngine({ storageBackend: 'indexeddb', walSyncMode: 'full' });
await e.open(name, 1);
await e.createTable({ name: 'users', columns: { id: { type: 'string', primaryKey: true }, name: { type: 'string' } } });
await e.insert('users', [{ id: '1', name: 'Alice' }]);
await e.alterTable('users', 'ADD', { name: 'phone', type: 'string' });
await e.insert('users', [{ id: '2', name: 'Frank', phone: '123' }]);
await e.alterTable('users', 'DROP', { name: 'phone', type: 'string' });
// 不 close,模拟崩溃
e = new AriaEngine({ storageBackend: 'indexeddb', walSyncMode: 'full' });
await e.open(name, 1);
const schema = await e.getTableSchema('users');
expect(Object.keys(schema!.columns)).not.toContain('phone');
const rows = await e.find('users', { table: 'users' });
for (const row of rows) expect('phone' in row).toBe(false);
await e.close();
});
});