From 204220581794a6987b5db6242b048b416113c33f Mon Sep 17 00:00:00 2001 From: Hein Date: Wed, 30 Sep 2026 23:11:30 +0200 Subject: [PATCH] fix(pgsql): return subquery preload errors instead of logging and continuing --- pkg/common/adapters/database/pgsql.go | 6 +- .../adapters/database/pgsql_preload_test.go | 56 +++++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 pkg/common/adapters/database/pgsql_preload_test.go diff --git a/pkg/common/adapters/database/pgsql.go b/pkg/common/adapters/database/pgsql.go index 96b3b5c..8d7ff7a 100644 --- a/pkg/common/adapters/database/pgsql.go +++ b/pkg/common/adapters/database/pgsql.go @@ -1208,7 +1208,7 @@ func (p *PgSQLSelectQuery) applySubqueryPreloads(ctx context.Context, dest inter for i := 0; i < destValue.Len(); i++ { elem := destValue.Index(i) if err := p.loadPreloadsForRecord(ctx, elem, subqueryPreloads); err != nil { - logger.Warn("Failed to load preloads for record %d: %v", i, err) + return fmt.Errorf("record %d: %w", i, err) } } return nil @@ -1256,7 +1256,9 @@ func (p *PgSQLSelectQuery) loadPreloadsForRecord(ctx context.Context, record ref // Build and execute the preload query err := p.executePreloadQuery(ctx, field, meta, fkValue, preload) if err != nil { - logger.Warn("Failed to execute preload query for '%s': %v", preload.relation, err) + // Inside a transaction a failed statement aborts it, so carrying on would + // only turn into a misleading "transaction is aborted" on the next query. + return fmt.Errorf("preload %s: %w", preload.relation, err) } } diff --git a/pkg/common/adapters/database/pgsql_preload_test.go b/pkg/common/adapters/database/pgsql_preload_test.go new file mode 100644 index 0000000..ee3f6e5 --- /dev/null +++ b/pkg/common/adapters/database/pgsql_preload_test.go @@ -0,0 +1,56 @@ +package database + +import ( + "context" + "errors" + "strings" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + + "github.com/bitechdev/ResolveSpec/pkg/common" +) + +type preloadChild struct { + ID int `db:"id"` + UserID int `db:"user_id"` +} + +func (preloadChild) TableName() string { return "children" } + +type preloadParent struct { + ID int `db:"id"` + Children []preloadChild `bun:"rel:has-many,join:ID=user_id"` +} + +func (preloadParent) TableName() string { return "parents" } + +func TestSubqueryPreloadErrorIsReturned(t *testing.T) { + for name, dest := range map[string]interface{}{ + "slice": &[]preloadParent{}, + "single": &preloadParent{}, + } { + t.Run(name, func(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatal(err) + } + defer db.Close() + + mock.ExpectBegin() + mock.ExpectQuery(`FROM parents`).WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(1)) + mock.ExpectQuery(`FROM children`).WillReturnError(errors.New("boom")) + mock.ExpectRollback() + + err = NewPgSQLAdapter(db).RunInTransaction(context.Background(), func(tx common.Database) error { + return tx.NewSelect().Model(&preloadParent{}).PreloadRelation("Children").Scan(context.Background(), dest) + }) + if err == nil || !strings.Contains(err.Error(), "preload Children") { + t.Fatalf("expected the preload error, got %v", err) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Fatal(err) + } + }) + } +}