mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-06-09 23:33:45 +00:00
fix(database): add Scan method to insert query interfaces
* Implement Scan method for BunInsertQuery, GormInsertQuery, and PgSQLInsertQuery * Update mock implementations to support Scan method * Introduce GetForeignKeyColumn utility for foreign key resolution * Add tests for GetForeignKeyColumn functionality
This commit is contained in:
@@ -973,6 +973,62 @@ func GetRelationType(model interface{}, fieldName string) RelationType {
|
||||
return RelationUnknown
|
||||
}
|
||||
|
||||
// GetForeignKeyColumn returns the DB column name of the foreign key that the
|
||||
// relation field identified by parentKey owns on modelType.
|
||||
//
|
||||
// It checks tags in priority order:
|
||||
// 1. Bun join: tag — e.g. `bun:"rel:belongs-to,join:department_id=id"` → "department_id"
|
||||
// 2. GORM foreignKey: tag — e.g. `gorm:"foreignKey:DepartmentID"` → column of DepartmentID field
|
||||
// 3. Returns "" when no tag is found (caller should fall back to convention)
|
||||
//
|
||||
// parentKey is matched case-insensitively against the field name and JSON tag.
|
||||
func GetForeignKeyColumn(modelType reflect.Type, parentKey string) string {
|
||||
for modelType.Kind() == reflect.Ptr || modelType.Kind() == reflect.Slice {
|
||||
modelType = modelType.Elem()
|
||||
}
|
||||
if modelType.Kind() != reflect.Struct {
|
||||
return ""
|
||||
}
|
||||
|
||||
for i := 0; i < modelType.NumField(); i++ {
|
||||
field := modelType.Field(i)
|
||||
|
||||
name := field.Name
|
||||
jsonName := strings.Split(field.Tag.Get("json"), ",")[0]
|
||||
if !strings.EqualFold(name, parentKey) && !strings.EqualFold(jsonName, parentKey) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Bun: join:local_col=foreign_col
|
||||
for _, part := range strings.Split(field.Tag.Get("bun"), ",") {
|
||||
part = strings.TrimSpace(part)
|
||||
if strings.HasPrefix(part, "join:") {
|
||||
// join: may contain multiple pairs separated by spaces: "join:a=b join:c=d"
|
||||
// but typically it's a single pair; take the first local column
|
||||
pair := strings.TrimPrefix(part, "join:")
|
||||
if idx := strings.Index(pair, "="); idx > 0 {
|
||||
return pair[:idx]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GORM: foreignKey:FieldName
|
||||
for _, part := range strings.Split(field.Tag.Get("gorm"), ";") {
|
||||
part = strings.TrimSpace(part)
|
||||
if strings.HasPrefix(part, "foreignKey:") {
|
||||
fkFieldName := strings.TrimPrefix(part, "foreignKey:")
|
||||
if fkField, ok := modelType.FieldByName(fkFieldName); ok {
|
||||
return getColumnNameFromField(fkField)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetRelationModel gets the model type for a relation field
|
||||
// It searches for the field by name in the following order (case-insensitive):
|
||||
// 1. Actual field name
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
package reflection
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// --- local test models ---
|
||||
|
||||
type fkDept struct{}
|
||||
|
||||
// bunEmployee uses bun join: tag to declare the FK column explicitly.
|
||||
type bunEmployee struct {
|
||||
DeptID string `bun:"dept_id" json:"dept_id"`
|
||||
Department *fkDept `bun:"rel:belongs-to,join:dept_id=id" json:"department"`
|
||||
}
|
||||
|
||||
// gormEmployee uses gorm foreignKey: tag (mirrors testmodels.Employee).
|
||||
type gormEmployee struct {
|
||||
DepartmentID string `json:"department_id"`
|
||||
ManagerID string `json:"manager_id"`
|
||||
Department *fkDept `gorm:"foreignKey:DepartmentID;references:ID" json:"department"`
|
||||
Manager *fkDept `gorm:"foreignKey:ManagerID;references:ID" json:"manager"`
|
||||
}
|
||||
|
||||
// conventionEmployee has no explicit FK tag — relies on naming convention.
|
||||
type conventionEmployee struct {
|
||||
DepartmentID string `json:"department_id"`
|
||||
Department *fkDept `json:"department"`
|
||||
}
|
||||
|
||||
// noTagEmployee has a relation field with no FK tag and no convention match.
|
||||
type noTagEmployee struct {
|
||||
Unrelated *fkDept `json:"unrelated"`
|
||||
}
|
||||
|
||||
func TestGetForeignKeyColumn(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
modelType reflect.Type
|
||||
parentKey string
|
||||
want string
|
||||
}{
|
||||
// Bun join: tag
|
||||
{
|
||||
name: "bun join tag returns local column",
|
||||
modelType: reflect.TypeOf(bunEmployee{}),
|
||||
parentKey: "department",
|
||||
want: "dept_id",
|
||||
},
|
||||
{
|
||||
name: "bun join tag matched via json tag (case-insensitive)",
|
||||
modelType: reflect.TypeOf(bunEmployee{}),
|
||||
parentKey: "Department",
|
||||
want: "dept_id",
|
||||
},
|
||||
|
||||
// GORM foreignKey: tag
|
||||
{
|
||||
name: "gorm foreignKey resolves to column name",
|
||||
modelType: reflect.TypeOf(gormEmployee{}),
|
||||
parentKey: "department",
|
||||
want: "department_id",
|
||||
},
|
||||
{
|
||||
name: "gorm foreignKey resolves second relation",
|
||||
modelType: reflect.TypeOf(gormEmployee{}),
|
||||
parentKey: "manager",
|
||||
want: "manager_id",
|
||||
},
|
||||
{
|
||||
name: "gorm foreignKey matched case-insensitively",
|
||||
modelType: reflect.TypeOf(gormEmployee{}),
|
||||
parentKey: "Department",
|
||||
want: "department_id",
|
||||
},
|
||||
|
||||
// Pointer and slice unwrapping
|
||||
{
|
||||
name: "pointer to struct is unwrapped",
|
||||
modelType: reflect.TypeOf(&gormEmployee{}),
|
||||
parentKey: "department",
|
||||
want: "department_id",
|
||||
},
|
||||
{
|
||||
name: "slice of struct is unwrapped",
|
||||
modelType: reflect.TypeOf([]gormEmployee{}),
|
||||
parentKey: "department",
|
||||
want: "department_id",
|
||||
},
|
||||
|
||||
// No tag — returns "" so caller can fall back to convention
|
||||
{
|
||||
name: "relation with no FK tag returns empty string",
|
||||
modelType: reflect.TypeOf(conventionEmployee{}),
|
||||
parentKey: "department",
|
||||
want: "",
|
||||
},
|
||||
|
||||
// Unknown parent key
|
||||
{
|
||||
name: "unknown parent key returns empty string",
|
||||
modelType: reflect.TypeOf(gormEmployee{}),
|
||||
parentKey: "nonexistent",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "non-struct type returns empty string",
|
||||
modelType: reflect.TypeOf(""),
|
||||
parentKey: "department",
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := GetForeignKeyColumn(tt.modelType, tt.parentKey)
|
||||
if got != tt.want {
|
||||
t.Errorf("GetForeignKeyColumn(%v, %q) = %q, want %q", tt.modelType, tt.parentKey, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user