More Panic Recovery for reflection on orm

This commit is contained in:
Hein 2025-11-20 15:20:21 +02:00
parent 311e50bfdd
commit 745564f2e7
4 changed files with 173 additions and 28 deletions

View File

@ -9,6 +9,7 @@ import (
"github.com/uptrace/bun" "github.com/uptrace/bun"
"github.com/bitechdev/ResolveSpec/pkg/common" "github.com/bitechdev/ResolveSpec/pkg/common"
"github.com/bitechdev/ResolveSpec/pkg/logger"
"github.com/bitechdev/ResolveSpec/pkg/modelregistry" "github.com/bitechdev/ResolveSpec/pkg/modelregistry"
"github.com/bitechdev/ResolveSpec/pkg/reflection" "github.com/bitechdev/ResolveSpec/pkg/reflection"
) )
@ -43,12 +44,22 @@ func (b *BunAdapter) NewDelete() common.DeleteQuery {
return &BunDeleteQuery{query: b.db.NewDelete()} return &BunDeleteQuery{query: b.db.NewDelete()}
} }
func (b *BunAdapter) Exec(ctx context.Context, query string, args ...interface{}) (common.Result, error) { func (b *BunAdapter) Exec(ctx context.Context, query string, args ...interface{}) (res common.Result, err error) {
defer func() {
if panicErr := logger.RecoverPanic("BunAdapter.Exec"); panicErr != nil {
err = panicErr
}
}()
result, err := b.db.ExecContext(ctx, query, args...) result, err := b.db.ExecContext(ctx, query, args...)
return &BunResult{result: result}, err return &BunResult{result: result}, err
} }
func (b *BunAdapter) Query(ctx context.Context, dest interface{}, query string, args ...interface{}) error { func (b *BunAdapter) Query(ctx context.Context, dest interface{}, query string, args ...interface{}) (err error) {
defer func() {
if panicErr := logger.RecoverPanic("BunAdapter.Query"); panicErr != nil {
err = panicErr
}
}()
return b.db.NewRaw(query, args...).Scan(ctx, dest) return b.db.NewRaw(query, args...).Scan(ctx, dest)
} }
@ -73,7 +84,12 @@ func (b *BunAdapter) RollbackTx(ctx context.Context) error {
return nil return nil
} }
func (b *BunAdapter) RunInTransaction(ctx context.Context, fn func(common.Database) error) error { func (b *BunAdapter) RunInTransaction(ctx context.Context, fn func(common.Database) error) (err error) {
defer func() {
if panicErr := logger.RecoverPanic("BunAdapter.RunInTransaction"); panicErr != nil {
err = panicErr
}
}()
return b.db.RunInTx(ctx, &sql.TxOptions{}, func(ctx context.Context, tx bun.Tx) error { return b.db.RunInTx(ctx, &sql.TxOptions{}, func(ctx context.Context, tx bun.Tx) error {
// Create adapter with transaction // Create adapter with transaction
adapter := &BunTxAdapter{tx: tx} adapter := &BunTxAdapter{tx: tx}
@ -276,15 +292,38 @@ func (b *BunSelectQuery) Having(having string, args ...interface{}) common.Selec
return b return b
} }
func (b *BunSelectQuery) Scan(ctx context.Context, dest interface{}) error { func (b *BunSelectQuery) Scan(ctx context.Context, dest interface{}) (err error) {
defer func() {
if panicErr := logger.RecoverPanic("BunSelectQuery.Scan"); panicErr != nil {
err = panicErr
}
}()
if dest == nil {
return fmt.Errorf("destination cannot be nil")
}
return b.query.Scan(ctx, dest) return b.query.Scan(ctx, dest)
} }
func (b *BunSelectQuery) ScanModel(ctx context.Context) error { func (b *BunSelectQuery) ScanModel(ctx context.Context) (err error) {
defer func() {
if panicErr := logger.RecoverPanic("BunSelectQuery.ScanModel"); panicErr != nil {
err = panicErr
}
}()
if b.query.GetModel() == nil {
return fmt.Errorf("model is nil")
}
return b.query.Scan(ctx) return b.query.Scan(ctx)
} }
func (b *BunSelectQuery) Count(ctx context.Context) (int, error) { func (b *BunSelectQuery) Count(ctx context.Context) (count int, err error) {
defer func() {
if panicErr := logger.RecoverPanic("BunSelectQuery.Count"); panicErr != nil {
err = panicErr
count = 0
}
}()
// If Model() was set, use bun's native Count() which works properly // If Model() was set, use bun's native Count() which works properly
if b.hasModel { if b.hasModel {
count, err := b.query.Count(ctx) count, err := b.query.Count(ctx)
@ -293,15 +332,20 @@ func (b *BunSelectQuery) Count(ctx context.Context) (int, error) {
// Otherwise, wrap as subquery to avoid "Model(nil)" error // Otherwise, wrap as subquery to avoid "Model(nil)" error
// This is needed when only Table() is set without a model // This is needed when only Table() is set without a model
var count int err = b.db.NewSelect().
err := b.db.NewSelect().
TableExpr("(?) AS subquery", b.query). TableExpr("(?) AS subquery", b.query).
ColumnExpr("COUNT(*)"). ColumnExpr("COUNT(*)").
Scan(ctx, &count) Scan(ctx, &count)
return count, err return count, err
} }
func (b *BunSelectQuery) Exists(ctx context.Context) (bool, error) { func (b *BunSelectQuery) Exists(ctx context.Context) (exists bool, err error) {
defer func() {
if panicErr := logger.RecoverPanic("BunSelectQuery.Exists"); panicErr != nil {
err = panicErr
exists = false
}
}()
return b.query.Exists(ctx) return b.query.Exists(ctx)
} }
@ -320,7 +364,6 @@ func (b *BunInsertQuery) Model(model interface{}) common.InsertQuery {
func (b *BunInsertQuery) Table(table string) common.InsertQuery { func (b *BunInsertQuery) Table(table string) common.InsertQuery {
if b.hasModel { if b.hasModel {
// If model is set, do not override table name
return b return b
} }
b.query = b.query.Table(table) b.query = b.query.Table(table)
@ -347,7 +390,12 @@ func (b *BunInsertQuery) Returning(columns ...string) common.InsertQuery {
return b return b
} }
func (b *BunInsertQuery) Exec(ctx context.Context) (common.Result, error) { func (b *BunInsertQuery) Exec(ctx context.Context) (res common.Result, err error) {
defer func() {
if panicErr := logger.RecoverPanic("BunInsertQuery.Exec"); panicErr != nil {
err = panicErr
}
}()
if b.values != nil && len(b.values) > 0 { if b.values != nil && len(b.values) > 0 {
if !b.hasModel { if !b.hasModel {
// If no model was set, use the values map as the model // If no model was set, use the values map as the model
@ -428,7 +476,12 @@ func (b *BunUpdateQuery) Returning(columns ...string) common.UpdateQuery {
return b return b
} }
func (b *BunUpdateQuery) Exec(ctx context.Context) (common.Result, error) { func (b *BunUpdateQuery) Exec(ctx context.Context) (res common.Result, err error) {
defer func() {
if panicErr := logger.RecoverPanic("BunUpdateQuery.Exec"); panicErr != nil {
err = panicErr
}
}()
result, err := b.query.Exec(ctx) result, err := b.query.Exec(ctx)
return &BunResult{result: result}, err return &BunResult{result: result}, err
} }
@ -453,7 +506,12 @@ func (b *BunDeleteQuery) Where(query string, args ...interface{}) common.DeleteQ
return b return b
} }
func (b *BunDeleteQuery) Exec(ctx context.Context) (common.Result, error) { func (b *BunDeleteQuery) Exec(ctx context.Context) (res common.Result, err error) {
defer func() {
if panicErr := logger.RecoverPanic("BunDeleteQuery.Exec"); panicErr != nil {
err = panicErr
}
}()
result, err := b.query.Exec(ctx) result, err := b.query.Exec(ctx)
return &BunResult{result: result}, err return &BunResult{result: result}, err
} }

View File

@ -8,6 +8,7 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
"github.com/bitechdev/ResolveSpec/pkg/common" "github.com/bitechdev/ResolveSpec/pkg/common"
"github.com/bitechdev/ResolveSpec/pkg/logger"
"github.com/bitechdev/ResolveSpec/pkg/modelregistry" "github.com/bitechdev/ResolveSpec/pkg/modelregistry"
"github.com/bitechdev/ResolveSpec/pkg/reflection" "github.com/bitechdev/ResolveSpec/pkg/reflection"
) )
@ -38,12 +39,22 @@ func (g *GormAdapter) NewDelete() common.DeleteQuery {
return &GormDeleteQuery{db: g.db} return &GormDeleteQuery{db: g.db}
} }
func (g *GormAdapter) Exec(ctx context.Context, query string, args ...interface{}) (common.Result, error) { func (g *GormAdapter) Exec(ctx context.Context, query string, args ...interface{}) (res common.Result, err error) {
defer func() {
if panicErr := logger.RecoverPanic("GormAdapter.Exec"); panicErr != nil {
err = panicErr
}
}()
result := g.db.WithContext(ctx).Exec(query, args...) result := g.db.WithContext(ctx).Exec(query, args...)
return &GormResult{result: result}, result.Error return &GormResult{result: result}, result.Error
} }
func (g *GormAdapter) Query(ctx context.Context, dest interface{}, query string, args ...interface{}) error { func (g *GormAdapter) Query(ctx context.Context, dest interface{}, query string, args ...interface{}) (err error) {
defer func() {
if panicErr := logger.RecoverPanic("GormAdapter.Query"); panicErr != nil {
err = panicErr
}
}()
return g.db.WithContext(ctx).Raw(query, args...).Find(dest).Error return g.db.WithContext(ctx).Raw(query, args...).Find(dest).Error
} }
@ -63,7 +74,12 @@ func (g *GormAdapter) RollbackTx(ctx context.Context) error {
return g.db.WithContext(ctx).Rollback().Error return g.db.WithContext(ctx).Rollback().Error
} }
func (g *GormAdapter) RunInTransaction(ctx context.Context, fn func(common.Database) error) error { func (g *GormAdapter) RunInTransaction(ctx context.Context, fn func(common.Database) error) (err error) {
defer func() {
if panicErr := logger.RecoverPanic("GormAdapter.RunInTransaction"); panicErr != nil {
err = panicErr
}
}()
return g.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { return g.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
adapter := &GormAdapter{db: tx} adapter := &GormAdapter{db: tx}
return fn(adapter) return fn(adapter)
@ -255,26 +271,48 @@ func (g *GormSelectQuery) Having(having string, args ...interface{}) common.Sele
return g return g
} }
func (g *GormSelectQuery) Scan(ctx context.Context, dest interface{}) error { func (g *GormSelectQuery) Scan(ctx context.Context, dest interface{}) (err error) {
defer func() {
if panicErr := logger.RecoverPanic("GormSelectQuery.Scan"); panicErr != nil {
err = panicErr
}
}()
return g.db.WithContext(ctx).Find(dest).Error return g.db.WithContext(ctx).Find(dest).Error
} }
func (g *GormSelectQuery) ScanModel(ctx context.Context) error { func (g *GormSelectQuery) ScanModel(ctx context.Context) (err error) {
defer func() {
if panicErr := logger.RecoverPanic("GormSelectQuery.ScanModel"); panicErr != nil {
err = panicErr
}
}()
if g.db.Statement.Model == nil { if g.db.Statement.Model == nil {
return fmt.Errorf("ScanModel requires Model() to be set before scanning") return fmt.Errorf("ScanModel requires Model() to be set before scanning")
} }
return g.db.WithContext(ctx).Find(g.db.Statement.Model).Error return g.db.WithContext(ctx).Find(g.db.Statement.Model).Error
} }
func (g *GormSelectQuery) Count(ctx context.Context) (int, error) { func (g *GormSelectQuery) Count(ctx context.Context) (count int, err error) {
var count int64 defer func() {
err := g.db.WithContext(ctx).Count(&count).Error if panicErr := logger.RecoverPanic("GormSelectQuery.Count"); panicErr != nil {
return int(count), err err = panicErr
count = 0
}
}()
var count64 int64
err = g.db.WithContext(ctx).Count(&count64).Error
return int(count64), err
} }
func (g *GormSelectQuery) Exists(ctx context.Context) (bool, error) { func (g *GormSelectQuery) Exists(ctx context.Context) (exists bool, err error) {
defer func() {
if panicErr := logger.RecoverPanic("GormSelectQuery.Exists"); panicErr != nil {
err = panicErr
exists = false
}
}()
var count int64 var count int64
err := g.db.WithContext(ctx).Limit(1).Count(&count).Error err = g.db.WithContext(ctx).Limit(1).Count(&count).Error
return count > 0, err return count > 0, err
} }
@ -314,7 +352,12 @@ func (g *GormInsertQuery) Returning(columns ...string) common.InsertQuery {
return g return g
} }
func (g *GormInsertQuery) Exec(ctx context.Context) (common.Result, error) { func (g *GormInsertQuery) Exec(ctx context.Context) (res common.Result, err error) {
defer func() {
if panicErr := logger.RecoverPanic("GormInsertQuery.Exec"); panicErr != nil {
err = panicErr
}
}()
var result *gorm.DB var result *gorm.DB
switch { switch {
case g.model != nil: case g.model != nil:
@ -401,7 +444,12 @@ func (g *GormUpdateQuery) Returning(columns ...string) common.UpdateQuery {
return g return g
} }
func (g *GormUpdateQuery) Exec(ctx context.Context) (common.Result, error) { func (g *GormUpdateQuery) Exec(ctx context.Context) (res common.Result, err error) {
defer func() {
if panicErr := logger.RecoverPanic("GormUpdateQuery.Exec"); panicErr != nil {
err = panicErr
}
}()
result := g.db.WithContext(ctx).Updates(g.updates) result := g.db.WithContext(ctx).Updates(g.updates)
return &GormResult{result: result}, result.Error return &GormResult{result: result}, result.Error
} }
@ -428,7 +476,12 @@ func (g *GormDeleteQuery) Where(query string, args ...interface{}) common.Delete
return g return g
} }
func (g *GormDeleteQuery) Exec(ctx context.Context) (common.Result, error) { func (g *GormDeleteQuery) Exec(ctx context.Context) (res common.Result, err error) {
defer func() {
if panicErr := logger.RecoverPanic("GormDeleteQuery.Exec"); panicErr != nil {
err = panicErr
}
}()
result := g.db.WithContext(ctx).Delete(g.model) result := g.db.WithContext(ctx).Delete(g.model)
return &GormResult{result: result}, result.Error return &GormResult{result: result}, result.Error
} }

View File

@ -103,3 +103,14 @@ func CatchPanicCallback(location string, cb func(err any)) {
func CatchPanic(location string) { func CatchPanic(location string) {
CatchPanicCallback(location, nil) CatchPanicCallback(location, nil)
} }
// RecoverPanic recovers from panics and returns an error
// Use this in deferred functions to convert panics into errors
func RecoverPanic(methodName string) error {
if r := recover(); r != nil {
stack := debug.Stack()
Error("Panic in %s: %v\nStack trace:\n%s", methodName, r, string(stack))
return fmt.Errorf("panic in %s: %v", methodName, r)
}
return nil
}

View File

@ -715,11 +715,15 @@ func (h *Handler) getRelationModel(model interface{}, fieldName string) interfac
} }
modelType := reflect.TypeOf(model) modelType := reflect.TypeOf(model)
if modelType == nil {
return nil
}
if modelType.Kind() == reflect.Ptr { if modelType.Kind() == reflect.Ptr {
modelType = modelType.Elem() modelType = modelType.Elem()
} }
if modelType.Kind() != reflect.Struct { if modelType == nil || modelType.Kind() != reflect.Struct {
return nil return nil
} }
@ -731,11 +735,21 @@ func (h *Handler) getRelationModel(model interface{}, fieldName string) interfac
// Get the target type // Get the target type
targetType := field.Type targetType := field.Type
if targetType == nil {
return nil
}
if targetType.Kind() == reflect.Slice { if targetType.Kind() == reflect.Slice {
targetType = targetType.Elem() targetType = targetType.Elem()
if targetType == nil {
return nil
}
} }
if targetType.Kind() == reflect.Ptr { if targetType.Kind() == reflect.Ptr {
targetType = targetType.Elem() targetType = targetType.Elem()
if targetType == nil {
return nil
}
} }
if targetType.Kind() != reflect.Struct { if targetType.Kind() != reflect.Struct {
@ -755,11 +769,20 @@ func (h *Handler) resolveRelationName(model interface{}, nameOrTable string) str
} }
modelType := reflect.TypeOf(model) modelType := reflect.TypeOf(model)
if modelType == nil {
return nameOrTable
}
// Dereference pointer if needed // Dereference pointer if needed
if modelType.Kind() == reflect.Ptr { if modelType.Kind() == reflect.Ptr {
modelType = modelType.Elem() modelType = modelType.Elem()
} }
// Check again after dereferencing
if modelType == nil {
return nameOrTable
}
// Ensure it's a struct // Ensure it's a struct
if modelType.Kind() != reflect.Struct { if modelType.Kind() != reflect.Struct {
return nameOrTable return nameOrTable