- 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% 保持
541 lines
24 KiB
TypeScript
541 lines
24 KiB
TypeScript
/**
|
||
* v0.7.2 修复回归测试
|
||
*
|
||
* 覆盖:
|
||
* - UPDATE 语句级原子性(多行更新撞唯一约束 → 整句拒绝,四引擎)
|
||
* - 批内唯一互查(两行同时更新到同一新唯一值 → 整句拒绝)
|
||
* - 事务内 DDL 拒绝(ALTER TABLE / CREATE INDEX / DROP INDEX,Memory/KVStore 对齐 Aria)
|
||
* - SET NULL 级联撞 required 列 → FOREIGN_KEY_VIOLATION(delete 与 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;
|