Repository navigation
fix(postgres): reject work that caught a failed statement instead of committing nothing - #32
Merged
Merged
Conversation
…committing nothing PostgreSQL ends a transaction at its first failed statement and answers its COMMIT with a ROLLBACK, no error. So a store's work that caught a statement's error (an insert-if-absent catching 23505, say) and resolved made transaction() resolve as if committed, while every write in it was gone; on every PostgreSQL executor. Each now runs the work through runTransactionWork(): when the work's last statement failed, one statement more tells whether PostgreSQL ended the transaction, and the transaction then rolls back and rejects, with the statement's error as the cause. Work that recovered with ROLLBACK TO SAVEPOINT commits as before, and the normal path costs nothing. The contract's transactions case covers both, for every executor that runs it. fromPg(): an unknown isolation level is refused before the transaction takes a connection (it waited for one, and the pool then destroyed it), and a COMMIT that fails with an ERROR (a serialization failure, a deferred constraint) keeps a connection that one more statement shows is idle, instead of reconnecting for every such failure.
…t it fails an executor committing caught failures
…commit # Conflicts: # lib/postgres/executors/pg.executor.ts
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.
PR Checklist
Please check if your PR fulfills the following requirements:
PR Type
What kind of change does this PR introduce?
What is the current behavior?
Issue Number: N/A
Three issues the review of #31 found in the PostgreSQL executors' transactions, none introduced by it:
COMMITwith aROLLBACK, no error. A store's work that catches a statement's error (an insert-if-absent catching 23505, a "skip if locked" catching 55P03) and resolves makestransaction()resolve as if committed, while every write in it is gone. This is true of every PostgreSQL executor: fromPg (pool and client), fromDrizzle, fromKysely, fromTypeOrm, fromPrisma. The same work is fine on MySQL, which doesn't end a transaction at a failed statement.fromPg()checks the isolation level only afterpool.connect(), inside BEGIN'stry. An unknown level waits for a connection, can surface as a pool timeout instead of theTypeError, and has the pool destroy the healthy connection it took. Every other executor checks it first.fromPg()marks every failedCOMMITbroken, so the pool destroys connections PostgreSQL left idle and usable after an ERROR at commit (a serialization failure under SERIALIZABLE, a deferred constraint): a retry loop reconnects on each one.What is the new behavior?
runTransactionWork(). When the work's last statement failed, one statement more tells whether PostgreSQL ended the transaction (a statement the driver failed before sending leaves it as it was). If it did, the transaction rolls back and rejects, with the statement's error as the cause. Work that recovered withROLLBACK TO SAVEPOINTcommits as before. The usual path costs nothing.transactionscase (@nestjs/store-kit/testing) now covers both: work that swallowed a failed statement must reject and leave no rows, and work that rolled back to a savepoint must commit.SqlExecutor.transaction()'s doc says so.fromPg()buildsBEGINbefore taking a connection (PgClientExecutor.transaction()isasyncnow, so the refusal is still a rejection). A failedCOMMITkeeps its connection when one more statement shows it's idle.Checked locally against PGlite, as the sandbox here can't start PostgreSQL. The contract ran through
fromDrizzle(PGlite)as in CI. The other executors ran through node-postgres bridged in memory to PGlite: the full contract for fromPg (pool and client), and the transactions case for Kysely, TypeORM and Prisma. Every executor fails the new case without the change and passes with it. Also checked: a deferred-constraintCOMMITfailure keeps the pool's connection, and an unknown level rejects at once while the pool is exhausted. Both fail on master. CI's PostgreSQL run is the first on a real server.Does this PR introduce a breaking change?
Other information
transaction()now rejects work it used to resolve, but that work committed nothing, so a caller only loses a false success. A third-party executor that runs the published contract gets the new requirement in itstransactionscase.This conflicts with #31 in
PgPoolExecutor.transaction()andrunTransaction()(#31 checks the connection out through pg-pool's callback and wrapsquery). Whichever merges second needs a rebase:runTransaction()takes theBEGINtext, and the wrapper #31 passes it has nogetTransactionStatus(), which the failed-COMMITcheck treats as idle once its statement succeeds.