feat(qrm): support qrm:"optional" in strict field mapping
This commit is contained in:
parent
f33c2ee357
commit
80b4ddd383
3 changed files with 134 additions and 8 deletions
|
|
@ -22,7 +22,12 @@ type Config struct {
|
||||||
// StrictFieldMapping, when true, causes the scanning function to panic if it encounters any
|
// StrictFieldMapping, when true, causes the scanning function to panic if it encounters any
|
||||||
// destination struct fields that do not have matching columns in the SQL query result.
|
// destination struct fields that do not have matching columns in the SQL query result.
|
||||||
// This check applies only to fields that are mapped from a single column (simple/scanner/json_column).
|
// This check applies only to fields that are mapped from a single column (simple/scanner/json_column).
|
||||||
// Complex fields (struct/slice) are excluded because they are populated recursively and can be optional.
|
// Complex fields (struct/slice) are excluded because they are not mapped from a single column.
|
||||||
|
//
|
||||||
|
// Optional fields:
|
||||||
|
// If a destination field (including struct/slice fields) is not always selected by a query,
|
||||||
|
// it can be marked as optional using `qrm:"optional"`. When StrictFieldMapping is enabled,
|
||||||
|
// unmapped fields under an optional field will not trigger a panic.
|
||||||
// Does not apply to statements build with SELECT_JSON_OBJ or SELECT_JSON_ARR
|
// Does not apply to statements build with SELECT_JSON_OBJ or SELECT_JSON_ARR
|
||||||
StrictFieldMapping bool
|
StrictFieldMapping bool
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -83,19 +83,19 @@ func (s *ScanContext) EnsureEveryColumnRead() {
|
||||||
|
|
||||||
func (s *ScanContext) recordUnmappedField(structType reflect.Type, parentField *reflect.StructField, field reflect.StructField) {
|
func (s *ScanContext) recordUnmappedField(structType reflect.Type, parentField *reflect.StructField, field reflect.StructField) {
|
||||||
// skip private/unsettable fields (those are ignored by mapRowToStruct anyway)
|
// skip private/unsettable fields (those are ignored by mapRowToStruct anyway)
|
||||||
if field.PkgPath != "" {
|
if !field.IsExported() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: For unnamed/anonymous structs, Name() is empty, so String() is used for readability/uniqueness.
|
// NOTE: For unnamed/anonymous structs, Name() is empty, so String() is used for readability/uniqueness.
|
||||||
typeName := structType.String()
|
typeName := structType.Name()
|
||||||
if structType.Name() != "" {
|
if typeName == "" {
|
||||||
typeName = structType.Name()
|
typeName = structType.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
fieldIdent := fmt.Sprintf("%s.%s", typeName, field.Name)
|
fieldIdent := fmt.Sprintf("%s.%s", typeName, field.Name)
|
||||||
if parentField != nil {
|
if parentField != nil {
|
||||||
fieldIdent = fmt.Sprintf("%s.%s.%s", typeName, parentField.Name, field.Name)
|
fieldIdent = fmt.Sprintf("%s %s.%s", parentField.Name, typeName, field.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.unmappedFields = append(s.unmappedFields, fmt.Sprintf("'%s'", fieldIdent))
|
s.unmappedFields = append(s.unmappedFields, fmt.Sprintf("'%s'", fieldIdent))
|
||||||
|
|
@ -108,6 +108,38 @@ func (s *ScanContext) EnsureEveryFieldMapped() {
|
||||||
panic("jet: fields never mapped: " + strings.Join(s.unmappedFields, ", "))
|
panic("jet: fields never mapped: " + strings.Join(s.unmappedFields, ", "))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isOptionalQrmField(field *reflect.StructField) bool {
|
||||||
|
if field == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
tag := field.Tag.Get("qrm")
|
||||||
|
if tag == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, part := range strings.Split(tag, ",") {
|
||||||
|
if strings.TrimSpace(part) == "optional" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldRecordUnmappedField(parentField *reflect.StructField, field reflect.StructField, fieldMap fieldMapping) bool {
|
||||||
|
if !GlobalConfig.StrictFieldMapping {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if fieldMap.Type == complexType {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if fieldMap.rowIndex != -1 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if isOptionalQrmField(parentField) || isOptionalQrmField(&field) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func createScanSlice(columnCount int) []interface{} {
|
func createScanSlice(columnCount int) []interface{} {
|
||||||
scanPtrSlice := make([]interface{}, columnCount)
|
scanPtrSlice := make([]interface{}, columnCount)
|
||||||
|
|
||||||
|
|
@ -173,7 +205,7 @@ func (s *ScanContext) getTypeInfo(structType reflect.Type, parentField *reflect.
|
||||||
fieldMap.Type = simpleType
|
fieldMap.Type = simpleType
|
||||||
}
|
}
|
||||||
|
|
||||||
if GlobalConfig.StrictFieldMapping && fieldMap.rowIndex == -1 && fieldMap.Type != complexType {
|
if shouldRecordUnmappedField(parentField, field, fieldMap) {
|
||||||
s.recordUnmappedField(structType, parentField, field)
|
s.recordUnmappedField(structType, parentField, field)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -160,7 +160,7 @@ LIMIT ?;
|
||||||
}
|
}
|
||||||
|
|
||||||
requireStrictFieldMapping(func() {
|
requireStrictFieldMapping(func() {
|
||||||
require.PanicsWithValue(t, "jet: fields never mapped: 'Inner.Child.Missing'", func() {
|
require.PanicsWithValue(t, "jet: fields never mapped: 'Child Inner.Missing'", func() {
|
||||||
var dest []Outer
|
var dest []Outer
|
||||||
_ = queryAll.Query(db, &dest)
|
_ = queryAll.Query(db, &dest)
|
||||||
})
|
})
|
||||||
|
|
@ -187,6 +187,95 @@ LIMIT ?;
|
||||||
_ = rows.Close()
|
_ = rows.Close()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("missing joined table columns panics for nested struct field", func(t *testing.T) {
|
||||||
|
filmOnly := SELECT(Film.AllColumns).FROM(Film).LIMIT(1)
|
||||||
|
|
||||||
|
type Dest struct {
|
||||||
|
model.Film
|
||||||
|
Actor model.Actor
|
||||||
|
}
|
||||||
|
|
||||||
|
allowUnmappedFields(func() {
|
||||||
|
var dest []Dest
|
||||||
|
require.NoError(t, filmOnly.Query(db, &dest))
|
||||||
|
require.Len(t, dest, 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
requireStrictFieldMapping(func() {
|
||||||
|
require.PanicsWithValue(t, "jet: fields never mapped: 'Actor Actor.ActorID', 'Actor Actor.FirstName', 'Actor Actor.LastName', 'Actor Actor.LastUpdate'", func() {
|
||||||
|
var dest []Dest
|
||||||
|
_ = filmOnly.Query(db, &dest)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("missing joined table columns do not panic when nested struct field is optional", func(t *testing.T) {
|
||||||
|
filmOnly := SELECT(Film.AllColumns).FROM(Film).LIMIT(1)
|
||||||
|
|
||||||
|
type Dest struct {
|
||||||
|
model.Film
|
||||||
|
Actor model.Actor `qrm:"optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
requireStrictFieldMapping(func() {
|
||||||
|
var dest []Dest
|
||||||
|
require.NoError(t, filmOnly.Query(db, &dest))
|
||||||
|
require.Len(t, dest, 1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("missing joined table columns panics for nested slice field", func(t *testing.T) {
|
||||||
|
filmOnly := SELECT(Film.AllColumns).FROM(Film).LIMIT(1)
|
||||||
|
|
||||||
|
type Dest struct {
|
||||||
|
model.Film
|
||||||
|
Actor []model.Actor
|
||||||
|
}
|
||||||
|
|
||||||
|
allowUnmappedFields(func() {
|
||||||
|
var dest []Dest
|
||||||
|
require.NoError(t, filmOnly.Query(db, &dest))
|
||||||
|
require.Len(t, dest, 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
requireStrictFieldMapping(func() {
|
||||||
|
require.PanicsWithValue(t, "jet: fields never mapped: 'Actor Actor.ActorID', 'Actor Actor.FirstName', 'Actor Actor.LastName', 'Actor Actor.LastUpdate'", func() {
|
||||||
|
var dest []Dest
|
||||||
|
_ = filmOnly.Query(db, &dest)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("missing joined table columns do not panic when nested slice field is optional", func(t *testing.T) {
|
||||||
|
filmOnly := SELECT(Film.AllColumns).FROM(Film).LIMIT(1)
|
||||||
|
|
||||||
|
type Dest struct {
|
||||||
|
model.Film
|
||||||
|
Actor []model.Actor `qrm:"optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
requireStrictFieldMapping(func() {
|
||||||
|
var dest []Dest
|
||||||
|
require.NoError(t, filmOnly.Query(db, &dest))
|
||||||
|
require.Len(t, dest, 1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("optional tag skips strict field mapping for missing simple field", func(t *testing.T) {
|
||||||
|
query := SELECT(Actor.ActorID).FROM(Actor).WHERE(Actor.ActorID.EQ(Int(2))).LIMIT(1)
|
||||||
|
|
||||||
|
type DestOptional struct {
|
||||||
|
ActorID int32 `alias:"actor.actor_id"`
|
||||||
|
OptionalMissing string `alias:"actor.missing_column" qrm:"optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
requireStrictFieldMapping(func() {
|
||||||
|
var dest []DestOptional
|
||||||
|
require.NoError(t, query.Query(db, &dest))
|
||||||
|
require.Len(t, dest, 1)
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
var actor2 = model.Actor{
|
var actor2 = model.Actor{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue