/** * v0.8.0 回归 —— 语句级原子性与 ALTER 约束(四引擎一致性) * * A1 UPDATE 批内主键碰撞静默丢行 * 阶段 1 只用 `table.has(newPk)` 与**语句执行前**的表比对,看不到同一语句内其它行 * 即将写入的新主键;阶段 2 逐行写同一个 key 互相覆盖。实测 `UPDATE t SET id='X'` * (匹配 3 行)返回 affected=3,而表中只剩 1 行。 * INSERT 路径在 v0.7.3 已做批内相互检查,UPDATE 漏了。 * * A2 ALTER ADD COLUMN ... UNIQUE 形同虚设 * 只写 schema 而不建索引桶/索引 LSM,而唯一性预检完全依赖索引 —— 重复值可任意写入。 * Memory 侧后果更严重:重启时 createTable 依 schema 建桶、回灌第二行触发 * UNIQUE_VIOLATION,而该异常被引擎 open 路径吞掉 → **行静默消失**(实测 2 行变 1 行)。 * * 两者都在四个引擎上验证:memory / disk / hybrid / aria。 */ import { MetonaSqlark } from '../src/core'; const MODES: Array<[string, Record]> = [ ['memory', {}], ['disk', {}], ['hybrid', {}], ['aria', { diskEngine: 'memory' }], ]; describe('[v0.8.0] A1 UPDATE 批内主键碰撞', () => { test('多行改到同一新主键 → 整体拒绝且不丢行', async () => { for (const [mode, extra] of MODES) { const db = await MetonaSqlark.create({ name: `v080-a1-${mode}`, mode, ...extra } as never); await db.defineTable('t', { id: { type: 'string', primaryKey: true }, v: { type: 'number' } }); await db.query("INSERT INTO t VALUES ('a',1),('b',2),('c',3)"); await expect(db.query("UPDATE t SET id = 'X'")) .rejects.toMatchObject({ code: 'DUPLICATE_KEY' }); const rows = await db.query('SELECT * FROM t ORDER BY id') as Array<{ id: string }>; // 关键:三行必须全部保留(此前会被逐行覆盖成 1 行) expect(rows.map((r) => r.id)).toEqual(['a', 'b', 'c']); await db.close(); } }); test('合法的单行主键变更仍然可用', async () => { for (const [mode, extra] of MODES) { const db = await MetonaSqlark.create({ name: `v080-a1b-${mode}`, mode, ...extra } as never); await db.defineTable('t', { id: { type: 'string', primaryKey: true }, v: { type: 'number' } }); await db.query("INSERT INTO t VALUES ('a',1),('b',2)"); const affected = await db.query("UPDATE t SET id = 'z' WHERE id = 'a'"); expect(affected).toBe(1); const rows = await db.query('SELECT * FROM t ORDER BY id') as Array<{ id: string }>; expect(rows.map((r) => r.id)).toEqual(['b', 'z']); await db.close(); } }); }); describe('[v0.8.0] A2 ALTER ADD UNIQUE 约束', () => { test('ALTER 添加的 UNIQUE 必须真正生效', async () => { for (const [mode, extra] of MODES) { const db = await MetonaSqlark.create({ name: `v080-a2-${mode}`, mode, ...extra } as never); await db.defineTable('t', { id: { type: 'string', primaryKey: true } }); await db.query("INSERT INTO t VALUES ('1')"); await db.query('ALTER TABLE t ADD COLUMN email STRING UNIQUE'); await db.query("UPDATE t SET email = 'a@x.com' WHERE id = '1'"); // 重复值必须被拒绝(此前四个引擎全部静默接受) await expect(db.query("INSERT INTO t VALUES ('2', 'a@x.com')")) .rejects.toMatchObject({ code: 'UNIQUE_VIOLATION' }); const rows = await db.query('SELECT COUNT(*) AS c FROM t') as Array<{ c: number }>; expect(rows[0].c).toBe(1); await db.close(); } }); test('CREATE UNIQUE INDEX 存量重复必须报错,且数据不被破坏', async () => { for (const [mode, extra] of MODES) { const db = await MetonaSqlark.create({ name: `v080-a2r-${mode}`, mode, ...extra } as never); await db.defineTable('t', { id: { type: 'string', primaryKey: true }, email: { type: 'string' } }); await db.query("INSERT INTO t VALUES ('1','dup@x'),('2','dup@x')"); await expect(db.query('CREATE UNIQUE INDEX uq ON t (email)')) .rejects.toMatchObject({ code: 'UNIQUE_VIOLATION' }); // 失败不得破坏既有数据 const rows = await db.query('SELECT COUNT(*) AS c FROM t') as Array<{ c: number }>; expect(rows[0].c).toBe(2); await db.close(); } }); });