Files
MetonaSqlark/tests/v072-fixes.test.ts
thzxx 05e6823bf1
CI / test (22.x) (push) Successful in 24m26s
CI / e2e (push) Successful in 10m0s
CI / test (18.x) (push) Successful in 27m14s
CI / test (20.x) (push) Failing after 1h19m9s
CI / test (24.x) (push) Successful in 37m49s
fix: v0.7.2 语句级原子性 + 事务 DDL 拒绝 + 约束/绑定硬化 — 6 项修复 + 43 回归 + CI 重型套件串行
- UPDATE 语句级部分提交(P1,四引擎):两阶段全量预检后执行,批内唯一互查,
  任何一行失败整句不执行(aria 场景 WAL 与内存不再错位)
- 事务内 ALTER/CREATE INDEX/DROP INDEX 残留(P1):Memory/KVStore 显式拒绝
  (对齐 Aria),createTable/dropTable 保持可回滚
- SET NULL 级联绕过 required 约束(P1):预检阶段整体拒绝 FOREIGN_KEY_VIOLATION
- bindParameters 注释误判(P2):行注释/块注释中的 ? 与引号不再参与绑定
- 未闭合字符串静默接受 → lexer 抛 PARSE_ERROR;未知 where 操作符抛 QUERY_ERROR
- UPDATE undefined 覆盖列值 → 语义化为不更新(null 仍置空)
- Hybrid 写穿透非原子(P1):磁盘失败自动重载内存对齐磁盘再抛原错误
- CI:Run tests 拆常规并行 + 重型串行(runInBand),重型测试超时余量提升,
  性能护栏 kv 120→240s / opfs 150→300s(仍拦截悬崖回归)
- 测试 1155 → 1198(74 套件),覆盖率 89.82% 保持
2026-08-13 15:31:16 +08:00

541 lines
24 KiB
TypeScript
Raw Permalink 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.7.2 修复回归测试
*
* 覆盖:
* - UPDATE 语句级原子性(多行更新撞唯一约束 → 整句拒绝,四引擎)
* - 批内唯一互查(两行同时更新到同一新唯一值 → 整句拒绝)
* - 事务内 DDL 拒绝(ALTER TABLE / CREATE INDEX / DROP INDEXMemory/KVStore 对齐 Aria
* - SET NULL 级联撞 required 列 → FOREIGN_KEY_VIOLATIONdelete 与 update 主键变更,Memory/Aria
* - bindParameters 注释感知(行注释 / 块注释中的 ? 与引号不参与绑定)
* - 未闭合字符串字面量 → PARSE_ERROR
* - 未知 where 操作符 → QUERY_ERROR(不再静默全匹配)
* - UPDATE undefined 值语义化(保留旧值,不再删除列)
* - Hybrid 磁盘写失败补偿(内存对齐磁盘状态)
*/
import { MetonaSqlark } from '../src/core';
import { parse } from '../src/sql/parser';
import { bindParameters } from '../src/sql/params';
import { matchWhere } from '../src/query/where-matcher';
import { MemoryEngine } from '../src/engine/memory';
import { KVStoreEngine } from '../src/engine/kvstore_engine';
import { HybridEngine } from '../src/hybrid/index';
import { createSchema } from '../src/table/schema';
function uniqueDB(): string {
return `v072-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
}
// ===================================================================
// UPDATE 语句级原子性
// ===================================================================
describe('v0.7.2 — UPDATE 语句级原子性', () => {
const modes = ['memory', 'disk', 'hybrid', 'aria'] as const;
for (const mode of modes) {
it(`${mode}: 多行更新撞唯一约束 → 整句拒绝、数据不变`, async () => {
const db = await MetonaSqlark.create({ name: uniqueDB(), mode, diskEngine: 'memory' });
await db.defineTable('t', {
id: { type: 'string', primaryKey: true },
email: { type: 'string', unique: true },
});
await db.table('t').insertMany([
{ id: 'a', email: 'a@x.com' },
{ id: 'b', email: 'b@x.com' },
{ id: 'c', email: 'c@x.com' },
]);
await db.table('t').update({ email: 'z@x.com' }).where({ id: 'a' }).execute();
// 一条语句同时把 b、c 改成 a 已占用的 z@x.com:b 成功改到一半,c 撞唯一
await expect(
db.table('t').update({ email: 'z@x.com' }).where({ id: 'c' }).execute(),
).rejects.toThrow(); // 单行冲突
// 关键场景:无 where 全表更新到同一值 → 第二行即冲突
await expect(
db.table('t').update({ email: 'y@x.com' }).execute(),
).rejects.toMatchObject({ code: 'UNIQUE_VIOLATION' });
// 整句拒绝:任何行都不该被改成 y@x.com
const rows = await db.query('SELECT id, email FROM t ORDER BY id') as Record<string, unknown>[];
expect(rows).toEqual([
{ id: 'a', email: 'z@x.com' },
{ id: 'b', email: 'b@x.com' },
{ id: 'c', email: 'c@x.com' },
]);
await db.close();
});
}
it('memory: 校验失败(required 置 null)也整句拒绝', async () => {
const db = await MetonaSqlark.create({ name: uniqueDB(), mode: 'memory' });
await db.defineTable('t', {
id: { type: 'string', primaryKey: true },
name: { type: 'string', required: true },
});
await db.table('t').insertMany([
{ id: '1', name: 'A' },
{ id: '2', name: 'B' },
]);
await expect(
db.table('t').update({ name: null }).execute(),
).rejects.toMatchObject({ code: 'VALIDATION_ERROR' });
const rows = await db.query('SELECT * FROM t ORDER BY id') as Record<string, unknown>[];
expect(rows).toEqual([{ id: '1', name: 'A' }, { id: '2', name: 'B' }]);
await db.close();
});
it('aria: 主键变更撞已有主键整句拒绝(含 WAL 一致性)', async () => {
const db = await MetonaSqlark.create({ name: uniqueDB(), mode: 'aria', diskEngine: 'memory' });
await db.defineTable('t', {
id: { type: 'string', primaryKey: true },
n: { type: 'number' },
});
await db.table('t').insertMany([
{ id: 'a', n: 1 }, { id: 'b', n: 2 }, { id: 'c', n: 3 },
]);
// a → c 撞已有 c;且 d 行的更新(同语句)也不得生效
await expect(
db.table('t').update({ id: 'c' }).where({ id: 'a' }).execute(),
).rejects.toMatchObject({ code: 'DUPLICATE_KEY' });
const rows = await db.query('SELECT * FROM t ORDER BY id') as Record<string, unknown>[];
expect(rows.map((r) => r.id)).toEqual(['a', 'b', 'c']);
await db.close();
});
});
// ===================================================================
// 批内唯一互查
// ===================================================================
describe('v0.7.2 — 批内唯一互查', () => {
it('两行同时更新到同一新唯一值 → 整句拒绝(memory)', async () => {
const db = await MetonaSqlark.create({ name: uniqueDB(), mode: 'memory' });
await db.defineTable('t', {
id: { type: 'string', primaryKey: true },
email: { type: 'string', unique: true },
});
await db.table('t').insertMany([
{ id: 'a', email: 'a@x.com' },
{ id: 'b', email: 'b@x.com' },
]);
await expect(
db.table('t').update({ email: 'z@x.com' }).execute(),
).rejects.toMatchObject({ code: 'UNIQUE_VIOLATION' });
const rows = await db.query('SELECT email FROM t ORDER BY id') as Record<string, unknown>[];
expect(rows).toEqual([{ email: 'a@x.com' }, { email: 'b@x.com' }]);
await db.close();
});
it('两行同时更新到同一新唯一值 → 整句拒绝(aria)', async () => {
const db = await MetonaSqlark.create({ name: uniqueDB(), mode: 'aria', diskEngine: 'memory' });
await db.defineTable('t', {
id: { type: 'string', primaryKey: true },
email: { type: 'string', unique: true },
});
await db.table('t').insertMany([
{ id: 'a', email: 'a@x.com' },
{ id: 'b', email: 'b@x.com' },
]);
await expect(
db.table('t').update({ email: 'z@x.com' }).execute(),
).rejects.toMatchObject({ code: 'UNIQUE_VIOLATION' });
const rows = await db.query('SELECT email FROM t ORDER BY id') as Record<string, unknown>[];
expect(rows).toEqual([{ email: 'a@x.com' }, { email: 'b@x.com' }]);
await db.close();
});
it('唯一值不变时更新自身不误报(回归)', async () => {
const db = await MetonaSqlark.create({ name: uniqueDB(), mode: 'memory' });
await db.defineTable('t', {
id: { type: 'string', primaryKey: true },
email: { type: 'string', unique: true },
name: { type: 'string' },
});
await db.table('t').insertMany([
{ id: 'a', email: 'a@x.com', name: 'A' },
]);
const n = await db.table('t').update({ name: 'A2', email: 'a@x.com' }).execute();
expect(n).toBe(1);
const rows = await db.query('SELECT * FROM t') as Record<string, unknown>[];
expect(rows[0].name).toBe('A2');
await db.close();
});
});
// ===================================================================
// 事务内 DDL 拒绝
// ===================================================================
describe('v0.7.2 — 事务内 DDL 拒绝(三引擎一致)', () => {
it('memory: 事务内 ALTER TABLE 抛 NOT_SUPPORTED,事务可继续提交', async () => {
const db = await MetonaSqlark.create({ name: uniqueDB(), mode: 'memory' });
await db.defineTable('t', { id: { type: 'string', primaryKey: true }, a: { type: 'number' } });
await db.query('BEGIN');
await db.query("INSERT INTO t VALUES ('1', 10)");
await expect(db.query('ALTER TABLE t ADD COLUMN b STRING')).rejects.toMatchObject({ code: 'NOT_SUPPORTED' });
await db.query('COMMIT');
const schema = await db.getEngine().getTableSchema('t');
expect('b' in (schema!.columns)).toBe(false);
const rows = await db.query('SELECT * FROM t') as Record<string, unknown>[];
expect(rows).toEqual([{ id: '1', a: 10 }]);
await db.close();
});
it('memory: ROLLBACK 后无 DDL 残留', async () => {
const db = await MetonaSqlark.create({ name: uniqueDB(), mode: 'memory' });
await db.defineTable('t', { id: { type: 'string', primaryKey: true } });
await db.query('BEGIN');
await expect(db.query('ALTER TABLE t ADD COLUMN b STRING')).rejects.toMatchObject({ code: 'NOT_SUPPORTED' });
await db.query('ROLLBACK');
const schema = await db.getEngine().getTableSchema('t');
expect(Object.keys(schema!.columns)).toEqual(['id']);
await db.close();
});
it('kv: 事务内 ALTER TABLE 抛 NOT_SUPPORTED', async () => {
const db = await MetonaSqlark.create({ name: uniqueDB(), mode: 'disk', diskEngine: 'memory' });
await db.defineTable('t', { id: { type: 'string', primaryKey: true } });
await db.query('BEGIN');
await expect(db.query('ALTER TABLE t ADD COLUMN b STRING')).rejects.toMatchObject({ code: 'NOT_SUPPORTED' });
await db.query('ROLLBACK');
const schema = await db.getEngine().getTableSchema('t');
expect(Object.keys(schema!.columns)).toEqual(['id']);
await db.close();
});
it('kv: 事务内 CREATE INDEX / DROP INDEX 抛 NOT_SUPPORTED', async () => {
const db = await MetonaSqlark.create({ name: uniqueDB(), mode: 'disk', diskEngine: 'memory' });
await db.defineTable('t', { id: { type: 'string', primaryKey: true }, v: { type: 'number' } });
await db.query('BEGIN');
await expect(db.query('CREATE INDEX idx_v ON t (v)')).rejects.toMatchObject({ code: 'NOT_SUPPORTED' });
await db.query('ROLLBACK');
const schema = await db.getEngine().getTableSchema('t');
expect(schema!.columns.v.index).toBeFalsy();
await db.close();
});
it('memory: 事务内 createTable/dropTable 仍可回滚(既有行为保持)', async () => {
const eng = new MemoryEngine();
await eng.open(uniqueDB(), 1);
await eng.createTable(createSchema('t', { id: { type: 'string', primaryKey: true } }));
await eng.beginTransaction();
await eng.createTable(createSchema('tmp', { id: { type: 'string', primaryKey: true } }));
await eng.rollbackTransaction();
expect(await eng.hasTable('tmp')).toBe(false);
expect(await eng.hasTable('t')).toBe(true);
await eng.close();
});
it('hybrid: 事务内 ALTER TABLE 抛 NOT_SUPPORTED(内存层拒绝)', async () => {
const db = await MetonaSqlark.create({ name: uniqueDB(), mode: 'hybrid', diskEngine: 'memory' });
await db.defineTable('t', { id: { type: 'string', primaryKey: true } });
await db.query('BEGIN');
await expect(db.query('ALTER TABLE t ADD COLUMN b STRING')).rejects.toMatchObject({ code: 'NOT_SUPPORTED' });
await db.query('ROLLBACK');
const schema = await db.getEngine().getTableSchema('t');
expect(Object.keys(schema!.columns)).toEqual(['id']);
await db.close();
});
});
// ===================================================================
// SET NULL 级联撞 required 列
// ===================================================================
describe('v0.7.2 — SET NULL 级联撞 required 列', () => {
it('memory: delete 级联 SET NULL 到 required 列 → FOREIGN_KEY_VIOLATION', async () => {
const db = await MetonaSqlark.create({ name: uniqueDB(), mode: 'memory' });
await db.defineTable('parent', { id: { type: 'string', primaryKey: true } });
await db.defineTable('child', {
id: { type: 'string', primaryKey: true },
pid: { type: 'string', required: true, references: 'parent.id', onDelete: 'SET NULL' },
});
await db.query("INSERT INTO parent VALUES ('p1')");
await db.query("INSERT INTO child VALUES ('c1', 'p1')");
await expect(db.query("DELETE FROM parent WHERE id = 'p1'")).rejects.toMatchObject({ code: 'FOREIGN_KEY_VIOLATION' });
// 整体拒绝:父行仍在
const rows = await db.query('SELECT * FROM parent') as Record<string, unknown>[];
expect(rows).toHaveLength(1);
await db.close();
});
it('aria: delete 级联 SET NULL 到 required 列 → FOREIGN_KEY_VIOLATION', async () => {
const db = await MetonaSqlark.create({ name: uniqueDB(), mode: 'aria', diskEngine: 'memory' });
await db.defineTable('parent', { id: { type: 'string', primaryKey: true } });
await db.defineTable('child', {
id: { type: 'string', primaryKey: true },
pid: { type: 'string', required: true, references: 'parent.id', onDelete: 'SET NULL' },
});
await db.query("INSERT INTO parent VALUES ('p1')");
await db.query("INSERT INTO child VALUES ('c1', 'p1')");
await expect(db.query("DELETE FROM parent WHERE id = 'p1'")).rejects.toMatchObject({ code: 'FOREIGN_KEY_VIOLATION' });
const rows = await db.query('SELECT * FROM parent') as Record<string, unknown>[];
expect(rows).toHaveLength(1);
await db.close();
});
it('memory: update 主键变更 SET NULL 到 required 列 → 整体拒绝', async () => {
const db = await MetonaSqlark.create({ name: uniqueDB(), mode: 'memory' });
await db.defineTable('parent', { id: { type: 'string', primaryKey: true } });
await db.defineTable('child', {
id: { type: 'string', primaryKey: true },
pid: { type: 'string', required: true, references: 'parent.id', onUpdate: 'SET NULL' },
});
await db.query("INSERT INTO parent VALUES ('p1')");
await db.query("INSERT INTO child VALUES ('c1', 'p1')");
await expect(
db.table('parent').update({ id: 'p2' }).where({ id: 'p1' }).execute(),
).rejects.toMatchObject({ code: 'FOREIGN_KEY_VIOLATION' });
const rows = await db.query('SELECT * FROM parent') as Record<string, unknown>[];
expect(rows).toEqual([{ id: 'p1' }]);
await db.close();
});
it('aria: update 主键变更 SET NULL 到 required 列 → 整体拒绝', async () => {
const db = await MetonaSqlark.create({ name: uniqueDB(), mode: 'aria', diskEngine: 'memory' });
await db.defineTable('parent', { id: { type: 'string', primaryKey: true } });
await db.defineTable('child', {
id: { type: 'string', primaryKey: true },
pid: { type: 'string', required: true, references: 'parent.id', onUpdate: 'SET NULL' },
});
await db.query("INSERT INTO parent VALUES ('p1')");
await db.query("INSERT INTO child VALUES ('c1', 'p1')");
await expect(
db.table('parent').update({ id: 'p2' }).where({ id: 'p1' }).execute(),
).rejects.toMatchObject({ code: 'FOREIGN_KEY_VIOLATION' });
const rows = await db.query('SELECT * FROM parent') as Record<string, unknown>[];
expect(rows).toEqual([{ id: 'p1' }]);
await db.close();
});
it('SET NULL 到非 required 列仍正常(回归)', async () => {
const db = await MetonaSqlark.create({ name: uniqueDB(), mode: 'memory' });
await db.defineTable('parent', { id: { type: 'string', primaryKey: true } });
await db.defineTable('child', {
id: { type: 'string', primaryKey: true },
pid: { type: 'string', references: 'parent.id', onDelete: 'SET NULL' },
});
await db.query("INSERT INTO parent VALUES ('p1')");
await db.query("INSERT INTO child VALUES ('c1', 'p1')");
await db.query("DELETE FROM parent WHERE id = 'p1'");
const rows = await db.query('SELECT * FROM child') as Record<string, unknown>[];
expect(rows).toEqual([{ id: 'c1', pid: null }]);
await db.close();
});
});
// ===================================================================
// bindParameters 注释感知
// ===================================================================
describe('v0.7.2 — bindParameters 注释感知', () => {
it('行注释中的 ? 不参与占位符', () => {
const sql = "SELECT * FROM t -- comment with ? inside\n WHERE id = ?";
const bound = bindParameters(sql, ['1']);
expect(bound).toContain('comment with ? inside');
expect(bound).toContain("WHERE id = '1'");
});
it('块注释中的 ? 不参与占位符', () => {
const sql = 'SELECT * FROM t /* c ? c */ WHERE id = ?';
const bound = bindParameters(sql, ['1']);
expect(bound).toContain('/* c ? c */');
expect(bound).toContain("WHERE id = '1'");
});
it("行注释中的单引号不触发未闭合字符串(-- don't", () => {
const sql = "SELECT * FROM t WHERE id = ? -- don't touch";
const bound = bindParameters(sql, ['1']);
expect(bound).toContain("-- don't touch");
});
it('块注释中的单引号不触发未闭合字符串', () => {
const sql = "SELECT * FROM t /* it's fine */ WHERE id = ?";
const bound = bindParameters(sql, ['1']);
expect(bound).toContain("/* it's fine */");
});
it('占位符数量按注释外计算(注释里两个 ? 不报参数不足)', () => {
const sql = "SELECT * FROM t WHERE id = ? /* ? ? */ AND x = ? -- ?\n";
const bound = bindParameters(sql, ['1', '2']);
expect(bound).toContain("'1'");
expect(bound).toContain("'2'");
});
it('SQL 注入载荷仍被安全编码(回归)', () => {
const sql = 'SELECT * FROM t WHERE name = ?';
const bound = bindParameters(sql, ["O'Brien'; DROP TABLE t; --"]);
expect(bound).toContain("'O''Brien''; DROP TABLE t; --'");
});
});
// ===================================================================
// 未闭合字符串字面量
// ===================================================================
describe('v0.7.2 — 未闭合字符串字面量', () => {
it("SELECT 'abc → PARSE_ERROR", () => {
expect(() => parse("SELECT 'abc")).toThrowError(/Unterminated string/);
});
it('query API 同样抛 PARSE_ERROR', async () => {
const db = await MetonaSqlark.create({ name: uniqueDB(), mode: 'memory' });
await expect(db.query("SELECT 'abc")).rejects.toMatchObject({ code: 'PARSE_ERROR' });
await db.close();
});
});
// ===================================================================
// 未知 where 操作符
// ===================================================================
describe('v0.7.2 — 未知 where 操作符抛错', () => {
it('$between 未实现 → QUERY_ERROR(不再静默全匹配)', () => {
expect(() => matchWhere({ age: 3 }, { age: { $between: [1, 2] } } as never)).toThrowError(/Unknown where operator/);
});
it('query API 路径同样报错', async () => {
const db = await MetonaSqlark.create({ name: uniqueDB(), mode: 'memory' });
await db.defineTable('t', { id: { type: 'string', primaryKey: true } });
await db.query("INSERT INTO t VALUES ('1')");
await expect(
db.getEngine().find('t', { table: 't', where: { id: { $betwen: '1' } } as never }),
).rejects.toThrowError(/Unknown where operator/);
await db.close();
});
});
// ===================================================================
// UPDATE undefined 语义化
// ===================================================================
describe('v0.7.2 — UPDATE undefined 语义化', () => {
const modes = ['memory', 'disk', 'hybrid', 'aria'] as const;
for (const mode of modes) {
it(`${mode}: update({ col: undefined }) 保留旧值`, async () => {
const db = await MetonaSqlark.create({ name: uniqueDB(), mode, diskEngine: 'memory' });
await db.defineTable('t', {
id: { type: 'string', primaryKey: true },
tag: { type: 'string', default: 'def' },
});
await db.query("INSERT INTO t VALUES ('1', 'x')");
await db.table('t').update({ tag: undefined }).where({ id: '1' }).execute();
const rows = await db.query('SELECT * FROM t') as Record<string, unknown>[];
expect(rows[0]).toEqual({ id: '1', tag: 'x' });
await db.close();
});
it(`${mode}: update({ col: null }) 仍显式置空`, async () => {
const db = await MetonaSqlark.create({ name: uniqueDB(), mode, diskEngine: 'memory' });
await db.defineTable('t', {
id: { type: 'string', primaryKey: true },
tag: { type: 'string' },
});
await db.query("INSERT INTO t VALUES ('1', 'x')");
await db.table('t').update({ tag: null }).where({ id: '1' }).execute();
const rows = await db.query('SELECT * FROM t') as Record<string, unknown>[];
expect(rows[0]).toEqual({ id: '1', tag: null });
await db.close();
});
}
it('主键 undefined 不触发主键变更路径(kv 引擎)', async () => {
const db = await MetonaSqlark.create({ name: uniqueDB(), mode: 'disk', diskEngine: 'memory' });
await db.defineTable('t', { id: { type: 'string', primaryKey: true }, n: { type: 'number' } });
await db.query("INSERT INTO t VALUES ('1', 1)");
const n = await db.table('t').update({ id: undefined, n: 2 }).where({ id: '1' }).execute();
expect(n).toBe(1);
const rows = await db.query('SELECT * FROM t') as Record<string, unknown>[];
expect(rows[0]).toEqual({ id: '1', n: 2 });
await db.close();
});
});
// ===================================================================
// Hybrid 磁盘写失败补偿
// ===================================================================
describe('v0.7.2 — Hybrid 磁盘写失败补偿', () => {
it('insert 磁盘失败 → 错误抛出 + 内存与磁盘一致', async () => {
const db = await MetonaSqlark.create({ name: uniqueDB(), mode: 'hybrid', diskEngine: 'memory' });
await db.defineTable('t', { id: { type: 'string', primaryKey: true } });
await db.query("INSERT INTO t VALUES ('1')");
const eng = db.getEngine() as HybridEngine;
const disk = (eng as unknown as { diskEngine: IStorageEngineMock }).diskEngine;
const origInsert = disk.insert.bind(disk);
let fail = true;
disk.insert = async (table: string, rows: Record<string, unknown>[]) => {
if (fail) throw new Error('disk down');
return origInsert(table, rows);
};
await expect(db.query("INSERT INTO t VALUES ('2')")).rejects.toThrow('disk down');
// 内存已对齐磁盘:'2' 不在(补偿后重载)
const rows = await db.query('SELECT * FROM t') as Record<string, unknown>[];
expect(rows).toEqual([{ id: '1' }]);
// 恢复后正常写入
fail = false;
await db.query("INSERT INTO t VALUES ('3')");
const rows2 = await db.query('SELECT * FROM t ORDER BY id') as Record<string, unknown>[];
expect(rows2).toEqual([{ id: '1' }, { id: '3' }]);
await db.close();
});
it('update 磁盘失败 → 错误抛出 + 内存对齐磁盘', async () => {
const db = await MetonaSqlark.create({ name: uniqueDB(), mode: 'hybrid', diskEngine: 'memory' });
await db.defineTable('t', { id: { type: 'string', primaryKey: true }, n: { type: 'number' } });
await db.query("INSERT INTO t VALUES ('1', 1)");
const eng = db.getEngine() as HybridEngine;
const disk = (eng as unknown as { diskEngine: IStorageEngineMock }).diskEngine;
disk.update = async () => { throw new Error('disk down'); };
await expect(db.table('t').update({ n: 99 }).where({ id: '1' }).execute()).rejects.toThrow('disk down');
const rows = await db.query('SELECT * FROM t') as Record<string, unknown>[];
expect(rows).toEqual([{ id: '1', n: 1 }]);
await db.close();
});
it('delete 磁盘失败 → 错误抛出 + 内存对齐磁盘', async () => {
const db = await MetonaSqlark.create({ name: uniqueDB(), mode: 'hybrid', diskEngine: 'memory' });
await db.defineTable('t', { id: { type: 'string', primaryKey: true } });
await db.query("INSERT INTO t VALUES ('1')");
const eng = db.getEngine() as HybridEngine;
const disk = (eng as unknown as { diskEngine: IStorageEngineMock }).diskEngine;
disk.delete = async () => { throw new Error('disk down'); };
await expect(db.table('t').delete().where({ id: '1' }).execute()).rejects.toThrow('disk down');
const rows = await db.query('SELECT * FROM t') as Record<string, unknown>[];
expect(rows).toEqual([{ id: '1' }]);
await db.close();
});
it('事务内写失败仍由快照回滚(补偿不影响事务路径)', async () => {
const db = await MetonaSqlark.create({ name: uniqueDB(), mode: 'hybrid', diskEngine: 'memory' });
await db.defineTable('t', { id: { type: 'string', primaryKey: true } });
await db.query("INSERT INTO t VALUES ('1')");
await expect(db.transaction(async (trx) => {
await trx.table('t').insert({ id: '2' });
throw new Error('tx fail');
})).rejects.toThrow('tx fail');
const rows = await db.query('SELECT * FROM t') as Record<string, unknown>[];
expect(rows).toEqual([{ id: '1' }]);
await db.close();
});
});
interface IStorageEngineMock {
insert: (table: string, rows: Record<string, unknown>[]) => Promise<string[]>;
update: (table: string, query: never, updates: Record<string, unknown>) => Promise<number>;
delete: (table: string, query: never) => Promise<number>;
}
// 确保 KVStoreEngine 也导出(引用用)
void KVStoreEngine;