release: v0.4.1 — Aria 级联/ALTER/clearAll + 流式查询/派生表 + 正确性加固
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

新增:
- 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:
thzxx
2026-08-08 13:36:34 +08:00
parent 82ad8e93b9
commit a1e4f5071c
38 changed files with 13493 additions and 8756 deletions
+199
View File
@@ -0,0 +1,199 @@
/**
* 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();
});
});