fix(11-07): hand after-commit callbacks a clean statement

- lagoon.Transaction, the lagoon:after_commit flush and the immediate
  AfterCommit path pass a handle with an empty statement on the write's
  connection (Session NewDB+Context, Clauses(), Session NewDB)
- a WithContext query through the handle no longer continues from the
  written model's statement (deferred from 11-05)
- TestTransactionAfterCommit/callback_handle_has_a_clean_statement covers
  the implicit, plain-transaction and lagoon.Transaction paths
This commit is contained in:
Jakub Zych
2026-09-30 14:01:22 +02:00
parent 6562d94ef3
commit c544319cec
3 changed files with 96 additions and 7 deletions

View File

@@ -70,7 +70,7 @@ func Transaction(ctx context.Context, gdb *gorm.DB, fn func(ctx context.Context,
}); err != nil {
return err
}
runAfterCommit(ctx, gdb.Session(&gorm.Session{NewDB: true, Context: ctx}), buf.take())
runAfterCommit(ctx, cleanHandle(gdb, ctx), buf.take())
return nil
}
@@ -80,7 +80,13 @@ func Transaction(ctx context.Context, gdb *gorm.DB, fn func(ctx context.Context,
// example from a GORM create callback) it runs after that commit through the
// AfterCommitCallback callback, and not at all when the write fails.
// Anywhere else, including inside a plain gorm Transaction, fn runs
// immediately with db.
// immediately on db's connection.
//
// The handle fn receives always has an empty statement on the connection
// the work belongs to (the pool after a commit, the open transaction
// inside a plain gorm Transaction), whatever handle AfterCommit was called
// with. Queries through it never continue from the written model's
// statement, even when they start with WithContext.
func AfterCommit(ctx context.Context, db *gorm.DB, fn func(ctx context.Context, db *gorm.DB)) {
if fn == nil {
return
@@ -108,7 +114,7 @@ func AfterCommit(ctx context.Context, db *gorm.DB, fn func(ctx context.Context,
return
}
}
runAfterCommit(ctx, db, []func(context.Context, *gorm.DB){fn})
runAfterCommit(ctx, cleanHandle(db, ctx), []func(context.Context, *gorm.DB){fn})
}
func bufferFrom(ctx context.Context, db *gorm.DB) *afterCommitBuffer {
@@ -143,7 +149,21 @@ func flushStatementAfterCommit(db *gorm.DB) {
if ctx == nil {
ctx = context.Background()
}
runAfterCommit(ctx, db.Session(&gorm.Session{NewDB: true, Context: ctx}), fns)
runAfterCommit(ctx, cleanHandle(db, ctx), fns)
}
// cleanHandle returns a handle on db's connection with an empty statement.
// db.Session with NewDB and a Context is not enough on a callback's
// handle: the Context makes it clone the write's statement (model, table,
// clauses), and a later WithContext on the result continues from that
// clone, so a query would run against the written model's table. Clauses()
// starts a fresh statement on the same connection, and the final Session
// makes the next chained call start from it again. A nil db stays nil.
func cleanHandle(db *gorm.DB, ctx context.Context) *gorm.DB {
if db == nil {
return nil
}
return db.Session(&gorm.Session{NewDB: true, Context: ctx}).Clauses().Session(&gorm.Session{NewDB: true})
}
func runAfterCommit(ctx context.Context, db *gorm.DB, fns []func(context.Context, *gorm.DB)) {