From 7487d83b31d0f5fed098e0ac9cfc37872ea47cfa Mon Sep 17 00:00:00 2001 From: Zihao Peng <79237158+xiaobaZeo@users.noreply.github.com> Date: Mon, 7 Sep 2026 10:05:02 +0800 Subject: [PATCH] fix(db): ensure transaction depth resets when rollback fails Guarantee that NodeSqliteAdapter._txDepth always resets to 0 via try/finally, preventing transaction isolation loss on subsequent operations if SQLite aborts the transaction before rollback completes. Co-Authored-By: Claude Code --- CHANGELOG.md | 2 ++ __tests__/sqlite-backend.test.ts | 53 ++++++++++++++++++++++++++++++++ src/db/sqlite-adapter.ts | 14 ++++++--- 3 files changed, 65 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1815c4150..2ffa28879 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -215,6 +215,8 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - Fixed a long-running `codegraph ui` session serving a symbol that a sync had already deleted. The viewer keeps one connection to your index open, and its in-memory lookup didn't notice when another process — your agent's sync, or `codegraph sync` — rewrote the file underneath it, so a symbol screen could keep showing a body with no callers while search correctly reported it had moved. Because a symbol's identity includes the line it starts on, this happened after almost any edit above it. +- **A database transaction error cannot leave subsequent operations uncommitted.** When an operation inside a transaction fails and the database aborts the transaction before a rollback finishes, the connection's transaction counter is now guaranteed to reset so later transactions on the same connection preserve their ACID isolation. + ## [1.6.0] - 2026-08-26 ### Highlights diff --git a/__tests__/sqlite-backend.test.ts b/__tests__/sqlite-backend.test.ts index 0815551dc..e532adbc6 100644 --- a/__tests__/sqlite-backend.test.ts +++ b/__tests__/sqlite-backend.test.ts @@ -41,4 +41,57 @@ describe('DatabaseConnection — backend reporting', () => { cg.destroy(); } }); + + describe('transaction depth and rollback safety (CG-SQLITE-TX-01)', () => { + it('resets transaction depth to 0 on standard error rollback and allows subsequent transactions', () => { + const conn = DatabaseConnection.initialize(path.join(dir, 'tx-test.db')); + const db = (conn as any).db; + db.exec('CREATE TABLE items (id INT, val TEXT)'); + + const failingTx = db.transaction(() => { + db.exec("INSERT INTO items VALUES (1, 'one')"); + throw new Error('boom'); + }); + + expect(() => failingTx()).toThrow('boom'); + + // Subsequent transaction should work normally, demonstrating _txDepth reset to 0 + const successTx = db.transaction(() => { + db.exec("INSERT INTO items VALUES (2, 'two')"); + }); + expect(() => successTx()).not.toThrow(); + + const rows = db.prepare('SELECT * FROM items').all(); + expect(rows).toHaveLength(1); + expect((rows[0] as any).id).toBe(2); + conn.close(); + }); + + it('resets transaction depth to 0 even if ROLLBACK fails or transaction was already aborted', () => { + const conn = DatabaseConnection.initialize(path.join(dir, 'tx-abort-test.db')); + const db = (conn as any).db; + db.exec('CREATE TABLE items (id INT, val TEXT)'); + + // Simulate a case where SQLite aborts the transaction or ROLLBACK is invoked when no tx is active + const doubleAbortTx = db.transaction(() => { + db.exec("INSERT INTO items VALUES (1, 'one')"); + // Manually rollback inside, so when the outer transaction block tries to ROLLBACK, it errors: + (db as any)._db.exec('ROLLBACK'); + throw new Error('closure error after manual abort'); + }); + + expect(() => doubleAbortTx()).toThrow('closure error after manual abort'); + + // Verify that _txDepth is safely reset to 0 and a new transaction can be created and committed + const nextTx = db.transaction(() => { + db.exec("INSERT INTO items VALUES (3, 'three')"); + }); + expect(() => nextTx()).not.toThrow(); + + const rows = db.prepare('SELECT * FROM items').all(); + expect(rows).toHaveLength(1); + expect((rows[0] as any).id).toBe(3); + conn.close(); + }); + }); }); diff --git a/src/db/sqlite-adapter.ts b/src/db/sqlite-adapter.ts index 2ca02e5be..c49c9992e 100644 --- a/src/db/sqlite-adapter.ts +++ b/src/db/sqlite-adapter.ts @@ -124,15 +124,21 @@ class NodeSqliteAdapter implements SqliteDatabase { } this._db.exec('BEGIN'); this._txDepth = 1; + let committed = false; try { const result = fn(...args); this._db.exec('COMMIT'); - this._txDepth = 0; + committed = true; return result; - } catch (error) { - this._db.exec('ROLLBACK'); + } finally { + if (!committed) { + try { + this._db.exec('ROLLBACK'); + } catch { + // Ignore rollback errors if SQLite automatically aborted/rolled back + } + } this._txDepth = 0; - throw error; } }; }