工作流 C-1 / C-3 前半 + 测试代码类型检查。 【故障注入基座】新增 tests/helpers/storage-harness.ts + faulty-backend.ts - TransactionalFileStore:忠实 OPFS 提交语义(close 才可见)+ 字节级故障注入 (failNextWrite/Append/Delete、truncateAppendTo 撕裂写、crashPending 真崩溃) - 删除旧 opfs-mock:读返回内部引用、keepExistingData:false 不截断、close 空实现 导致"提交前可见"等真实缺陷无法被测出(31 个测试文件迁移至新 harness) - 删除 aria-opfs-backend 内的第三份重复 mock(含从未被断言使用的 writeCalls 死代码 与 entry.content.subarray 恒等分支) - FaultyBackend:包装任意 IStorageBackend 注入故障;crash() 明确区别于 close() (后者是优雅停机,会刷完写队列 —— 这正是此前所有"崩溃恢复"测试的真相) - 16 条基座自测证明注入真的生效(含 close 不能当崩溃的对照组) 【覆盖率口径】jest.config.cjs - 移除 '!src/**/index.ts'(该 glob 把 AriaEngine 主实现等 15 个实现文件整体 排除出统计,与 v0.2.6 曾承认过的问题同源),改为只排除纯类型声明文件并附理由 - 新增 coverageThreshold 门禁(此前完全不存在) - 真实基线:语句 90.66% / 分支 82.94% / 函数 94.36% / 行 93.43% - 修正 testMatch 使 tests/helpers 下的测试可被发现 【测试代码类型检查】tsconfig.test.json + npm run typecheck:tests - 修复 103 个测试代码类型错误(此前 babel 剥离类型 + tsconfig 排除 tests,全部隐藏) - 新增 tests/helpers/assertions.ts:nonNull/decode/rows/object/engineMethod/expectCode 以断言收窄替代 as any - 消除 21 个 lint warning(含 v043-hardening 中定义后从未调用的 mockOPFS 死代码) - parser.test.ts 12 处 toBeDefined() 空断言升级为结构断言(并新增 AND/OR 优先级用例, 当前红灯,对应总账第 11 项,将在工作流 A 修复) 【版本契约】新增 tests/version-contract.test.ts - 校验 src VERSION / package.json / dist 三者一致,替代两处硬编码版本字面量 【CI 门禁】.gitea/workflows/ci.yml - lint 去掉 continue-on-error(此前永远不让 CI 变红) - 新增 tests 类型检查、--coverage 覆盖率门禁、dist 与源码同步校验 - 版本 0.7.4 升至 0.8.0
423 lines
19 KiB
TypeScript
423 lines
19 KiB
TypeScript
/**
|
||
* v0.3.3 修复验证测试
|
||
* 验证所有 P0/P1 修复点:
|
||
* - P0-1: Aria WAL DROP_TABLE 崩溃恢复(表/数据不复活)+ WAL 恢复后截断
|
||
* - P0-2: MemoryEngine update/delete 索引维护(unique 约束 + 索引查询 + 级联清理)
|
||
* - P1-3: Aria 事务内读到自己写入的变更(insert 后 update/delete)
|
||
* - P1-4: Aria clear() 写 WAL + 事务化
|
||
* - P1-5: WAL 字节数跟踪(full 模式 walSizeThreshold 生效)
|
||
* - P1-6: Aria 主键列不建冗余二级索引(PK $in / 范围查询走主 LSM)
|
||
* - P1-7: ORDER BY 支持 SELECT 别名
|
||
* - P1-8: SQL 字符串 '' 标准转义
|
||
* - P1-9: Savepoint 回滚后 MVCC 版本链一致 + rollback 索引重建
|
||
*/
|
||
|
||
import { VERSION } from '../src/constants';
|
||
import { MetonaSqlark } from '../src/core';
|
||
import { AriaEngine } from '../src/engine/aria/index';
|
||
import { MemoryEngine } from '../src/engine/memory';
|
||
import { WAL } from '../src/engine/aria/wal/log';
|
||
import { tokenize } from '../src/sql/lexer';
|
||
|
||
import { resetOPFSMock } from './helpers/storage-harness';
|
||
|
||
beforeEach(() => { resetOPFSMock(); });
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// P0-1: Aria WAL DROP_TABLE 崩溃恢复
|
||
// ---------------------------------------------------------------------------
|
||
|
||
describe('[v0.3.3] P0-1: WAL DROP_TABLE 崩溃恢复', () => {
|
||
const mkEngine = async (name: string): Promise<AriaEngine> => {
|
||
const e = new AriaEngine({ storageBackend: 'opfs', walSyncMode: 'full' });
|
||
await e.open(name, 1);
|
||
return e;
|
||
};
|
||
|
||
test('删表后崩溃(不 close),重启后表与数据不复活', async () => {
|
||
const dbName = `crash-drop-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||
let e = await mkEngine(dbName);
|
||
await e.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true } } });
|
||
await e.insert('t', [{ id: '1' }, { id: '2' }]);
|
||
await e.dropTable('t');
|
||
|
||
// 模拟崩溃:不 close,直接重建引擎(WAL 未 checkpoint)
|
||
e = await mkEngine(dbName);
|
||
const names = await e.getTableNames();
|
||
expect(names).not.toContain('t');
|
||
await e.close();
|
||
});
|
||
|
||
test('删表崩溃后重建同名表,旧数据不复活', async () => {
|
||
const dbName = `crash-drop2-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||
let e = await mkEngine(dbName);
|
||
await e.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true } } });
|
||
await e.insert('t', [{ id: '1' }, { id: '2' }]);
|
||
await e.dropTable('t');
|
||
|
||
e = await mkEngine(dbName);
|
||
await e.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true } } });
|
||
const rows = await e.find('t', { table: 't' });
|
||
expect(rows).toHaveLength(0);
|
||
await e.close();
|
||
});
|
||
|
||
test('WAL 恢复后截断:重启不再重复回放(checkpoint 后 WAL 空)', async () => {
|
||
const dbName = `crash-wal-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||
let e = await mkEngine(dbName);
|
||
await e.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true }, v: { type: 'number' } } });
|
||
await e.insert('t', [{ id: '1', v: 1 }, { id: '2', v: 2 }]);
|
||
await e.dropTable('t');
|
||
|
||
// 第一次恢复:WAL 回放 + 截断
|
||
e = await mkEngine(dbName);
|
||
expect(await e.getTableNames()).not.toContain('t');
|
||
|
||
// 第二次恢复:WAL 已空,不再有任何回放副作用
|
||
await e.close();
|
||
e = await mkEngine(dbName);
|
||
expect(await e.getTableNames()).not.toContain('t');
|
||
await e.close();
|
||
});
|
||
});
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// P0-2: MemoryEngine update/delete 索引维护
|
||
// ---------------------------------------------------------------------------
|
||
|
||
describe('[v0.3.3] P0-2: Memory 引擎索引维护', () => {
|
||
test('update 修改唯一列后,重复值插入被拦截(unique 约束不被绕过)', async () => {
|
||
const e = new MemoryEngine();
|
||
await e.open('x', 1);
|
||
await e.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true }, email: { type: 'string', unique: true, index: true } } });
|
||
await e.insert('t', [{ id: '1', email: 'a@x.com' }]);
|
||
await e.update('t', { table: 't', where: { id: '1' } }, { email: 'b@x.com' });
|
||
await expect(e.insert('t', [{ id: '2', email: 'b@x.com' }])).rejects.toThrow('Unique constraint');
|
||
});
|
||
|
||
test('update 后按新值索引查询命中', async () => {
|
||
const e = new MemoryEngine();
|
||
await e.open('x', 1);
|
||
await e.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true }, email: { type: 'string', index: true } } });
|
||
await e.insert('t', [{ id: '1', email: 'a@x.com' }]);
|
||
await e.update('t', { table: 't', where: { id: '1' } }, { email: 'b@x.com' });
|
||
const rows = await e.find('t', { table: 't', where: { email: 'b@x.com' } });
|
||
expect(rows).toHaveLength(1);
|
||
expect(rows[0].id).toBe('1');
|
||
});
|
||
|
||
test('update 到已存在的唯一值时抛 UNIQUE_VIOLATION', async () => {
|
||
const e = new MemoryEngine();
|
||
await e.open('x', 1);
|
||
await e.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true }, email: { type: 'string', unique: true, index: true } } });
|
||
await e.insert('t', [{ id: '1', email: 'a@x.com' }, { id: '2', email: 'b@x.com' }]);
|
||
await expect(e.update('t', { table: 't', where: { id: '1' } }, { email: 'b@x.com' })).rejects.toThrow('Unique constraint');
|
||
});
|
||
|
||
test('delete 后索引清理:同值可重新插入且索引查询正确', async () => {
|
||
const e = new MemoryEngine();
|
||
await e.open('x', 1);
|
||
await e.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true }, email: { type: 'string', unique: true, index: true } } });
|
||
await e.insert('t', [{ id: '1', email: 'a@x.com' }]);
|
||
await e.delete('t', { table: 't', where: { id: '1' } });
|
||
await e.insert('t', [{ id: '2', email: 'a@x.com' }]);
|
||
const rows = await e.find('t', { table: 't', where: { email: 'a@x.com' } });
|
||
expect(rows).toHaveLength(1);
|
||
expect(rows[0].id).toBe('2');
|
||
});
|
||
|
||
test('级联删除清理子表索引条目', async () => {
|
||
const e = new MemoryEngine();
|
||
await e.open('x', 1);
|
||
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' } });
|
||
// u1 已删,o1 级联删除 → 按 user_id 索引查询应为空
|
||
const rows = await e.find('orders', { table: 'orders', where: { user_id: 'u1' } });
|
||
expect(rows).toHaveLength(0);
|
||
});
|
||
});
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// P1-3: Aria 事务内读到自己写入的变更
|
||
// ---------------------------------------------------------------------------
|
||
|
||
describe('[v0.3.3] P1-3: Aria 事务内读写一致', () => {
|
||
test('事务内 insert 后 update 同一行生效', async () => {
|
||
const e = new AriaEngine({ storageBackend: 'memory', walSyncMode: 'full' });
|
||
await e.open('txn1', 1);
|
||
await e.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true }, name: { type: 'string' } } });
|
||
await e.beginTransaction();
|
||
await e.insert('t', [{ id: '1', name: 'Alice' }]);
|
||
const n = await e.update('t', { table: 't', where: { id: '1' } }, { name: 'Bob' });
|
||
await e.commitTransaction();
|
||
const rows = await e.find('t', { table: 't' });
|
||
expect(n).toBe(1);
|
||
expect(rows[0].name).toBe('Bob');
|
||
await e.close();
|
||
});
|
||
|
||
test('事务内 insert 后 delete 该行生效', async () => {
|
||
const e = new AriaEngine({ storageBackend: 'memory', walSyncMode: 'full' });
|
||
await e.open('txn2', 1);
|
||
await e.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true } } });
|
||
await e.beginTransaction();
|
||
await e.insert('t', [{ id: '1' }, { id: '2' }]);
|
||
const d = await e.delete('t', { table: 't', where: { id: '2' } });
|
||
await e.commitTransaction();
|
||
const rows = await e.find('t', { table: 't' });
|
||
expect(d).toBe(1);
|
||
expect(rows).toHaveLength(1);
|
||
await e.close();
|
||
});
|
||
|
||
test('rollback 后索引无残留(事务内写入的索引被重建清理)', async () => {
|
||
const e = new AriaEngine({ storageBackend: 'memory', walSyncMode: 'full' });
|
||
await e.open('txn3', 1);
|
||
await e.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true }, name: { type: 'string', index: true } } });
|
||
await e.beginTransaction();
|
||
await e.insert('t', [{ id: '1', name: 'Dave' }]);
|
||
await e.rollbackTransaction();
|
||
const rows = await e.find('t', { table: 't', where: { name: 'Dave' } });
|
||
expect(rows).toHaveLength(0);
|
||
await e.close();
|
||
});
|
||
|
||
test('rollback 更新后索引恢复旧值', async () => {
|
||
const e = new AriaEngine({ storageBackend: 'memory', walSyncMode: 'full' });
|
||
await e.open('txn4', 1);
|
||
await e.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true }, name: { type: 'string', index: true } } });
|
||
await e.insert('t', [{ id: '1', name: 'Bob' }]);
|
||
await e.beginTransaction();
|
||
await e.update('t', { table: 't', where: { id: '1' } }, { name: 'Eve' });
|
||
await e.rollbackTransaction();
|
||
const rows = await e.find('t', { table: 't', where: { name: 'Bob' } });
|
||
expect(rows).toHaveLength(1);
|
||
const stale = await e.find('t', { table: 't', where: { name: 'Eve' } });
|
||
expect(stale).toHaveLength(0);
|
||
await e.close();
|
||
});
|
||
|
||
test('事务内 clear 生效且可提交', async () => {
|
||
const e = new AriaEngine({ storageBackend: 'memory', walSyncMode: 'full' });
|
||
await e.open('txn5', 1);
|
||
await e.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true } } });
|
||
await e.insert('t', [{ id: '1' }, { id: '2' }]);
|
||
await e.beginTransaction();
|
||
await e.clear('t');
|
||
await e.commitTransaction();
|
||
expect(await e.count('t')).toBe(0);
|
||
await e.close();
|
||
});
|
||
});
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// P1-5: WAL 字节数跟踪
|
||
// ---------------------------------------------------------------------------
|
||
|
||
describe('[v0.3.3] P1-5: WAL 字节数跟踪', () => {
|
||
test('full 模式 append 后 getBufferedBytes 反映实际字节数', async () => {
|
||
const chunks: Uint8Array[] = [];
|
||
const wal = new WAL({
|
||
append: async (d) => { chunks.push(d); },
|
||
readAll: async () => { const t = chunks.reduce((s, c) => s + c.byteLength, 0); const out = new Uint8Array(t); let o = 0; for (const c of chunks) { out.set(c, o); o += c.byteLength; } return out; },
|
||
truncate: async () => { chunks.length = 0; },
|
||
exists: async () => chunks.length > 0,
|
||
}, true, 'full');
|
||
|
||
await wal.append({ type: 1, txnId: 0, tableName: 't', key: '1', data: { a: 1 } } as never);
|
||
const bytesAfterAppend = wal.getBufferedBytes();
|
||
expect(bytesAfterAppend).toBeGreaterThan(0);
|
||
|
||
await wal.checkpoint();
|
||
expect(wal.getBufferedBytes()).toBe(0);
|
||
});
|
||
|
||
test('batch 模式 flush 后字节数保留(未 checkpoint 前)', async () => {
|
||
const chunks: Uint8Array[] = [];
|
||
const wal = new WAL({
|
||
append: async (d) => { chunks.push(d); },
|
||
readAll: async () => new Uint8Array(0),
|
||
truncate: async () => { chunks.length = 0; },
|
||
exists: async () => chunks.length > 0,
|
||
}, true, 'batch');
|
||
|
||
await wal.append({ type: 1, txnId: 0, tableName: 't', key: '1', data: { a: 1 } } as never);
|
||
await wal.flush();
|
||
expect(wal.getBufferedBytes()).toBeGreaterThan(0); // 已写盘但未 checkpoint
|
||
await wal.checkpoint();
|
||
expect(wal.getBufferedBytes()).toBe(0);
|
||
});
|
||
});
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// P1-6: Aria 主键列走主 LSM(无冗余二级索引)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
describe('[v0.3.3] P1-6: Aria 主键查询优化', () => {
|
||
test('PK $in 查询走主 LSM 返回正确行', async () => {
|
||
const e = new AriaEngine({ storageBackend: 'memory', walSyncMode: 'full' });
|
||
await e.open('pk1', 1);
|
||
await e.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true }, v: { type: 'number' } } });
|
||
await e.insert('t', [{ id: '1', v: 10 }, { id: '2', v: 20 }, { id: '3', v: 30 }]);
|
||
const rows = await e.find('t', { table: 't', where: { id: { $in: ['1', '3'] } } });
|
||
expect(rows.map((r) => r.id).sort()).toEqual(['1', '3']);
|
||
await e.close();
|
||
});
|
||
|
||
test('PK 范围查询(字符串字典序)', async () => {
|
||
const e = new AriaEngine({ storageBackend: 'memory', walSyncMode: 'full' });
|
||
await e.open('pk2', 1);
|
||
await e.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true }, v: { type: 'number' } } });
|
||
await e.insert('t', [{ id: 'a', v: 1 }, { id: 'b', v: 2 }, { id: 'c', v: 3 }]);
|
||
const rows = await e.find('t', { table: 't', where: { id: { $gte: 'b' } } });
|
||
expect(rows.map((r) => r.id)).toEqual(['b', 'c']);
|
||
const lt = await e.find('t', { table: 't', where: { id: { $lt: 'b' } } });
|
||
expect(lt.map((r) => r.id)).toEqual(['a']);
|
||
await e.close();
|
||
});
|
||
|
||
test('主键列不再创建独立二级索引(dropIndex 仍保护主键)', async () => {
|
||
const e = new AriaEngine({ storageBackend: 'memory', walSyncMode: 'full' });
|
||
await e.open('pk3', 1);
|
||
await e.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true }, name: { type: 'string', index: true } } });
|
||
// 主键索引保护仍在
|
||
await expect(e.dropIndex('t', 'id')).rejects.toThrow('Cannot drop primary key');
|
||
// 二级索引(name)仍可正常使用
|
||
await e.insert('t', [{ id: '1', name: 'x' }]);
|
||
const rows = await e.find('t', { table: 't', where: { name: 'x' } });
|
||
expect(rows).toHaveLength(1);
|
||
await e.close();
|
||
});
|
||
});
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// P1-7: ORDER BY 别名
|
||
// ---------------------------------------------------------------------------
|
||
|
||
describe('[v0.3.3] P1-7: ORDER BY 别名', () => {
|
||
test('SELECT 别名可被 ORDER BY 引用', async () => {
|
||
const db = new MetonaSqlark({ name: `alias-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, mode: 'memory' });
|
||
await db.init();
|
||
await db.defineTable('users', {
|
||
id: { type: 'string', primaryKey: true },
|
||
name: { type: 'string' },
|
||
age: { type: 'number' },
|
||
});
|
||
await db.query("INSERT INTO users VALUES ('1', 'Alice', 30)");
|
||
await db.query("INSERT INTO users VALUES ('2', 'Bob', 25)");
|
||
await db.query("INSERT INTO users VALUES ('3', 'Carol', 35)");
|
||
|
||
const rows = await db.query('SELECT name AS n FROM users ORDER BY n DESC') as Record<string, unknown>[];
|
||
expect(rows.map((r) => r.n)).toEqual(['Carol', 'Bob', 'Alice']);
|
||
|
||
const withLimit = await db.query('SELECT name AS n FROM users ORDER BY n ASC LIMIT 2') as Record<string, unknown>[];
|
||
expect(withLimit.map((r) => r.n)).toEqual(['Alice', 'Bob']);
|
||
await db.close();
|
||
});
|
||
|
||
test('GROUP BY 场景 ORDER BY 聚合别名', async () => {
|
||
const db = new MetonaSqlark({ name: `alias2-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, 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)");
|
||
await db.query("INSERT INTO emp VALUES ('2', 'eng', 200)");
|
||
await db.query("INSERT INTO emp VALUES ('3', 'ops', 300)");
|
||
|
||
const rows = await db.query('SELECT dept, COUNT(*) AS cnt FROM emp GROUP BY dept ORDER BY cnt DESC') as Record<string, unknown>[];
|
||
expect(rows[0]).toEqual({ dept: 'eng', cnt: 2 });
|
||
await db.close();
|
||
});
|
||
});
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// P1-8: SQL 字符串 '' 标准转义
|
||
// ---------------------------------------------------------------------------
|
||
|
||
describe('[v0.3.3] P1-8: SQL 字符串转义', () => {
|
||
test("'' 双引号转义为单个引号", async () => {
|
||
const db = new MetonaSqlark({ name: `esc-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, mode: 'memory' });
|
||
await db.init();
|
||
await db.defineTable('t', {
|
||
id: { type: 'string', primaryKey: true },
|
||
note: { type: 'string' },
|
||
});
|
||
await db.query("INSERT INTO t VALUES ('1', 'it''s a test')");
|
||
const rows = await db.query('SELECT * FROM t') as Record<string, unknown>[];
|
||
expect(rows[0].note).toBe("it's a test");
|
||
await db.close();
|
||
});
|
||
|
||
test('反斜杠转义仍兼容', () => {
|
||
const tokens = tokenize("SELECT 'a\\'b'");
|
||
expect(tokens[1].value).toBe("a'b");
|
||
});
|
||
});
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// P1-9: Savepoint 回滚后 MVCC 一致
|
||
// ---------------------------------------------------------------------------
|
||
|
||
describe('[v0.3.3] P1-9: Savepoint + MVCC 一致性', () => {
|
||
test('rollbackToSavepoint 后提交,数据为 savepoint 时状态', async () => {
|
||
const e = new AriaEngine({ storageBackend: 'memory', walSyncMode: 'full' });
|
||
await e.open('sp1', 1);
|
||
await e.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true }, v: { type: 'number' } } });
|
||
await e.insert('t', [{ id: '1', v: 1 }]);
|
||
await e.beginTransaction();
|
||
await e.insert('t', [{ id: '2', v: 2 }]);
|
||
await e.savepoint('sp');
|
||
await e.insert('t', [{ id: '3', v: 3 }]);
|
||
await e.rollbackToSavepoint('sp');
|
||
await e.commitTransaction();
|
||
const rows = await e.find('t', { table: 't' });
|
||
expect(rows.map((r) => r.id).sort()).toEqual(['1', '2']);
|
||
await e.close();
|
||
});
|
||
|
||
test('savepoint 回滚后事务仍可继续写入并提交', async () => {
|
||
const e = new AriaEngine({ storageBackend: 'memory', walSyncMode: 'full' });
|
||
await e.open('sp2', 1);
|
||
await e.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true } } });
|
||
await e.beginTransaction();
|
||
await e.savepoint('sp');
|
||
await e.insert('t', [{ id: '1' }]);
|
||
await e.rollbackToSavepoint('sp');
|
||
await e.insert('t', [{ id: '2' }]);
|
||
await e.commitTransaction();
|
||
const rows = await e.find('t', { table: 't' });
|
||
expect(rows.map((r) => r.id)).toEqual(['2']);
|
||
await e.close();
|
||
});
|
||
});
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 端到端冒烟
|
||
// ---------------------------------------------------------------------------
|
||
|
||
describe('[v0.3.3] 端到端', () => {
|
||
test('全部修复点可共存于 MetonaSqlark API', async () => {
|
||
// v0.8.0: 版本一致性由 tests/version-contract.test.ts 统一校验
|
||
expect(VERSION).toMatch(/^\d+\.\d+\.\d+$/);
|
||
const db = new MetonaSqlark({ name: `e2e-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, mode: 'memory' });
|
||
await db.init();
|
||
await db.defineTable('users', {
|
||
id: { type: 'string', primaryKey: true },
|
||
email: { type: 'string', unique: true, index: true },
|
||
age: { type: 'number' },
|
||
});
|
||
await db.query("INSERT INTO users VALUES ('1', 'a@x.com', 20)");
|
||
await db.query("INSERT INTO users VALUES ('2', 'b@x.com', 30)");
|
||
// 别名排序 + 唯一约束组合
|
||
const rows = await db.query('SELECT email AS e FROM users ORDER BY e DESC') as Record<string, unknown>[];
|
||
expect(rows.map((r) => r.e)).toEqual(['b@x.com', 'a@x.com']);
|
||
await db.close();
|
||
});
|
||
});
|