fix(datastore): handle opaque type conversions (#14477)
fixes #5225
diff --git a/datastore/load.go b/datastore/load.go
index 7793496..78b5d28 100644
--- a/datastore/load.go
+++ b/datastore/load.go
@@ -287,6 +287,10 @@
case reflect.String:
x, ok := pValue.(string)
if !ok && pValue != nil {
+ if b, ok := pValue.([]byte); ok {
+ v.SetString(string(b))
+ return ""
+ }
return typeMismatchReason(p, v)
}
v.SetString(x)
@@ -348,20 +352,56 @@
return err.Error()
}
case int64:
+ if v.Elem().Type() == typeOfTime {
+ s := x / 1e6
+ ns := (x % 1e6) * 1e3
+ v.Elem().Set(reflect.ValueOf(time.Unix(s, ns).In(time.UTC)))
+ return ""
+ }
+ if v.Elem().Kind() < reflect.Int || v.Elem().Kind() > reflect.Int64 {
+ return typeMismatchReason(p, v)
+ }
if v.Elem().OverflowInt(x) {
return overflowReason(x, v.Elem())
}
v.Elem().SetInt(x)
case float64:
+ if v.Elem().Kind() != reflect.Float32 && v.Elem().Kind() != reflect.Float64 {
+ return typeMismatchReason(p, v)
+ }
if v.Elem().OverflowFloat(x) {
return overflowReason(x, v.Elem())
}
v.Elem().SetFloat(x)
case bool:
+ if v.Elem().Kind() != reflect.Bool {
+ return typeMismatchReason(p, v)
+ }
v.Elem().SetBool(x)
case string:
- v.Elem().SetString(x)
+ if v.Elem().Kind() == reflect.String {
+ v.Elem().SetString(x)
+ return ""
+ }
+ if v.Elem().Kind() == reflect.Slice && v.Elem().Type().Elem().Kind() == reflect.Uint8 {
+ v.Elem().SetBytes([]byte(x))
+ return ""
+ }
+ return typeMismatchReason(p, v)
+ case []byte:
+ if v.Elem().Kind() == reflect.String {
+ v.Elem().SetString(string(x))
+ return ""
+ }
+ if v.Elem().Kind() == reflect.Slice && v.Elem().Type().Elem().Kind() == reflect.Uint8 {
+ v.Elem().SetBytes(x)
+ return ""
+ }
+ return typeMismatchReason(p, v)
case GeoPoint, time.Time:
+ if v.Elem().Type() != reflect.TypeOf(x) {
+ return typeMismatchReason(p, v)
+ }
v.Elem().Set(reflect.ValueOf(x))
default:
return typeMismatchReason(p, v)
@@ -377,7 +417,7 @@
micros, ok := pValue.(int64)
if ok {
s := micros / 1e6
- ns := micros % 1e6
+ ns := (micros % 1e6) * 1e3
v.Set(reflect.ValueOf(time.Unix(s, ns).In(time.UTC)))
break
}
@@ -412,11 +452,15 @@
}
}
case reflect.Slice:
- x, ok := pValue.([]byte)
- if !ok && pValue != nil {
+ if v.Type().Elem().Kind() != reflect.Uint8 {
return typeMismatchReason(p, v)
}
- if v.Type().Elem().Kind() != reflect.Uint8 {
+ x, ok := pValue.([]byte)
+ if !ok && pValue != nil {
+ if s, ok := pValue.(string); ok {
+ v.SetBytes([]byte(s))
+ return ""
+ }
return typeMismatchReason(p, v)
}
v.SetBytes(x)
diff --git a/datastore/load_test.go b/datastore/load_test.go
index 381871e..e2e12c5 100644
--- a/datastore/load_test.go
+++ b/datastore/load_test.go
@@ -1389,3 +1389,170 @@
}
}
}
+
+func TestIssue5225(t *testing.T) {
+ type CustomType struct {
+ ID []byte `datastore:"custom_identifier"`
+ Name string `datastore:"name"`
+ }
+
+ // Case 1: Struct has []byte, but Property has string (e.g. from projection)
+ t.Run("String To []byte", func(t *testing.T) {
+ x := &CustomType{}
+ ps := []Property{
+ {Name: "custom_identifier", Value: "some-id"},
+ }
+ err := LoadStruct(x, ps)
+ if err != nil {
+ t.Errorf("LoadStruct failed: %v", err)
+ }
+ if string(x.ID) != "some-id" {
+ t.Errorf("got x.ID = %q, want %q", string(x.ID), "some-id")
+ }
+ })
+
+ // Case 2: Struct has string, but Property has []byte (e.g. from projection)
+ t.Run("[]byte To String", func(t *testing.T) {
+ x := &CustomType{}
+ ps := []Property{
+ {Name: "name", Value: []byte("some-name")},
+ }
+ err := LoadStruct(x, ps)
+ if err != nil {
+ t.Errorf("LoadStruct failed: %v", err)
+ }
+ if x.Name != "some-name" {
+ t.Errorf("got x.Name = %q, want %q", x.Name, "some-name")
+ }
+ })
+
+ // Case 3: Pointer to string, but Property has []byte
+ t.Run("[]byte To *String", func(t *testing.T) {
+ type PtrType struct {
+ Name *string `datastore:"name"`
+ }
+ x := &PtrType{}
+ ps := []Property{
+ {Name: "name", Value: []byte("some-name")},
+ }
+ err := LoadStruct(x, ps)
+ if err != nil {
+ t.Errorf("LoadStruct failed: %v", err)
+ }
+ if x.Name == nil || *x.Name != "some-name" {
+ got := "nil"
+ if x.Name != nil {
+ got = *x.Name
+ }
+ t.Errorf("got x.Name = %q, want %q", got, "some-name")
+ }
+ })
+
+ // Case 4: time.Time, but Property has int64 (micros)
+ t.Run("int64 To time.Time", func(t *testing.T) {
+ type TimeType struct {
+ T time.Time `datastore:"t"`
+ }
+ x := &TimeType{}
+ // 1639728000123456 is 2021-12-17 08:00:00.123456 UTC
+ ps := []Property{
+ {Name: "t", Value: int64(1639728000123456)},
+ }
+ err := LoadStruct(x, ps)
+ if err != nil {
+ t.Errorf("LoadStruct failed: %v", err)
+ }
+ want := time.Unix(1639728000, 123456000).In(time.UTC)
+ if !x.T.Equal(want) {
+ t.Errorf("got x.T = %v, want %v", x.T, want)
+ }
+ })
+
+ // Case 5: *time.Time, but Property has int64 (micros)
+ t.Run("int64 To *time.Time", func(t *testing.T) {
+ type TimePtrType struct {
+ T *time.Time `datastore:"t"`
+ }
+ x := &TimePtrType{}
+ ps := []Property{
+ {Name: "t", Value: int64(1639728000123456)},
+ }
+ err := LoadStruct(x, ps)
+ if err != nil {
+ t.Errorf("LoadStruct failed: %v", err)
+ }
+ want := time.Unix(1639728000, 123456000).In(time.UTC)
+ if x.T == nil || !x.T.Equal(want) {
+ t.Errorf("got x.T = %v, want %v", x.T, want)
+ }
+ })
+
+ // Case 6: *[]byte, but Property has string
+ t.Run("String To *[]byte", func(t *testing.T) {
+ type PtrSliceType struct {
+ ID *[]byte `datastore:"id"`
+ }
+ x := &PtrSliceType{}
+ ps := []Property{
+ {Name: "id", Value: "some-id"},
+ }
+ err := LoadStruct(x, ps)
+ if err != nil {
+ t.Errorf("LoadStruct failed: %v", err)
+ }
+ if x.ID == nil || string(*x.ID) != "some-id" {
+ got := "nil"
+ if x.ID != nil {
+ got = string(*x.ID)
+ }
+ t.Errorf("got x.ID = %q, want %q", got, "some-id")
+ }
+ })
+
+ // Case 7: *[]byte, but Property has []byte
+ t.Run("[]byte To *[]byte", func(t *testing.T) {
+ type PtrSliceType struct {
+ ID *[]byte `datastore:"id"`
+ }
+ x := &PtrSliceType{}
+ ps := []Property{
+ {Name: "id", Value: []byte("some-id")},
+ }
+ err := LoadStruct(x, ps)
+ if err != nil {
+ t.Errorf("LoadStruct failed: %v", err)
+ }
+ if x.ID == nil || string(*x.ID) != "some-id" {
+ got := "nil"
+ if x.ID != nil {
+ got = string(*x.ID)
+ }
+ t.Errorf("got x.ID = %q, want %q", got, "some-id")
+ }
+ })
+
+ // Case 8: Saving *[]byte
+ t.Run("Save *[]byte", func(t *testing.T) {
+ type PtrSliceType struct {
+ ID *[]byte `datastore:"id"`
+ }
+ data := []byte("some-id")
+ x := &PtrSliceType{ID: &data}
+ props, err := SaveStruct(x)
+ if err != nil {
+ t.Errorf("SaveStruct failed: %v", err)
+ }
+ found := false
+ for _, p := range props {
+ if p.Name == "id" {
+ found = true
+ if string(p.Value.([]byte)) != "some-id" {
+ t.Errorf("got p.Value = %q, want %q", string(p.Value.([]byte)), "some-id")
+ }
+ }
+ }
+ if !found {
+ t.Errorf("property 'id' not found in %v", props)
+ }
+ })
+}
diff --git a/datastore/save.go b/datastore/save.go
index 5c1478e..e21fd52 100644
--- a/datastore/save.go
+++ b/datastore/save.go
@@ -520,7 +520,7 @@
// isValidPointerType reports whether a struct field can be a pointer to type t
// for the purposes of saving and loading.
func isValidPointerType(t reflect.Type) bool {
- if t == typeOfTime || t == typeOfGeoPoint {
+ if t == typeOfTime || t == typeOfGeoPoint || t == typeOfByteSlice {
return true
}
switch t.Kind() {