Describe the bug
Summary
When relaxedDurability: false, successful top-level query() and exec() calls await syncToFs() before returning. transaction() does not provide the equivalent synchronization boundary.
Inside a transaction, #runQuery() and #runExec() correctly suppress synchronization while #inTransaction is true. However, transaction() executes COMMIT while that flag is still true, then clears the flag and returns without calling syncToFs().
To Reproduce - include code sample(s)
As a result, this can resolve without synchronizing the filesystem:
await pg.transaction(async (tx) => {
await tx.exec("INSERT INTO example ...");
});
// No syncToFs() has occurred after COMMIT.
Details
Additional context
BEGIN currently causes a sync before #inTransaction is set, but there is no sync after the transaction’s filesystem changes.
Impact
This is significant for custom filesystem implementations where syncToFs() is the awaited durability and health boundary.
Even with relaxedDurability: false:
await pg.transaction(...) can resolve before the committed transaction is persisted by the filesystem.
- Worker, tab, or process termination immediately afterwards can lose work that callers reasonably believed had crossed the same boundary as a successful
query() or exec().
- Filesystem synchronization failures or latched health failures are not delivered until a later top-level operation calls
syncToFs().
This makes transaction completion behave differently from other successful PGlite operations.
Relevant control flow
#runQuery() and #runExec() contain:
if (!this.#inTransaction) {
await this.syncToFs();
}
But the successful transaction path is effectively:
await this.#runExec("COMMIT"); // #inTransaction is still true
this.#inTransaction = false;
return result; // No syncToFs()
Explicit rollback and exception-driven rollback have the same missing terminal synchronization boundary.
Expected behavior
Before transaction() resolves, its terminal path should perform the normal awaited filesystem synchronization after leaving the in-transaction state.
Ideally, this should happen while the transaction exclusivity boundary is still held, so another operation cannot interleave between the transaction ending and its filesystem sync.
The relevant paths are:
- successful
COMMIT;
- explicit
tx.rollback();
- exception-driven
ROLLBACK.
Error precedence on the rollback/exception path may need an explicit decision, but the filesystem should at least receive the terminal sync or health check before transaction exclusivity is released.
Describe the bug
Summary
When
relaxedDurability: false, successful top-levelquery()andexec()calls awaitsyncToFs()before returning.transaction()does not provide the equivalent synchronization boundary.Inside a transaction,
#runQuery()and#runExec()correctly suppress synchronization while#inTransactionistrue. However,transaction()executesCOMMITwhile that flag is still true, then clears the flag and returns without callingsyncToFs().To Reproduce - include code sample(s)
As a result, this can resolve without synchronizing the filesystem:
Details
Additional context
BEGIN currently causes a sync before #inTransaction is set, but there is no sync after the transaction’s filesystem changes.
Impact
This is significant for custom filesystem implementations where
syncToFs()is the awaited durability and health boundary.Even with
relaxedDurability: false:await pg.transaction(...)can resolve before the committed transaction is persisted by the filesystem.query()orexec().syncToFs().This makes transaction completion behave differently from other successful PGlite operations.
Relevant control flow
#runQuery()and#runExec()contain:But the successful transaction path is effectively:
Explicit rollback and exception-driven rollback have the same missing terminal synchronization boundary.
Expected behavior
Before
transaction()resolves, its terminal path should perform the normal awaited filesystem synchronization after leaving the in-transaction state.Ideally, this should happen while the transaction exclusivity boundary is still held, so another operation cannot interleave between the transaction ending and its filesystem sync.
The relevant paths are:
COMMIT;tx.rollback();ROLLBACK.Error precedence on the rollback/exception path may need an explicit decision, but the filesystem should at least receive the terminal sync or health check before transaction exclusivity is released.