mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2025-12-31 08:44:25 +00:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
59bd709460 | ||
|
|
05962035b6 | ||
|
|
1cd04b7083 | ||
|
|
0d4909054c | ||
|
|
745564f2e7 |
@@ -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 r := recover(); r != nil {
|
||||||
|
err = logger.HandlePanic("BunAdapter.Exec", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
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 r := recover(); r != nil {
|
||||||
|
err = logger.HandlePanic("BunAdapter.Query", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
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 r := recover(); r != nil {
|
||||||
|
err = logger.HandlePanic("BunAdapter.RunInTransaction", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
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}
|
||||||
@@ -219,6 +235,11 @@ func (b *BunSelectQuery) Preload(relation string, conditions ...interface{}) com
|
|||||||
|
|
||||||
func (b *BunSelectQuery) PreloadRelation(relation string, apply ...func(common.SelectQuery) common.SelectQuery) common.SelectQuery {
|
func (b *BunSelectQuery) PreloadRelation(relation string, apply ...func(common.SelectQuery) common.SelectQuery) common.SelectQuery {
|
||||||
b.query = b.query.Relation(relation, func(sq *bun.SelectQuery) *bun.SelectQuery {
|
b.query = b.query.Relation(relation, func(sq *bun.SelectQuery) *bun.SelectQuery {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
logger.HandlePanic("BunSelectQuery.PreloadRelation", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
if len(apply) == 0 {
|
if len(apply) == 0 {
|
||||||
return sq
|
return sq
|
||||||
}
|
}
|
||||||
@@ -276,15 +297,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 r := recover(); r != nil {
|
||||||
|
err = logger.HandlePanic("BunSelectQuery.Scan", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
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 r := recover(); r != nil {
|
||||||
|
err = logger.HandlePanic("BunSelectQuery.ScanModel", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
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 r := recover(); r != nil {
|
||||||
|
err = logger.HandlePanic("BunSelectQuery.Count", r)
|
||||||
|
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 +337,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 r := recover(); r != nil {
|
||||||
|
err = logger.HandlePanic("BunSelectQuery.Exists", r)
|
||||||
|
exists = false
|
||||||
|
}
|
||||||
|
}()
|
||||||
return b.query.Exists(ctx)
|
return b.query.Exists(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -320,7 +369,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 +395,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 r := recover(); r != nil {
|
||||||
|
err = logger.HandlePanic("BunInsertQuery.Exec", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
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 +481,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 r := recover(); r != nil {
|
||||||
|
err = logger.HandlePanic("BunUpdateQuery.Exec", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
result, err := b.query.Exec(ctx)
|
result, err := b.query.Exec(ctx)
|
||||||
return &BunResult{result: result}, err
|
return &BunResult{result: result}, err
|
||||||
}
|
}
|
||||||
@@ -453,7 +511,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 r := recover(); r != nil {
|
||||||
|
err = logger.HandlePanic("BunDeleteQuery.Exec", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
result, err := b.query.Exec(ctx)
|
result, err := b.query.Exec(ctx)
|
||||||
return &BunResult{result: result}, err
|
return &BunResult{result: result}, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 r := recover(); r != nil {
|
||||||
|
err = logger.HandlePanic("GormAdapter.Exec", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
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 r := recover(); r != nil {
|
||||||
|
err = logger.HandlePanic("GormAdapter.Query", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
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 r := recover(); r != nil {
|
||||||
|
err = logger.HandlePanic("GormAdapter.RunInTransaction", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
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 r := recover(); r != nil {
|
||||||
|
err = logger.HandlePanic("GormSelectQuery.Scan", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
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 r := recover(); r != nil {
|
||||||
|
err = logger.HandlePanic("GormSelectQuery.ScanModel", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
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 r := recover(); r != nil {
|
||||||
return int(count), err
|
err = logger.HandlePanic("GormSelectQuery.Count", r)
|
||||||
|
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 r := recover(); r != nil {
|
||||||
|
err = logger.HandlePanic("GormSelectQuery.Exists", r)
|
||||||
|
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 r := recover(); r != nil {
|
||||||
|
err = logger.HandlePanic("GormInsertQuery.Exec", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
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 r := recover(); r != nil {
|
||||||
|
err = logger.HandlePanic("GormUpdateQuery.Exec", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
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 r := recover(); r != nil {
|
||||||
|
err = logger.HandlePanic("GormDeleteQuery.Exec", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|||||||
136
pkg/common/sql_helpers.go
Normal file
136
pkg/common/sql_helpers.go
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/bitechdev/ResolveSpec/pkg/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ValidateAndFixPreloadWhere validates that the WHERE clause for a preload contains
|
||||||
|
// the relation prefix (alias). If not present, it attempts to add it to column references.
|
||||||
|
// Returns the fixed WHERE clause and an error if it cannot be safely fixed.
|
||||||
|
func ValidateAndFixPreloadWhere(where string, relationName string) (string, error) {
|
||||||
|
if where == "" {
|
||||||
|
return where, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the relation name is already present in the WHERE clause
|
||||||
|
lowerWhere := strings.ToLower(where)
|
||||||
|
lowerRelation := strings.ToLower(relationName)
|
||||||
|
|
||||||
|
// Check for patterns like "relation.", "relation ", or just "relation" followed by a dot
|
||||||
|
if strings.Contains(lowerWhere, lowerRelation+".") ||
|
||||||
|
strings.Contains(lowerWhere, "`"+lowerRelation+"`.") ||
|
||||||
|
strings.Contains(lowerWhere, "\""+lowerRelation+"\".") {
|
||||||
|
// Relation prefix is already present
|
||||||
|
return where, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the WHERE clause is complex (contains OR, parentheses, subqueries, etc.),
|
||||||
|
// we can't safely auto-fix it - require explicit prefix
|
||||||
|
if strings.Contains(lowerWhere, " or ") ||
|
||||||
|
strings.Contains(where, "(") ||
|
||||||
|
strings.Contains(where, ")") {
|
||||||
|
return "", fmt.Errorf("preload WHERE condition must reference the relation '%s' (e.g., '%s.column_name'). Complex WHERE clauses with OR/parentheses must explicitly use the relation prefix", relationName, relationName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to add the relation prefix to simple column references
|
||||||
|
// This handles basic cases like "column = value" or "column = value AND other_column = value"
|
||||||
|
// Split by AND to handle multiple conditions (case-insensitive)
|
||||||
|
originalConditions := strings.Split(where, " AND ")
|
||||||
|
|
||||||
|
// If uppercase split didn't work, try lowercase
|
||||||
|
if len(originalConditions) == 1 {
|
||||||
|
originalConditions = strings.Split(where, " and ")
|
||||||
|
}
|
||||||
|
|
||||||
|
fixedConditions := make([]string, 0, len(originalConditions))
|
||||||
|
|
||||||
|
for _, cond := range originalConditions {
|
||||||
|
cond = strings.TrimSpace(cond)
|
||||||
|
if cond == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this condition already has a table prefix (contains a dot)
|
||||||
|
if strings.Contains(cond, ".") {
|
||||||
|
fixedConditions = append(fixedConditions, cond)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this is a SQL expression/literal that shouldn't be prefixed
|
||||||
|
lowerCond := strings.ToLower(strings.TrimSpace(cond))
|
||||||
|
if IsSQLExpression(lowerCond) {
|
||||||
|
// Don't prefix SQL expressions like "true", "false", "1=1", etc.
|
||||||
|
fixedConditions = append(fixedConditions, cond)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the column name (first identifier before operator)
|
||||||
|
columnName := ExtractColumnName(cond)
|
||||||
|
if columnName == "" {
|
||||||
|
// Can't identify column name, require explicit prefix
|
||||||
|
return "", fmt.Errorf("preload WHERE condition must reference the relation '%s' (e.g., '%s.column_name'). Cannot auto-fix condition: %s", relationName, relationName, cond)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add relation prefix to the column name only
|
||||||
|
fixedCond := strings.Replace(cond, columnName, relationName+"."+columnName, 1)
|
||||||
|
fixedConditions = append(fixedConditions, fixedCond)
|
||||||
|
}
|
||||||
|
|
||||||
|
fixedWhere := strings.Join(fixedConditions, " AND ")
|
||||||
|
logger.Debug("Auto-fixed preload WHERE clause: '%s' -> '%s'", where, fixedWhere)
|
||||||
|
return fixedWhere, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsSQLExpression checks if a condition is a SQL expression that shouldn't be prefixed
|
||||||
|
func IsSQLExpression(cond string) bool {
|
||||||
|
// Common SQL literals and expressions
|
||||||
|
sqlLiterals := []string{"true", "false", "null", "1=1", "1 = 1", "0=0", "0 = 0"}
|
||||||
|
for _, literal := range sqlLiterals {
|
||||||
|
if cond == literal {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtractColumnName extracts the column name from a WHERE condition
|
||||||
|
// For example: "status = 'active'" returns "status"
|
||||||
|
func ExtractColumnName(cond string) string {
|
||||||
|
// Common SQL operators
|
||||||
|
operators := []string{" = ", " != ", " <> ", " > ", " >= ", " < ", " <= ", " LIKE ", " like ", " IN ", " in ", " IS ", " is "}
|
||||||
|
|
||||||
|
for _, op := range operators {
|
||||||
|
if idx := strings.Index(cond, op); idx > 0 {
|
||||||
|
columnName := strings.TrimSpace(cond[:idx])
|
||||||
|
// Remove quotes if present
|
||||||
|
columnName = strings.Trim(columnName, "`\"'")
|
||||||
|
return columnName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no operator found, check if it's a simple identifier (for boolean columns)
|
||||||
|
parts := strings.Fields(cond)
|
||||||
|
if len(parts) > 0 {
|
||||||
|
columnName := strings.Trim(parts[0], "`\"'")
|
||||||
|
// Check if it's a valid identifier (not a SQL keyword)
|
||||||
|
if !IsSQLKeyword(strings.ToLower(columnName)) {
|
||||||
|
return columnName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsSQLKeyword checks if a string is a SQL keyword that shouldn't be treated as a column name
|
||||||
|
func IsSQLKeyword(word string) bool {
|
||||||
|
keywords := []string{"select", "from", "where", "and", "or", "not", "in", "is", "null", "true", "false", "like", "between", "exists"}
|
||||||
|
for _, kw := range keywords {
|
||||||
|
if word == kw {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -103,3 +103,18 @@ func CatchPanicCallback(location string, cb func(err any)) {
|
|||||||
func CatchPanic(location string) {
|
func CatchPanic(location string) {
|
||||||
CatchPanicCallback(location, nil)
|
CatchPanicCallback(location, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HandlePanic logs a panic and returns it as an error
|
||||||
|
// This should be called with the result of recover() from a deferred function
|
||||||
|
// Example usage:
|
||||||
|
//
|
||||||
|
// defer func() {
|
||||||
|
// if r := recover(); r != nil {
|
||||||
|
// err = logger.HandlePanic("MethodName", r)
|
||||||
|
// }
|
||||||
|
// }()
|
||||||
|
func HandlePanic(methodName string, r any) error {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -323,6 +323,127 @@ func ExtractColumnFromBunTag(tag string) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetSQLModelColumns extracts column names that have valid SQL field mappings
|
||||||
|
// This function only returns columns that:
|
||||||
|
// 1. Have bun or gorm tags (not just json tags)
|
||||||
|
// 2. Are not relations (no rel:, join:, foreignKey, references, many2many tags)
|
||||||
|
// 3. Are not scan-only embedded fields
|
||||||
|
func GetSQLModelColumns(model any) []string {
|
||||||
|
var columns []string
|
||||||
|
|
||||||
|
modelType := reflect.TypeOf(model)
|
||||||
|
|
||||||
|
// Unwrap pointers, slices, and arrays to get to the base struct type
|
||||||
|
for modelType != nil && (modelType.Kind() == reflect.Pointer || modelType.Kind() == reflect.Slice || modelType.Kind() == reflect.Array) {
|
||||||
|
modelType = modelType.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate that we have a struct type
|
||||||
|
if modelType == nil || modelType.Kind() != reflect.Struct {
|
||||||
|
return columns
|
||||||
|
}
|
||||||
|
|
||||||
|
collectSQLColumnsFromType(modelType, &columns, false)
|
||||||
|
|
||||||
|
return columns
|
||||||
|
}
|
||||||
|
|
||||||
|
// collectSQLColumnsFromType recursively collects SQL column names from a struct type
|
||||||
|
// scanOnlyEmbedded indicates if we're inside a scan-only embedded struct
|
||||||
|
func collectSQLColumnsFromType(typ reflect.Type, columns *[]string, scanOnlyEmbedded bool) {
|
||||||
|
for i := 0; i < typ.NumField(); i++ {
|
||||||
|
field := typ.Field(i)
|
||||||
|
|
||||||
|
// Check if this is an embedded struct
|
||||||
|
if field.Anonymous {
|
||||||
|
// Unwrap pointer type if necessary
|
||||||
|
fieldType := field.Type
|
||||||
|
if fieldType.Kind() == reflect.Pointer {
|
||||||
|
fieldType = fieldType.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the embedded struct itself is scan-only
|
||||||
|
isScanOnly := scanOnlyEmbedded
|
||||||
|
bunTag := field.Tag.Get("bun")
|
||||||
|
if bunTag != "" && isBunFieldScanOnly(bunTag) {
|
||||||
|
isScanOnly = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursively process embedded struct
|
||||||
|
if fieldType.Kind() == reflect.Struct {
|
||||||
|
collectSQLColumnsFromType(fieldType, columns, isScanOnly)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip fields in scan-only embedded structs
|
||||||
|
if scanOnlyEmbedded {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get bun and gorm tags
|
||||||
|
bunTag := field.Tag.Get("bun")
|
||||||
|
gormTag := field.Tag.Get("gorm")
|
||||||
|
|
||||||
|
// Skip if neither bun nor gorm tag exists
|
||||||
|
if bunTag == "" && gormTag == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip if explicitly marked with "-"
|
||||||
|
if bunTag == "-" || gormTag == "-" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip if field itself is scan-only (bun)
|
||||||
|
if bunTag != "" && isBunFieldScanOnly(bunTag) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip if field itself is read-only (gorm)
|
||||||
|
if gormTag != "" && isGormFieldReadOnly(gormTag) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip relation fields (bun)
|
||||||
|
if bunTag != "" {
|
||||||
|
// Skip if it's a bun relation (rel:, join:, or m2m:)
|
||||||
|
if strings.Contains(bunTag, "rel:") ||
|
||||||
|
strings.Contains(bunTag, "join:") ||
|
||||||
|
strings.Contains(bunTag, "m2m:") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip relation fields (gorm)
|
||||||
|
if gormTag != "" {
|
||||||
|
// Skip if it has gorm relationship tags
|
||||||
|
if strings.Contains(gormTag, "foreignKey:") ||
|
||||||
|
strings.Contains(gormTag, "references:") ||
|
||||||
|
strings.Contains(gormTag, "many2many:") ||
|
||||||
|
strings.Contains(gormTag, "constraint:") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get column name
|
||||||
|
columnName := ""
|
||||||
|
if bunTag != "" {
|
||||||
|
columnName = ExtractColumnFromBunTag(bunTag)
|
||||||
|
}
|
||||||
|
if columnName == "" && gormTag != "" {
|
||||||
|
columnName = ExtractColumnFromGormTag(gormTag)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip if we couldn't extract a column name
|
||||||
|
if columnName == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
*columns = append(*columns, columnName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// IsColumnWritable checks if a column can be written to in the database
|
// IsColumnWritable checks if a column can be written to in the database
|
||||||
// For bun: returns false if the field has "scanonly" tag
|
// For bun: returns false if the field has "scanonly" tag
|
||||||
// For gorm: returns false if the field has "<-:false" or "->" (read-only) tag
|
// For gorm: returns false if the field has "<-:false" or "->" (read-only) tag
|
||||||
|
|||||||
@@ -474,3 +474,143 @@ func TestIsColumnWritableWithEmbedded(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test models with relations for GetSQLModelColumns
|
||||||
|
type User struct {
|
||||||
|
ID int `bun:"id,pk" json:"id"`
|
||||||
|
Name string `bun:"name" json:"name"`
|
||||||
|
Email string `bun:"email" json:"email"`
|
||||||
|
ProfileData string `json:"profile_data"` // No bun/gorm tag
|
||||||
|
Posts []Post `bun:"rel:has-many,join:id=user_id" json:"posts"`
|
||||||
|
Profile *Profile `bun:"rel:has-one,join:id=user_id" json:"profile"`
|
||||||
|
RowNumber int64 `bun:",scanonly" json:"_rownumber"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Post struct {
|
||||||
|
ID int `gorm:"column:id;primaryKey" json:"id"`
|
||||||
|
Title string `gorm:"column:title" json:"title"`
|
||||||
|
UserID int `gorm:"column:user_id;foreignKey" json:"user_id"`
|
||||||
|
User *User `gorm:"foreignKey:UserID;references:ID" json:"user"`
|
||||||
|
Tags []Tag `gorm:"many2many:post_tags" json:"tags"`
|
||||||
|
Content string `json:"content"` // No bun/gorm tag
|
||||||
|
}
|
||||||
|
|
||||||
|
type Profile struct {
|
||||||
|
ID int `bun:"id,pk" json:"id"`
|
||||||
|
Bio string `bun:"bio" json:"bio"`
|
||||||
|
UserID int `bun:"user_id" json:"user_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Tag struct {
|
||||||
|
ID int `gorm:"column:id;primaryKey" json:"id"`
|
||||||
|
Name string `gorm:"column:name" json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Model with scan-only embedded struct
|
||||||
|
type EntityWithScanOnlyEmbedded struct {
|
||||||
|
ID int `bun:"id,pk" json:"id"`
|
||||||
|
Name string `bun:"name" json:"name"`
|
||||||
|
AdhocBuffer `bun:",scanonly"` // Entire embedded struct is scan-only
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetSQLModelColumns(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
model any
|
||||||
|
expected []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Bun model with relations - excludes relations and non-SQL fields",
|
||||||
|
model: User{},
|
||||||
|
// Should include: id, name, email (has bun tags)
|
||||||
|
// Should exclude: profile_data (no bun tag), Posts/Profile (relations), RowNumber (scan-only in embedded would be excluded)
|
||||||
|
expected: []string{"id", "name", "email"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "GORM model with relations - excludes relations and non-SQL fields",
|
||||||
|
model: Post{},
|
||||||
|
// Should include: id, title, user_id (has gorm tags)
|
||||||
|
// Should exclude: content (no gorm tag), User/Tags (relations)
|
||||||
|
expected: []string{"id", "title", "user_id"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Model with embedded base and scan-only embedded",
|
||||||
|
model: EntityWithScanOnlyEmbedded{},
|
||||||
|
// Should include: id, name from main struct
|
||||||
|
// Should exclude: all fields from AdhocBuffer (scan-only embedded struct)
|
||||||
|
expected: []string{"id", "name"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Model with embedded - includes SQL fields, excludes scan-only",
|
||||||
|
model: ModelWithEmbedded{},
|
||||||
|
// Should include: rid_base, created_at (from BaseModel), name, description (from main)
|
||||||
|
// Should exclude: cql1, cql2, _rownumber (from AdhocBuffer - scan-only fields)
|
||||||
|
expected: []string{"rid_base", "created_at", "name", "description"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "GORM model with embedded - includes SQL fields, excludes scan-only",
|
||||||
|
model: GormModelWithEmbedded{},
|
||||||
|
// Should include: rid_base, created_at (from GormBaseModel), name, description (from main)
|
||||||
|
// Should exclude: cql1, cql2 (scan-only), _rownumber (no gorm column tag, marked as -)
|
||||||
|
expected: []string{"rid_base", "created_at", "name", "description"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Simple Profile model",
|
||||||
|
model: Profile{},
|
||||||
|
// Should include all fields with bun tags
|
||||||
|
expected: []string{"id", "bio", "user_id"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := GetSQLModelColumns(tt.model)
|
||||||
|
if len(result) != len(tt.expected) {
|
||||||
|
t.Errorf("GetSQLModelColumns() returned %d columns, want %d.\nGot: %v\nWant: %v",
|
||||||
|
len(result), len(tt.expected), result, tt.expected)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i, col := range result {
|
||||||
|
if col != tt.expected[i] {
|
||||||
|
t.Errorf("GetSQLModelColumns()[%d] = %v, want %v.\nFull result: %v",
|
||||||
|
i, col, tt.expected[i], result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetSQLModelColumnsVsGetModelColumns(t *testing.T) {
|
||||||
|
// Demonstrate the difference between GetModelColumns and GetSQLModelColumns
|
||||||
|
user := User{}
|
||||||
|
|
||||||
|
allColumns := GetModelColumns(user)
|
||||||
|
sqlColumns := GetSQLModelColumns(user)
|
||||||
|
|
||||||
|
t.Logf("GetModelColumns(User): %v", allColumns)
|
||||||
|
t.Logf("GetSQLModelColumns(User): %v", sqlColumns)
|
||||||
|
|
||||||
|
// GetModelColumns should return more columns (includes fields with only json tags)
|
||||||
|
if len(allColumns) <= len(sqlColumns) {
|
||||||
|
t.Errorf("Expected GetModelColumns to return more columns than GetSQLModelColumns")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSQLModelColumns should not include 'profile_data' (no bun tag)
|
||||||
|
for _, col := range sqlColumns {
|
||||||
|
if col == "profile_data" {
|
||||||
|
t.Errorf("GetSQLModelColumns should not include 'profile_data' (no bun/gorm tag)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetModelColumns should include 'profile_data' (has json tag)
|
||||||
|
hasProfileData := false
|
||||||
|
for _, col := range allColumns {
|
||||||
|
if col == "profile_data" {
|
||||||
|
hasProfileData = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hasProfileData {
|
||||||
|
t.Errorf("GetModelColumns should include 'profile_data' (has json tag)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -191,6 +191,11 @@ func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, id st
|
|||||||
query = query.Table(tableName)
|
query = query.Table(tableName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(options.Columns) == 0 && (len(options.ComputedColumns) > 0) {
|
||||||
|
logger.Debug("Populating options.Columns with all model columns since computed columns are additions")
|
||||||
|
options.Columns = reflection.GetSQLModelColumns(model)
|
||||||
|
}
|
||||||
|
|
||||||
// Apply column selection
|
// Apply column selection
|
||||||
if len(options.Columns) > 0 {
|
if len(options.Columns) > 0 {
|
||||||
logger.Debug("Selecting columns: %v", options.Columns)
|
logger.Debug("Selecting columns: %v", options.Columns)
|
||||||
@@ -1132,15 +1137,20 @@ func (h *Handler) applyPreloads(model interface{}, query common.SelectQuery, pre
|
|||||||
// ORMs like GORM and Bun expect the struct field name, not the JSON name
|
// ORMs like GORM and Bun expect the struct field name, not the JSON name
|
||||||
relationFieldName := relInfo.fieldName
|
relationFieldName := relInfo.fieldName
|
||||||
|
|
||||||
// For now, we'll preload without conditions
|
// Validate and fix WHERE clause to ensure it contains the relation prefix
|
||||||
// TODO: Implement column selection and filtering for preloads
|
if len(preload.Where) > 0 {
|
||||||
// This requires a more sophisticated approach with callbacks or query builders
|
fixedWhere, err := common.ValidateAndFixPreloadWhere(preload.Where, relationFieldName)
|
||||||
// Apply preloading
|
if err != nil {
|
||||||
|
logger.Error("Invalid preload WHERE clause for relation '%s': %v", relationFieldName, err)
|
||||||
|
panic(fmt.Errorf("invalid preload WHERE clause for relation '%s': %w", relationFieldName, err))
|
||||||
|
}
|
||||||
|
preload.Where = fixedWhere
|
||||||
|
}
|
||||||
|
|
||||||
logger.Debug("Applying preload: %s", relationFieldName)
|
logger.Debug("Applying preload: %s", relationFieldName)
|
||||||
query = query.PreloadRelation(relationFieldName, func(sq common.SelectQuery) common.SelectQuery {
|
query = query.PreloadRelation(relationFieldName, func(sq common.SelectQuery) common.SelectQuery {
|
||||||
if len(preload.OmitColumns) > 0 {
|
if len(preload.OmitColumns) > 0 {
|
||||||
allCols := reflection.GetModelColumns(model)
|
allCols := reflection.GetSQLModelColumns(model)
|
||||||
// Remove omitted columns
|
// Remove omitted columns
|
||||||
preload.Columns = []string{}
|
preload.Columns = []string{}
|
||||||
for _, col := range allCols {
|
for _, col := range allCols {
|
||||||
|
|||||||
@@ -260,9 +260,12 @@ func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, id st
|
|||||||
query = query.Table(tableName)
|
query = query.Table(tableName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: X-Files configuration is now applied via parseXFiles which populates
|
// If we have computed columns/expressions but options.Columns is empty,
|
||||||
// ExtendedRequestOptions fields (columns, filters, sort, preload, etc.)
|
// populate it with all model columns first since computed columns are additions
|
||||||
// These are applied below in the normal query building process
|
if len(options.Columns) == 0 && (len(options.ComputedQL) > 0 || len(options.ComputedColumns) > 0) {
|
||||||
|
logger.Debug("Populating options.Columns with all model columns since computed columns are additions")
|
||||||
|
options.Columns = reflection.GetSQLModelColumns(model)
|
||||||
|
}
|
||||||
|
|
||||||
// Apply ComputedQL fields if any
|
// Apply ComputedQL fields if any
|
||||||
if len(options.ComputedQL) > 0 {
|
if len(options.ComputedQL) > 0 {
|
||||||
@@ -344,6 +347,19 @@ func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, id st
|
|||||||
for idx := range options.Preload {
|
for idx := range options.Preload {
|
||||||
preload := options.Preload[idx]
|
preload := options.Preload[idx]
|
||||||
logger.Debug("Applying preload: %s", preload.Relation)
|
logger.Debug("Applying preload: %s", preload.Relation)
|
||||||
|
|
||||||
|
// Validate and fix WHERE clause to ensure it contains the relation prefix
|
||||||
|
if len(preload.Where) > 0 {
|
||||||
|
fixedWhere, err := common.ValidateAndFixPreloadWhere(preload.Where, preload.Relation)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Invalid preload WHERE clause for relation '%s': %v", preload.Relation, err)
|
||||||
|
h.sendError(w, http.StatusBadRequest, "invalid_preload_where",
|
||||||
|
fmt.Sprintf("Invalid preload WHERE clause for relation '%s'", preload.Relation), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
preload.Where = fixedWhere
|
||||||
|
}
|
||||||
|
|
||||||
query = query.PreloadRelation(preload.Relation, func(sq common.SelectQuery) common.SelectQuery {
|
query = query.PreloadRelation(preload.Relation, func(sq common.SelectQuery) common.SelectQuery {
|
||||||
if len(preload.OmitColumns) > 0 {
|
if len(preload.OmitColumns) > 0 {
|
||||||
allCols := reflection.GetModelColumns(model)
|
allCols := reflection.GetModelColumns(model)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user