Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions schema/field.go
Original file line number Diff line number Diff line change
Expand Up @@ -920,11 +920,46 @@ func (field *Field) setupValuerAndSetter(modelType reflect.Type) {
}
} else if _, ok := fieldValue.Interface().(sql.Scanner); ok {
// struct scanner

// isNilFieldPath checks if any intermediate pointer in the field access path is nil.
// This is used to skip Scan calls for fields within nil embedded pointer structs.
isNilFieldPath := func(v reflect.Value) bool {
v = reflect.Indirect(v)
for idx, fieldIdx := range field.StructField.Index {
if fieldIdx >= 0 {
v = v.Field(fieldIdx)
if v.Kind() == reflect.Ptr && v.IsNil() && idx < len(field.StructField.Index)-1 {
return true
}
} else {
v = v.Field(-fieldIdx - 1)
if v.IsNil() {
return true
}
if idx < len(field.StructField.Index)-1 {
v = v.Elem()
}
}
}
return false
}

field.Set = func(ctx context.Context, value reflect.Value, v interface{}) (err error) {
reflectV := reflect.ValueOf(v)
if !reflectV.IsValid() {
field.ReflectValueOf(ctx, value).Set(reflect.New(field.FieldType).Elem())
} else if reflectV.Kind() == reflect.Ptr && reflectV.IsNil() {
// Handle nil values for non-pointer struct scanners:
// - Keep pointer-type fields as nil
// - Skip fields in nil embedded pointer structs
// - Call Scan(nil) for standalone non-pointer scanner fields
if field.FieldType.Kind() == reflect.Ptr {
return
}
if isNilFieldPath(value) {
return
}
err = field.ReflectValueOf(ctx, value).Addr().Interface().(sql.Scanner).Scan(nil)
return
} else if reflectV.Type().AssignableTo(field.FieldType) {
field.ReflectValueOf(ctx, value).Set(reflectV)
Expand Down
70 changes: 70 additions & 0 deletions tests/scanner_valuer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (

"gorm.io/gorm"
"gorm.io/gorm/clause"
"gorm.io/gorm/schema"
. "gorm.io/gorm/utils/tests"
)

Expand Down Expand Up @@ -140,6 +141,31 @@ func TestInvalidValuer(t *testing.T) {
AssertEqual(t, data.Password, EncryptedData("newpass"))
}

func TestScannerHandlesNullForNonPointerField(t *testing.T) {
DB.Migrator().DropTable(&NullScannerModel{})
if err := DB.AutoMigrate(&NullScannerModel{}); err != nil {
t.Fatalf("failed to migrate NullScannerModel, got %v", err)
}

record := NullScannerModel{}
if err := DB.Create(&record).Error; err != nil {
t.Fatalf("failed to create NullScannerModel, got %v", err)
}

var result NullScannerModel
if err := DB.First(&result, record.ID).Error; err != nil {
t.Fatalf("failed to query NullScannerModel, got %v", err)
}

if result.Settings == nil {
t.Errorf("expected scanner to initialize empty map for NULL value")
}

if len(result.Settings) != 0 {
t.Errorf("expected empty map for NULL value, got %v", result.Settings)
}
}

type ScannerValuerStruct struct {
gorm.Model
Name sql.NullString
Expand Down Expand Up @@ -310,6 +336,50 @@ type NullString struct {
sql.NullString
}

type NullScannerModel struct {
ID uint
Settings extraSettings
}

type extraSettings map[string]any

func (s extraSettings) Value() (driver.Value, error) {
if s == nil {
return nil, nil
}

return json.Marshal(s)
}

func (s *extraSettings) Scan(src interface{}) error {
if src == nil {
*s = map[string]any{}
return nil
}

switch v := src.(type) {
case []byte:
return json.Unmarshal(v, s)
case string:
return json.Unmarshal([]byte(v), s)
default:
return fmt.Errorf("unsupported type %T for extraSettings", src)
}
}

func (extraSettings) GormDataType() string {
return "json"
}

func (extraSettings) GormDBDataType(db *gorm.DB, field *schema.Field) string {
switch db.Dialector.Name() {
case "sqlserver":
return "nvarchar(max)"
default:
return "json"
}
}

type Point struct {
X, Y int
}
Expand Down
Loading