feat(11-01): add lagoon.OnDatabase and after-commit transactions

- OnDatabase runs a callback once the database is published (now, or when
  lagoon.Publish runs), so GORM callbacks registered at Boot also install
  under serve, where Boot runs before the database is opened
- Transaction runs AfterCommit callbacks in order after a successful commit;
  nested calls are savepoints whose callbacks drop with them
- the lagoon:after_commit GORM callback flushes single-statement AfterCommit
  work after GORM's own commit; outside a transaction it runs immediately
This commit is contained in:
Jakub Zych
2026-09-29 15:25:51 +02:00
parent b319e7cc61
commit 6c1f94e57c
6 changed files with 612 additions and 5 deletions

View File

@@ -0,0 +1,85 @@
package lagoon
import (
"database/sql"
"errors"
"testing"
"git.golem15.com/golem15/summercms/modules/backpack"
"gorm.io/gorm"
)
// TestOnDatabaseAfterActivate covers the production boot order: plugins
// register database hooks at Boot, before serve publishes the database.
func TestOnDatabaseAfterActivate(t *testing.T) {
db, _ := dedicatedDB(t, "lagoon_ondatabase")
gdb, err := Use(t.Context(), db)
if err != nil {
t.Fatal(err)
}
t.Run("before_publish_runs_on_publish", func(t *testing.T) {
app := backpack.New(nil)
calls := 0
var gotSQL *sql.DB
var gotGDB *gorm.DB
if err := OnDatabase(app, func(s *sql.DB, g *gorm.DB) error {
calls++
gotSQL, gotGDB = s, g
return nil
}); err != nil {
t.Fatal(err)
}
if calls != 0 {
t.Fatalf("hook ran %d times before Publish", calls)
}
if err := Publish(app, db, gdb); err != nil {
t.Fatal(err)
}
if calls != 1 || gotSQL != db || gotGDB != gdb {
t.Fatalf("after Publish: calls=%d sql=%v gdb=%v", calls, gotSQL == db, gotGDB == gdb)
}
})
t.Run("after_publish_runs_now", func(t *testing.T) {
app := backpack.New(nil)
if err := Publish(app, db, gdb); err != nil {
t.Fatal(err)
}
calls := 0
if err := OnDatabase(app, func(*sql.DB, *gorm.DB) error { calls++; return nil }); err != nil {
t.Fatal(err)
}
if calls != 1 {
t.Fatalf("hook ran %d times, want 1", calls)
}
})
t.Run("gorm_only_published_runs_now", func(t *testing.T) {
app := backpack.New(nil)
if err := app.Publish(gdb); err != nil {
t.Fatal(err)
}
var gotSQL *sql.DB
if err := OnDatabase(app, func(s *sql.DB, _ *gorm.DB) error { gotSQL = s; return nil }); err != nil {
t.Fatal(err)
}
if gotSQL != db {
t.Fatal("hook did not receive the pool behind the published *gorm.DB")
}
})
t.Run("errors_surface", func(t *testing.T) {
boom := errors.New("boom")
app := backpack.New(nil)
if err := OnDatabase(app, func(*sql.DB, *gorm.DB) error { return boom }); err != nil {
t.Fatal(err)
}
if err := Publish(app, db, gdb); !errors.Is(err, boom) {
t.Fatalf("Publish err = %v, want the hook error", err)
}
if err := OnDatabase(app, func(*sql.DB, *gorm.DB) error { return boom }); !errors.Is(err, boom) {
t.Fatalf("OnDatabase err = %v, want the hook error", err)
}
})
}