Skip to content

Commit 84d2146

Browse files
authored
fix(datastore): handle opaque type conversions (#14477)
fixes #5225
1 parent 3ff8f40 commit 84d2146

3 files changed

Lines changed: 217 additions & 6 deletions

File tree

datastore/load.go

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,10 @@ func setVal(v reflect.Value, p Property) (s string) {
287287
case reflect.String:
288288
x, ok := pValue.(string)
289289
if !ok && pValue != nil {
290+
if b, ok := pValue.([]byte); ok {
291+
v.SetString(string(b))
292+
return ""
293+
}
290294
return typeMismatchReason(p, v)
291295
}
292296
v.SetString(x)
@@ -348,20 +352,56 @@ func setVal(v reflect.Value, p Property) (s string) {
348352
return err.Error()
349353
}
350354
case int64:
355+
if v.Elem().Type() == typeOfTime {
356+
s := x / 1e6
357+
ns := (x % 1e6) * 1e3
358+
v.Elem().Set(reflect.ValueOf(time.Unix(s, ns).In(time.UTC)))
359+
return ""
360+
}
361+
if v.Elem().Kind() < reflect.Int || v.Elem().Kind() > reflect.Int64 {
362+
return typeMismatchReason(p, v)
363+
}
351364
if v.Elem().OverflowInt(x) {
352365
return overflowReason(x, v.Elem())
353366
}
354367
v.Elem().SetInt(x)
355368
case float64:
369+
if v.Elem().Kind() != reflect.Float32 && v.Elem().Kind() != reflect.Float64 {
370+
return typeMismatchReason(p, v)
371+
}
356372
if v.Elem().OverflowFloat(x) {
357373
return overflowReason(x, v.Elem())
358374
}
359375
v.Elem().SetFloat(x)
360376
case bool:
377+
if v.Elem().Kind() != reflect.Bool {
378+
return typeMismatchReason(p, v)
379+
}
361380
v.Elem().SetBool(x)
362381
case string:
363-
v.Elem().SetString(x)
382+
if v.Elem().Kind() == reflect.String {
383+
v.Elem().SetString(x)
384+
return ""
385+
}
386+
if v.Elem().Kind() == reflect.Slice && v.Elem().Type().Elem().Kind() == reflect.Uint8 {
387+
v.Elem().SetBytes([]byte(x))
388+
return ""
389+
}
390+
return typeMismatchReason(p, v)
391+
case []byte:
392+
if v.Elem().Kind() == reflect.String {
393+
v.Elem().SetString(string(x))
394+
return ""
395+
}
396+
if v.Elem().Kind() == reflect.Slice && v.Elem().Type().Elem().Kind() == reflect.Uint8 {
397+
v.Elem().SetBytes(x)
398+
return ""
399+
}
400+
return typeMismatchReason(p, v)
364401
case GeoPoint, time.Time:
402+
if v.Elem().Type() != reflect.TypeOf(x) {
403+
return typeMismatchReason(p, v)
404+
}
365405
v.Elem().Set(reflect.ValueOf(x))
366406
default:
367407
return typeMismatchReason(p, v)
@@ -377,7 +417,7 @@ func setVal(v reflect.Value, p Property) (s string) {
377417
micros, ok := pValue.(int64)
378418
if ok {
379419
s := micros / 1e6
380-
ns := micros % 1e6
420+
ns := (micros % 1e6) * 1e3
381421
v.Set(reflect.ValueOf(time.Unix(s, ns).In(time.UTC)))
382422
break
383423
}
@@ -412,11 +452,15 @@ func setVal(v reflect.Value, p Property) (s string) {
412452
}
413453
}
414454
case reflect.Slice:
415-
x, ok := pValue.([]byte)
416-
if !ok && pValue != nil {
455+
if v.Type().Elem().Kind() != reflect.Uint8 {
417456
return typeMismatchReason(p, v)
418457
}
419-
if v.Type().Elem().Kind() != reflect.Uint8 {
458+
x, ok := pValue.([]byte)
459+
if !ok && pValue != nil {
460+
if s, ok := pValue.(string); ok {
461+
v.SetBytes([]byte(s))
462+
return ""
463+
}
420464
return typeMismatchReason(p, v)
421465
}
422466
v.SetBytes(x)

datastore/load_test.go

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1389,3 +1389,170 @@ func TestKeyLoaderEndToEnd(t *testing.T) {
13891389
}
13901390
}
13911391
}
1392+
1393+
func TestIssue5225(t *testing.T) {
1394+
type CustomType struct {
1395+
ID []byte `datastore:"custom_identifier"`
1396+
Name string `datastore:"name"`
1397+
}
1398+
1399+
// Case 1: Struct has []byte, but Property has string (e.g. from projection)
1400+
t.Run("String To []byte", func(t *testing.T) {
1401+
x := &CustomType{}
1402+
ps := []Property{
1403+
{Name: "custom_identifier", Value: "some-id"},
1404+
}
1405+
err := LoadStruct(x, ps)
1406+
if err != nil {
1407+
t.Errorf("LoadStruct failed: %v", err)
1408+
}
1409+
if string(x.ID) != "some-id" {
1410+
t.Errorf("got x.ID = %q, want %q", string(x.ID), "some-id")
1411+
}
1412+
})
1413+
1414+
// Case 2: Struct has string, but Property has []byte (e.g. from projection)
1415+
t.Run("[]byte To String", func(t *testing.T) {
1416+
x := &CustomType{}
1417+
ps := []Property{
1418+
{Name: "name", Value: []byte("some-name")},
1419+
}
1420+
err := LoadStruct(x, ps)
1421+
if err != nil {
1422+
t.Errorf("LoadStruct failed: %v", err)
1423+
}
1424+
if x.Name != "some-name" {
1425+
t.Errorf("got x.Name = %q, want %q", x.Name, "some-name")
1426+
}
1427+
})
1428+
1429+
// Case 3: Pointer to string, but Property has []byte
1430+
t.Run("[]byte To *String", func(t *testing.T) {
1431+
type PtrType struct {
1432+
Name *string `datastore:"name"`
1433+
}
1434+
x := &PtrType{}
1435+
ps := []Property{
1436+
{Name: "name", Value: []byte("some-name")},
1437+
}
1438+
err := LoadStruct(x, ps)
1439+
if err != nil {
1440+
t.Errorf("LoadStruct failed: %v", err)
1441+
}
1442+
if x.Name == nil || *x.Name != "some-name" {
1443+
got := "nil"
1444+
if x.Name != nil {
1445+
got = *x.Name
1446+
}
1447+
t.Errorf("got x.Name = %q, want %q", got, "some-name")
1448+
}
1449+
})
1450+
1451+
// Case 4: time.Time, but Property has int64 (micros)
1452+
t.Run("int64 To time.Time", func(t *testing.T) {
1453+
type TimeType struct {
1454+
T time.Time `datastore:"t"`
1455+
}
1456+
x := &TimeType{}
1457+
// 1639728000123456 is 2021-12-17 08:00:00.123456 UTC
1458+
ps := []Property{
1459+
{Name: "t", Value: int64(1639728000123456)},
1460+
}
1461+
err := LoadStruct(x, ps)
1462+
if err != nil {
1463+
t.Errorf("LoadStruct failed: %v", err)
1464+
}
1465+
want := time.Unix(1639728000, 123456000).In(time.UTC)
1466+
if !x.T.Equal(want) {
1467+
t.Errorf("got x.T = %v, want %v", x.T, want)
1468+
}
1469+
})
1470+
1471+
// Case 5: *time.Time, but Property has int64 (micros)
1472+
t.Run("int64 To *time.Time", func(t *testing.T) {
1473+
type TimePtrType struct {
1474+
T *time.Time `datastore:"t"`
1475+
}
1476+
x := &TimePtrType{}
1477+
ps := []Property{
1478+
{Name: "t", Value: int64(1639728000123456)},
1479+
}
1480+
err := LoadStruct(x, ps)
1481+
if err != nil {
1482+
t.Errorf("LoadStruct failed: %v", err)
1483+
}
1484+
want := time.Unix(1639728000, 123456000).In(time.UTC)
1485+
if x.T == nil || !x.T.Equal(want) {
1486+
t.Errorf("got x.T = %v, want %v", x.T, want)
1487+
}
1488+
})
1489+
1490+
// Case 6: *[]byte, but Property has string
1491+
t.Run("String To *[]byte", func(t *testing.T) {
1492+
type PtrSliceType struct {
1493+
ID *[]byte `datastore:"id"`
1494+
}
1495+
x := &PtrSliceType{}
1496+
ps := []Property{
1497+
{Name: "id", Value: "some-id"},
1498+
}
1499+
err := LoadStruct(x, ps)
1500+
if err != nil {
1501+
t.Errorf("LoadStruct failed: %v", err)
1502+
}
1503+
if x.ID == nil || string(*x.ID) != "some-id" {
1504+
got := "nil"
1505+
if x.ID != nil {
1506+
got = string(*x.ID)
1507+
}
1508+
t.Errorf("got x.ID = %q, want %q", got, "some-id")
1509+
}
1510+
})
1511+
1512+
// Case 7: *[]byte, but Property has []byte
1513+
t.Run("[]byte To *[]byte", func(t *testing.T) {
1514+
type PtrSliceType struct {
1515+
ID *[]byte `datastore:"id"`
1516+
}
1517+
x := &PtrSliceType{}
1518+
ps := []Property{
1519+
{Name: "id", Value: []byte("some-id")},
1520+
}
1521+
err := LoadStruct(x, ps)
1522+
if err != nil {
1523+
t.Errorf("LoadStruct failed: %v", err)
1524+
}
1525+
if x.ID == nil || string(*x.ID) != "some-id" {
1526+
got := "nil"
1527+
if x.ID != nil {
1528+
got = string(*x.ID)
1529+
}
1530+
t.Errorf("got x.ID = %q, want %q", got, "some-id")
1531+
}
1532+
})
1533+
1534+
// Case 8: Saving *[]byte
1535+
t.Run("Save *[]byte", func(t *testing.T) {
1536+
type PtrSliceType struct {
1537+
ID *[]byte `datastore:"id"`
1538+
}
1539+
data := []byte("some-id")
1540+
x := &PtrSliceType{ID: &data}
1541+
props, err := SaveStruct(x)
1542+
if err != nil {
1543+
t.Errorf("SaveStruct failed: %v", err)
1544+
}
1545+
found := false
1546+
for _, p := range props {
1547+
if p.Name == "id" {
1548+
found = true
1549+
if string(p.Value.([]byte)) != "some-id" {
1550+
t.Errorf("got p.Value = %q, want %q", string(p.Value.([]byte)), "some-id")
1551+
}
1552+
}
1553+
}
1554+
if !found {
1555+
t.Errorf("property 'id' not found in %v", props)
1556+
}
1557+
})
1558+
}

datastore/save.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -520,7 +520,7 @@ func isEmptyValue(v reflect.Value) bool {
520520
// isValidPointerType reports whether a struct field can be a pointer to type t
521521
// for the purposes of saving and loading.
522522
func isValidPointerType(t reflect.Type) bool {
523-
if t == typeOfTime || t == typeOfGeoPoint {
523+
if t == typeOfTime || t == typeOfGeoPoint || t == typeOfByteSlice {
524524
return true
525525
}
526526
switch t.Kind() {

0 commit comments

Comments
 (0)