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,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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user