feat: add StrictFieldMapping config
This commit is contained in:
parent
adef2f9b1a
commit
f33c2ee357
4 changed files with 219 additions and 9 deletions
21
qrm/qrm.go
21
qrm/qrm.go
|
|
@ -6,8 +6,9 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/go-jet/jet/v2/internal/utils/must"
|
||||
"reflect"
|
||||
|
||||
"github.com/go-jet/jet/v2/internal/utils/must"
|
||||
)
|
||||
|
||||
// Config holds the configuration settings for QRM scanning behavior.
|
||||
|
|
@ -18,6 +19,13 @@ type Config struct {
|
|||
// Does not apply to statements build with SELECT_JSON_OBJ or SELECT_JSON_ARR
|
||||
StrictScan bool
|
||||
|
||||
// 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.
|
||||
// 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.
|
||||
// Does not apply to statements build with SELECT_JSON_OBJ or SELECT_JSON_ARR
|
||||
StrictFieldMapping bool
|
||||
|
||||
// JsonUnmarshalFunc is called by the Query method to unmarshal JSON query results created by
|
||||
// SELECT_JSON_OBJ and SELECT_JSON_ARR statements.
|
||||
// It can be replaced with any implementation that matches the standard "encoding/json" `Unmarshal` function signature.
|
||||
|
|
@ -28,8 +36,9 @@ type Config struct {
|
|||
// GlobalConfig is the package-wide configuration for SQL scanning.
|
||||
// This variable is not thread safe, and it should be modified only once, for instance, during application initialization.
|
||||
var GlobalConfig = Config{
|
||||
StrictScan: false,
|
||||
JsonUnmarshalFunc: json.Unmarshal,
|
||||
StrictScan: false,
|
||||
StrictFieldMapping: false,
|
||||
JsonUnmarshalFunc: json.Unmarshal,
|
||||
}
|
||||
|
||||
// ErrNoRows is returned by Query when query result set is empty
|
||||
|
|
@ -230,6 +239,9 @@ func ScanOneRowToDest(scanContext *ScanContext, rows *sql.Rows, destPtr interfac
|
|||
}
|
||||
|
||||
scanContext.EnsureEveryColumnRead() // can panic
|
||||
if GlobalConfig.StrictFieldMapping {
|
||||
scanContext.EnsureEveryFieldMapped() // can panic
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -276,6 +288,9 @@ func queryToSlice(ctx context.Context, db Queryable, query string, args []interf
|
|||
if scanContext.rowNum == 1 && GlobalConfig.StrictScan {
|
||||
scanContext.EnsureEveryColumnRead()
|
||||
}
|
||||
if scanContext.rowNum == 1 && GlobalConfig.StrictFieldMapping {
|
||||
scanContext.EnsureEveryFieldMapped()
|
||||
}
|
||||
}
|
||||
|
||||
err = rows.Close()
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ type ScanContext struct {
|
|||
typesVisited typeStack // to prevent circular dependency scan
|
||||
columnAlias []string
|
||||
columnIndexRead []bool
|
||||
|
||||
unmappedFields []string
|
||||
}
|
||||
|
||||
// NewScanContext creates new ScanContext from rows
|
||||
|
|
@ -79,6 +81,33 @@ func (s *ScanContext) EnsureEveryColumnRead() {
|
|||
}
|
||||
}
|
||||
|
||||
func (s *ScanContext) recordUnmappedField(structType reflect.Type, parentField *reflect.StructField, field reflect.StructField) {
|
||||
// skip private/unsettable fields (those are ignored by mapRowToStruct anyway)
|
||||
if field.PkgPath != "" {
|
||||
return
|
||||
}
|
||||
|
||||
// NOTE: For unnamed/anonymous structs, Name() is empty, so String() is used for readability/uniqueness.
|
||||
typeName := structType.String()
|
||||
if structType.Name() != "" {
|
||||
typeName = structType.Name()
|
||||
}
|
||||
|
||||
fieldIdent := fmt.Sprintf("%s.%s", typeName, field.Name)
|
||||
if parentField != nil {
|
||||
fieldIdent = fmt.Sprintf("%s.%s.%s", typeName, parentField.Name, field.Name)
|
||||
}
|
||||
|
||||
s.unmappedFields = append(s.unmappedFields, fmt.Sprintf("'%s'", fieldIdent))
|
||||
}
|
||||
|
||||
func (s *ScanContext) EnsureEveryFieldMapped() {
|
||||
if len(s.unmappedFields) == 0 {
|
||||
return
|
||||
}
|
||||
panic("jet: fields never mapped: " + strings.Join(s.unmappedFields, ", "))
|
||||
}
|
||||
|
||||
func createScanSlice(columnCount int) []interface{} {
|
||||
scanPtrSlice := make([]interface{}, columnCount)
|
||||
|
||||
|
|
@ -144,6 +173,10 @@ func (s *ScanContext) getTypeInfo(structType reflect.Type, parentField *reflect.
|
|||
fieldMap.Type = simpleType
|
||||
}
|
||||
|
||||
if GlobalConfig.StrictFieldMapping && fieldMap.rowIndex == -1 && fieldMap.Type != complexType {
|
||||
s.recordUnmappedField(structType, parentField, field)
|
||||
}
|
||||
|
||||
newTypeInfo.fieldMappings = append(newTypeInfo.fieldMappings, fieldMap)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue