Repository navigation
Conversation
…vered savepoint refusal postgres-js records the first failed statement of each begin or savepoint scope and rethrows it when that scope ends, unless the statement ran through one of the driver's own nested savepoint scopes. TypeGraph recovered deliberate statement failures with raw SAVEPOINT and ROLLBACK TO statements on the outer transaction handle, so on postgres-js the caller's transaction rejected at commit with the already-recovered driver error. One savepoint primitive, runSessionSavepoint in src/backend/session-savepoint.ts, now owns savepoint control for the session atomic program, recorded-capture savepoints, and the schema-adoption probe. On a postgres-js transaction it enters the driver's own savepoint scope (Drizzle's nested transaction) inside TypeGraph's savepoint, and the transaction session routes every statement through the innermost open scope's handle while it is open. Releasing TypeGraph's savepoint also releases the driver's, which the driver never releases itself.
…saction The postgres-js transaction session kept its open driver scope in one slot and restored the enclosing scope on exit, which is correct only while scopes nest strictly. Two savepoint-protected writes running concurrently on one transaction can overlap without nesting; when the earlier scope exited first, statements of the still-open scope were routed outside it, and its own exit then restored a scope that had already ended. Open scopes are now a stack. An exit that is not the innermost open scope throws CompilerInvariantError (code OVERLAPPING_SAVEPOINT_SCOPES), and once an ended scope is uncovered every later statement on the session refuses with the same error instead of running on an ended scope or the outer handle.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
postgres-js records the first failed statement of each
beginorsavepointscope and rethrows it when that scope ends, unless the statement ran through one of the driver's own nested savepoint scopes. TypeGraph recovers deliberate statement failures (a fused write program's aborting statement after a lost unique key, an imported edge row the database refuses) with rawSAVEPOINT/ROLLBACK TO SAVEPOINTstatements on the outer transaction handle. So on postgres-js, aUniquenessErrorcaught insidestore.transaction(...)still failed the transaction at commit with the recovered driver error, and animportGraphwhose per-row edge create was refused rolled back the whole import. node-postgres and PGlite committed the same transactions.What changed
Savepoint control now has one owner,
runSessionSavepoint(src/backend/session-savepoint.ts), used by the session atomic program, recorded-capture savepoints (runRecordedTransactionSavepoint) and the schema-adoption probe. It issuesSAVEPOINT/RELEASE/ROLLBACK TOon the session's own control channel. When the session's driver attributes failures to scopes, it enters that driver's own savepoint scope in between. A scope'srollbackdecision, which still returns a value, is carried out of the driver scope as a private throw, because the driver rolls back only on a throw.The PostgreSQL transaction session (
postgres-transaction-session.ts) exposes that driver scope on postgres-js through Drizzle's nestedtransaction(), which is postgres-jssql.savepoint(). While a scope is open, both of the session's execution surfaces send every statement through the nested handle. PostgreSQL already places those statements inside the savepoint whichever handle sent them, so routing them keeps the driver's failure attribution in step with the server, and it needs no second backend object. Other drivers keep the plain adapters and the same raw SQL as before.The driver scope sits inside TypeGraph's savepoint rather than replacing it, because postgres-js never releases its own savepoints. Without the enclosing release, every recovered scope nests one level deeper for the rest of the transaction: 10,000 scopes took 30 s and 147 MB of backend memory on PostgreSQL 18, against 6 s and 47 MB with the enclosing release, which costs one extra round trip per scope. The remaining memory is postgres-js preparing one statement per distinct savepoint name, bounded by the most savepoints a single transaction opens on that connection.
Driver scopes on one transaction must nest strictly. Two savepoint-protected writes running concurrently on one transaction (for example an
importGraphand abulkReplaceByIdunder onePromise.all) can overlap without nesting, which raw savepoints already mishandle on node-postgres. On postgres-js, the scope that exits out of order now throwsCompilerInvariantError(OVERLAPPING_SAVEPOINT_SCOPES), and every later statement on that transaction refuses with the same error. Without this, statements would silently run outside their savepoint or on a scope that has already ended.A savepoint recovery that itself fails is reported as
SavepointRecoveryFailure(anAggregateErrornaming both failures). The atomic program still wraps it inCompilerInvariantError. A failedRELEASEis now rolled back before it propagates, which recorded capture previously skipped.The new shared tests (
savepoint-recovery.ts) fail on the postgres-js lane on main, with the issue's23502forbulkReplaceByIdand with23505for the import, and pass on every lane with the fix. Disabling the driver scope at either site brings its failure back. A new shared adoption test refuses a native transaction handle whose transaction already ended, and fails on all three PostgreSQL lanes when the probe is removed. A unit test overlaps two driver scopes and asserts the invariant error, along with the refusal of every later statement; it fails against the previous single-slot session.Closes #787