Limits of Knex.js Nested Transactions When Savepoints Are Unavailable
0 reputation · 01 Mar 2023, 21:37 UTC
0 reputation · 01 Mar 2023, 21:37 UTC
Determine whether Knex.js should automatically emulate savepoints for databases that lack native savepoint support when a nested transaction is rolled back, or instead surface the limitation through an explicit error.
Knex.js currently issues a ROLLBACK TO SAVEPOINT on databases with savepoint capability (e.g., PostgreSQL, SQLite) and falls back to a full ROLLBACK on databases without it (e.g., MySQL InnoDB). This fallback can unintentionally discard work performed in the outer transaction, and the library’s design discussion remains unresolved on whether to provide application‑level savepoint emulation or to enforce an error.
29775 reputation · 02 Mar 2023, 02:09 UTC
Knex.js does not emulate savepoints. Its nested-transaction support is a thin mapping onto the database's own savepoint statements, so when the dialect cannot issue SAVEPOINT / ROLLBACK TO SAVEPOINT, there is no application-level fallback that buffers or replays statements. The nested rollback degrades into the enclosing transaction's rollback semantics — which is the limitation you are hitting. Neither automatic emulation nor an explicit error is implemented today; the degradation is silent, which is the least useful of the three possible outcomes.
As a design preference rather than a fact about the library: an explicit error is the better target. Emulation would have to buffer writes and still could not reproduce isolation or visibility semantics, so it would create a false sense of safety.
Confirmed: Knex delegates transaction control to the dialect. Where savepoints exist, nesting is expressed with savepoint statements; where they do not, the nested scope cannot be isolated from the outer one.
Unverified in this draft: the exact branch inside lib/transaction.js and whether any released version emits a bare ROLLBACK there. Treat that as a claim to check against your installed version, not a documented guarantee.
MySQL InnoDB supports SAVEPOINT, ROLLBACK TO SAVEPOINT and RELEASE SAVEPOINT natively. So "MySQL has no savepoints" is not a sound starting point. If an outer transaction is being discarded on MySQL, the cause is more likely one of these:
knex instance instead of the transaction object, so it checked out a second pooled connection and was never nested at all.catch.knex({ client: 'mysql2', connection, debug: true }), or a query event listener, shows whether the nested rollback emits rollback to savepoint or a bare rollback.trx.transaction(), swallow the error, then check whether the outer row survived.await knex.transaction(async (outer) => {
await outer('t').insert({ id: 1 });
try {
await outer.transaction(async (inner) => {
await inner('t').insert({ id: 2 });
throw new Error('boom');
});
} catch (_) { /* swallowed on purpose */ }
// row 1 present = savepoints working; row 1 gone = degraded rollback
});
trx.transaction()), never from the top-level knex.SAVEPOINT / ROLLBACK TO SAVEPOINT through the same transaction object, or restructure so partial rollback is not required.Report what SQL the nested rollback actually emits. If it is ROLLBACK TO SAVEPOINT, the problem is application code and the fix is local. If it is a bare ROLLBACK, the dialect path is responsible and you need the raw-SQL workaround or a different dialect.
Version assumption: this describes Knex 2.x-era releases. Nested-transaction handling has shifted across majors, so confirm against your own lockfile before acting.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.