mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2025-12-29 15:54:26 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
490ae37c6d | ||
|
|
99307e31e6 | ||
|
|
e3f7869c6d |
@@ -34,6 +34,63 @@ func (h *QueryDebugHook) AfterQuery(ctx context.Context, event *bun.QueryEvent)
|
||||
}
|
||||
}
|
||||
|
||||
// debugScanIntoStruct attempts to scan rows into a struct with detailed field-level logging
|
||||
// This helps identify which specific field is causing scanning issues
|
||||
func debugScanIntoStruct(rows interface{}, dest interface{}) error {
|
||||
v := reflect.ValueOf(dest)
|
||||
if v.Kind() != reflect.Ptr {
|
||||
return fmt.Errorf("dest must be a pointer")
|
||||
}
|
||||
|
||||
v = v.Elem()
|
||||
if v.Kind() != reflect.Struct && v.Kind() != reflect.Slice {
|
||||
return fmt.Errorf("dest must be pointer to struct or slice")
|
||||
}
|
||||
|
||||
// Log the type being scanned into
|
||||
typeName := v.Type().String()
|
||||
logger.Debug("Debug scan into type: %s (kind: %s)", typeName, v.Kind())
|
||||
|
||||
// Handle slice types - inspect the element type
|
||||
var structType reflect.Type
|
||||
if v.Kind() == reflect.Slice {
|
||||
elemType := v.Type().Elem()
|
||||
logger.Debug(" Slice element type: %s", elemType)
|
||||
|
||||
// If slice of pointers, get the underlying type
|
||||
if elemType.Kind() == reflect.Ptr {
|
||||
structType = elemType.Elem()
|
||||
} else {
|
||||
structType = elemType
|
||||
}
|
||||
} else if v.Kind() == reflect.Struct {
|
||||
structType = v.Type()
|
||||
}
|
||||
|
||||
// If we have a struct type, log all its fields
|
||||
if structType != nil && structType.Kind() == reflect.Struct {
|
||||
logger.Debug(" Struct %s has %d fields:", structType.Name(), structType.NumField())
|
||||
for i := 0; i < structType.NumField(); i++ {
|
||||
field := structType.Field(i)
|
||||
|
||||
// Log embedded fields specially
|
||||
if field.Anonymous {
|
||||
logger.Debug(" [%d] EMBEDDED: %s (type: %s, kind: %s, bun:%q)",
|
||||
i, field.Name, field.Type, field.Type.Kind(), field.Tag.Get("bun"))
|
||||
} else {
|
||||
bunTag := field.Tag.Get("bun")
|
||||
if bunTag == "" {
|
||||
bunTag = "(no tag)"
|
||||
}
|
||||
logger.Debug(" [%d] %s (type: %s, kind: %s, bun:%q)",
|
||||
i, field.Name, field.Type, field.Type.Kind(), bunTag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// BunAdapter adapts Bun to work with our Database interface
|
||||
// This demonstrates how the abstraction works with different ORMs
|
||||
type BunAdapter struct {
|
||||
@@ -52,6 +109,14 @@ func (b *BunAdapter) EnableQueryDebug() {
|
||||
logger.Info("Bun query debug mode enabled - all SQL queries will be logged")
|
||||
}
|
||||
|
||||
// EnableDetailedScanDebug enables verbose logging of scan operations
|
||||
// WARNING: This generates a LOT of log output. Use only for debugging specific issues.
|
||||
func (b *BunAdapter) EnableDetailedScanDebug() {
|
||||
logger.Info("Detailed scan debugging enabled - will log all field scanning operations")
|
||||
// This is a flag that can be checked in scan operations
|
||||
// Implementation would require modifying the scan logic
|
||||
}
|
||||
|
||||
// DisableQueryDebug removes all query hooks
|
||||
func (b *BunAdapter) DisableQueryDebug() {
|
||||
// Create a new DB without hooks
|
||||
@@ -676,6 +741,31 @@ func (b *BunSelectQuery) Scan(ctx context.Context, dest interface{}) (err error)
|
||||
func (b *BunSelectQuery) ScanModel(ctx context.Context) (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
// Enhanced panic recovery with model information
|
||||
model := b.query.GetModel()
|
||||
var modelInfo string
|
||||
if model != nil && model.Value() != nil {
|
||||
modelValue := model.Value()
|
||||
modelInfo = fmt.Sprintf("Model type: %T", modelValue)
|
||||
|
||||
// Try to get the model's underlying struct type
|
||||
v := reflect.ValueOf(modelValue)
|
||||
if v.Kind() == reflect.Ptr {
|
||||
v = v.Elem()
|
||||
}
|
||||
if v.Kind() == reflect.Slice {
|
||||
if v.Type().Elem().Kind() == reflect.Ptr {
|
||||
modelInfo += fmt.Sprintf(", Slice of: %s", v.Type().Elem().Elem().Name())
|
||||
} else {
|
||||
modelInfo += fmt.Sprintf(", Slice of: %s", v.Type().Elem().Name())
|
||||
}
|
||||
} else if v.Kind() == reflect.Struct {
|
||||
modelInfo += fmt.Sprintf(", Struct: %s", v.Type().Name())
|
||||
}
|
||||
}
|
||||
|
||||
sqlStr := b.query.String()
|
||||
logger.Error("Panic in BunSelectQuery.ScanModel: %v. %s. SQL: %s", r, modelInfo, sqlStr)
|
||||
err = logger.HandlePanic("BunSelectQuery.ScanModel", r)
|
||||
}
|
||||
}()
|
||||
@@ -683,6 +773,17 @@ func (b *BunSelectQuery) ScanModel(ctx context.Context) (err error) {
|
||||
return fmt.Errorf("model is nil")
|
||||
}
|
||||
|
||||
// Optional: Enable detailed field-level debugging (set to true to debug)
|
||||
const enableDetailedDebug = true
|
||||
if enableDetailedDebug {
|
||||
model := b.query.GetModel()
|
||||
if model != nil && model.Value() != nil {
|
||||
if err := debugScanIntoStruct(nil, model.Value()); err != nil {
|
||||
logger.Warn("Debug scan inspection failed: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Execute the main query first
|
||||
err = b.query.Scan(ctx)
|
||||
if err != nil {
|
||||
|
||||
@@ -430,7 +430,45 @@ func extractTableAndColumn(cond string) (table string, column string) {
|
||||
// Remove any quotes
|
||||
columnRef = strings.Trim(columnRef, "`\"'")
|
||||
|
||||
// Check if it contains a dot (qualified reference)
|
||||
// Check if there's a function call (contains opening parenthesis)
|
||||
openParenIdx := strings.Index(columnRef, "(")
|
||||
|
||||
if openParenIdx >= 0 {
|
||||
// There's a function call - find the FIRST dot after the opening paren
|
||||
// This handles cases like: ifblnk(users.status, orders.status) - extracts users.status
|
||||
dotIdx := strings.Index(columnRef[openParenIdx:], ".")
|
||||
if dotIdx > 0 {
|
||||
dotIdx += openParenIdx // Adjust to absolute position
|
||||
|
||||
// Extract table name (between paren and dot)
|
||||
// Find the last opening paren before this dot
|
||||
lastOpenParen := strings.LastIndex(columnRef[:dotIdx], "(")
|
||||
table = columnRef[lastOpenParen+1 : dotIdx]
|
||||
|
||||
// Find the column name - it ends at comma, closing paren, whitespace, or end of string
|
||||
columnStart := dotIdx + 1
|
||||
columnEnd := len(columnRef)
|
||||
|
||||
for i := columnStart; i < len(columnRef); i++ {
|
||||
ch := columnRef[i]
|
||||
if ch == ',' || ch == ')' || ch == ' ' || ch == '\t' {
|
||||
columnEnd = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
column = columnRef[columnStart:columnEnd]
|
||||
|
||||
// Remove quotes from table and column if present
|
||||
table = strings.Trim(table, "`\"'")
|
||||
column = strings.Trim(column, "`\"'")
|
||||
|
||||
return table, column
|
||||
}
|
||||
}
|
||||
|
||||
// No function call - check if it contains a dot (qualified reference)
|
||||
// Use LastIndex to handle schema.table.column properly
|
||||
if dotIdx := strings.LastIndex(columnRef, "."); dotIdx > 0 {
|
||||
table = columnRef[:dotIdx]
|
||||
column = columnRef[dotIdx+1:]
|
||||
|
||||
@@ -286,6 +286,48 @@ func TestExtractTableAndColumn(t *testing.T) {
|
||||
expectedTable: "",
|
||||
expectedCol: "",
|
||||
},
|
||||
{
|
||||
name: "function call with table.column - ifblnk",
|
||||
input: "ifblnk(users.status,0) in (1,2,3,4)",
|
||||
expectedTable: "users",
|
||||
expectedCol: "status",
|
||||
},
|
||||
{
|
||||
name: "function call with table.column - coalesce",
|
||||
input: "coalesce(users.age, 0) = 25",
|
||||
expectedTable: "users",
|
||||
expectedCol: "age",
|
||||
},
|
||||
{
|
||||
name: "nested function calls",
|
||||
input: "upper(trim(users.name)) = 'JOHN'",
|
||||
expectedTable: "users",
|
||||
expectedCol: "name",
|
||||
},
|
||||
{
|
||||
name: "function with multiple args and table.column",
|
||||
input: "substring(users.email, 1, 5) = 'admin'",
|
||||
expectedTable: "users",
|
||||
expectedCol: "email",
|
||||
},
|
||||
{
|
||||
name: "cast function with table.column",
|
||||
input: "cast(orders.total as decimal) > 100",
|
||||
expectedTable: "orders",
|
||||
expectedCol: "total",
|
||||
},
|
||||
{
|
||||
name: "complex nested functions",
|
||||
input: "coalesce(nullif(users.status, ''), 'default') = 'active'",
|
||||
expectedTable: "users",
|
||||
expectedCol: "status",
|
||||
},
|
||||
{
|
||||
name: "function with multiple table.column refs (extracts first)",
|
||||
input: "greatest(users.created_at, users.updated_at) > '2024-01-01'",
|
||||
expectedTable: "users",
|
||||
expectedCol: "created_at",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -352,6 +394,14 @@ func TestSanitizeWhereClauseWithPreloads(t *testing.T) {
|
||||
},
|
||||
expected: "users.status = 'active' AND Department.name = 'Engineering'",
|
||||
},
|
||||
|
||||
{
|
||||
name: "Function Call with correct table prefix - unchanged",
|
||||
where: "ifblnk(users.status,0) in (1,2,3,4)",
|
||||
tableName: "users",
|
||||
options: nil,
|
||||
expected: "ifblnk(users.status,0) in (1,2,3,4)",
|
||||
},
|
||||
{
|
||||
name: "no options provided - works as before",
|
||||
where: "wrong_table.status = 'active'",
|
||||
|
||||
Reference in New Issue
Block a user