fix: v0.7.2 语句级原子性 + 事务 DDL 拒绝 + 约束/绑定硬化 — 6 项修复 + 43 回归 + CI 重型套件串行
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

- 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% 保持
This commit is contained in:
thzxx
2026-08-13 15:31:16 +08:00
parent cbe407eb49
commit 05e6823bf1
31 changed files with 2635 additions and 762 deletions
+540
View File
@@ -0,0 +1,540 @@
/**
* 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;