Skip to content

Commit d57d7ae

Browse files
committed
Improve datastore support, particularly for embedded structs
1 parent ffa7fb2 commit d57d7ae

File tree

11 files changed

+488
-563
lines changed

11 files changed

+488
-563
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ Just switch the import path from `cloud.google.com/go/datastore` to `github.com/
5050

5151
These features are unsupported just because we haven't found a use for the feature yet. PRs welcome:
5252

53-
* Embedded structs, nested slices, map types, some advanced query features (streaming aggregations, OR filters).
53+
* Nested slices, map types, some advanced query features (streaming aggregations, OR filters).
5454

5555
## Testing
5656

pkg/datastore/encode_coverage_test.go

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,9 @@ func TestEncodeValue_ReflectionSlices(t *testing.T) {
3434

3535
for _, tt := range tests {
3636
t.Run(tt.name, func(t *testing.T) {
37-
result, err := encodeValue(tt.value)
37+
result, err := encodeAny(tt.value)
3838
if err != nil {
39-
t.Errorf("encodeValue(%v) failed: %v", tt.value, err)
39+
t.Errorf("encodeAny(%v) failed: %v", tt.value, err)
4040
}
4141
if result == nil {
4242
t.Error("Expected non-nil result")
@@ -63,17 +63,14 @@ func TestEncodeValue_Errors(t *testing.T) {
6363
"channel type",
6464
make(chan int),
6565
},
66-
{
67-
"struct type",
68-
struct{ Name string }{Name: "test"},
69-
},
66+
// Note: struct types are now supported as nested entities
7067
}
7168

7269
for _, tt := range tests {
7370
t.Run(tt.name, func(t *testing.T) {
74-
_, err := encodeValue(tt.value)
71+
_, err := encodeAny(tt.value)
7572
if err == nil {
76-
t.Errorf("encodeValue(%T) should have returned an error", tt.value)
73+
t.Errorf("encodeAny(%T) should have returned an error", tt.value)
7774
}
7875
})
7976
}
@@ -86,7 +83,7 @@ func TestEncodeValue_TimeSlice(t *testing.T) {
8683

8784
timeSlice := []time.Time{now, later}
8885

89-
result, err := encodeValue(timeSlice)
86+
result, err := encodeAny(timeSlice)
9087
if err != nil {
9188
t.Fatalf("encodeValue failed for time slice: %v", err)
9289
}
@@ -122,7 +119,7 @@ func TestEncodeValue_EmptySlices(t *testing.T) {
122119

123120
for _, tt := range tests {
124121
t.Run(tt.name, func(t *testing.T) {
125-
result, err := encodeValue(tt.value)
122+
result, err := encodeAny(tt.value)
126123
if err != nil {
127124
t.Errorf("encodeValue failed: %v", err)
128125
}
@@ -148,7 +145,7 @@ func TestEncodeValue_SingleElementSlices(t *testing.T) {
148145

149146
for _, tt := range tests {
150147
t.Run(tt.name, func(t *testing.T) {
151-
result, err := encodeValue(tt.value)
148+
result, err := encodeAny(tt.value)
152149
if err != nil {
153150
t.Errorf("encodeValue failed: %v", err)
154151
}

pkg/datastore/entity.go

Lines changed: 7 additions & 310 deletions
Original file line numberDiff line numberDiff line change
@@ -1,313 +1,10 @@
11
package datastore
22

3-
import (
4-
"errors"
5-
"fmt"
6-
"reflect"
7-
"strconv"
8-
"strings"
9-
"time"
10-
)
11-
12-
// encodeEntity converts a Go struct to a Datastore entity.
13-
func encodeEntity(key *Key, src any) (map[string]any, error) {
14-
v := reflect.ValueOf(src)
15-
if v.Kind() == reflect.Ptr {
16-
v = v.Elem()
17-
}
18-
19-
if v.Kind() != reflect.Struct {
20-
return nil, errors.New("src must be a struct or pointer to struct")
21-
}
22-
23-
t := v.Type()
24-
properties := make(map[string]any)
25-
26-
for i := range v.NumField() {
27-
field := t.Field(i)
28-
value := v.Field(i)
29-
30-
// Skip unexported fields
31-
if !field.IsExported() {
32-
continue
33-
}
34-
35-
// Get field name from datastore tag or use field name
36-
name := field.Name
37-
noIndex := false
38-
39-
if tag := field.Tag.Get("datastore"); tag != "" {
40-
parts := strings.Split(tag, ",")
41-
if parts[0] != "" && parts[0] != "-" {
42-
name = parts[0]
43-
}
44-
if len(parts) > 1 && parts[1] == "noindex" {
45-
noIndex = true
46-
}
47-
if parts[0] == "-" {
48-
continue
49-
}
50-
}
51-
52-
prop, err := encodeValue(value.Interface())
53-
if err != nil {
54-
return nil, fmt.Errorf("field %s: %w", field.Name, err)
55-
}
56-
57-
if noIndex {
58-
if m, ok := prop.(map[string]any); ok {
59-
m["excludeFromIndexes"] = true
60-
}
61-
}
62-
63-
properties[name] = prop
64-
}
65-
66-
return map[string]any{
67-
"key": keyToJSON(key),
68-
"properties": properties,
69-
}, nil
70-
}
71-
72-
// encodeValue converts a Go value to a Datastore property value.
73-
func encodeValue(v any) (any, error) {
74-
if v == nil {
75-
return map[string]any{"nullValue": nil}, nil
76-
}
77-
78-
switch val := v.(type) {
79-
case string:
80-
return map[string]any{"stringValue": val}, nil
81-
case int:
82-
return map[string]any{"integerValue": strconv.Itoa(val)}, nil
83-
case int64:
84-
return map[string]any{"integerValue": strconv.FormatInt(val, 10)}, nil
85-
case int32:
86-
return map[string]any{"integerValue": strconv.Itoa(int(val))}, nil
87-
case bool:
88-
return map[string]any{"booleanValue": val}, nil
89-
case float64:
90-
return map[string]any{"doubleValue": val}, nil
91-
case time.Time:
92-
return map[string]any{"timestampValue": val.Format(time.RFC3339Nano)}, nil
93-
case []string:
94-
values := make([]map[string]any, len(val))
95-
for i, s := range val {
96-
values[i] = map[string]any{"stringValue": s}
97-
}
98-
return map[string]any{"arrayValue": map[string]any{"values": values}}, nil
99-
case []int64:
100-
values := make([]map[string]any, len(val))
101-
for i, n := range val {
102-
values[i] = map[string]any{"integerValue": strconv.FormatInt(n, 10)}
103-
}
104-
return map[string]any{"arrayValue": map[string]any{"values": values}}, nil
105-
case []int:
106-
values := make([]map[string]any, len(val))
107-
for i, n := range val {
108-
values[i] = map[string]any{"integerValue": strconv.Itoa(n)}
109-
}
110-
return map[string]any{"arrayValue": map[string]any{"values": values}}, nil
111-
case []float64:
112-
values := make([]map[string]any, len(val))
113-
for i, f := range val {
114-
values[i] = map[string]any{"doubleValue": f}
115-
}
116-
return map[string]any{"arrayValue": map[string]any{"values": values}}, nil
117-
case []bool:
118-
values := make([]map[string]any, len(val))
119-
for i, b := range val {
120-
values[i] = map[string]any{"booleanValue": b}
121-
}
122-
return map[string]any{"arrayValue": map[string]any{"values": values}}, nil
123-
default:
124-
// Try to handle slices/arrays via reflection
125-
rv := reflect.ValueOf(v)
126-
if rv.Kind() == reflect.Slice || rv.Kind() == reflect.Array {
127-
length := rv.Len()
128-
values := make([]map[string]any, length)
129-
for i := range length {
130-
elem := rv.Index(i).Interface()
131-
encodedElem, err := encodeValue(elem)
132-
if err != nil {
133-
return nil, fmt.Errorf("failed to encode array element %d: %w", i, err)
134-
}
135-
// encodedElem is already a map[string]any with the type wrapper
136-
m, ok := encodedElem.(map[string]any)
137-
if !ok {
138-
return nil, fmt.Errorf("unexpected encoded value type for element %d", i)
139-
}
140-
values[i] = m
141-
}
142-
return map[string]any{"arrayValue": map[string]any{"values": values}}, nil
143-
}
144-
return nil, fmt.Errorf("unsupported type: %T", v)
145-
}
146-
}
147-
148-
// decodeEntity converts a Datastore entity to a Go struct.
149-
func decodeEntity(entity map[string]any, dst any) error {
150-
v := reflect.ValueOf(dst)
151-
if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Struct {
152-
return errors.New("dst must be a pointer to struct")
153-
}
154-
155-
v = v.Elem()
156-
t := v.Type()
157-
158-
properties, ok := entity["properties"].(map[string]any)
159-
if !ok {
160-
return errors.New("invalid entity format")
161-
}
162-
163-
for i := range v.NumField() {
164-
field := t.Field(i)
165-
value := v.Field(i)
166-
167-
if !field.IsExported() {
168-
continue
169-
}
170-
171-
// Get field name from datastore tag
172-
name := field.Name
173-
if tag := field.Tag.Get("datastore"); tag != "" {
174-
parts := strings.Split(tag, ",")
175-
if parts[0] != "" && parts[0] != "-" {
176-
name = parts[0]
177-
}
178-
if parts[0] == "-" {
179-
continue
180-
}
181-
}
3+
import "errors"
1824

183-
prop, ok := properties[name]
184-
if !ok {
185-
continue // Field not in entity
186-
}
187-
188-
propMap, ok := prop.(map[string]any)
189-
if !ok {
190-
continue
191-
}
192-
193-
if err := decodeValue(propMap, value); err != nil {
194-
return fmt.Errorf("field %s: %w", field.Name, err)
195-
}
196-
}
197-
198-
return nil
199-
}
200-
201-
// decodeValue decodes a Datastore property value into a Go reflect.Value.
202-
func decodeValue(prop map[string]any, dst reflect.Value) error {
203-
// Handle each type
204-
if val, ok := prop["stringValue"]; ok {
205-
if dst.Kind() == reflect.String {
206-
if s, ok := val.(string); ok {
207-
dst.SetString(s)
208-
return nil
209-
}
210-
}
211-
}
212-
213-
if val, ok := prop["integerValue"]; ok {
214-
var intVal int64
215-
switch v := val.(type) {
216-
case string:
217-
if _, err := fmt.Sscanf(v, "%d", &intVal); err != nil {
218-
return fmt.Errorf("invalid integer format: %w", err)
219-
}
220-
case float64:
221-
intVal = int64(v)
222-
}
223-
224-
switch dst.Kind() {
225-
case reflect.Int, reflect.Int64, reflect.Int32:
226-
dst.SetInt(intVal)
227-
return nil
228-
default:
229-
return fmt.Errorf("unsupported integer type: %v", dst.Kind())
230-
}
231-
}
232-
233-
if val, ok := prop["booleanValue"]; ok {
234-
if dst.Kind() == reflect.Bool {
235-
if b, ok := val.(bool); ok {
236-
dst.SetBool(b)
237-
return nil
238-
}
239-
}
240-
}
241-
242-
if val, ok := prop["doubleValue"]; ok {
243-
if dst.Kind() == reflect.Float64 {
244-
if f, ok := val.(float64); ok {
245-
dst.SetFloat(f)
246-
return nil
247-
}
248-
}
249-
}
250-
251-
if val, ok := prop["timestampValue"]; ok {
252-
if dst.Type() == reflect.TypeOf(time.Time{}) {
253-
if s, ok := val.(string); ok {
254-
t, err := time.Parse(time.RFC3339Nano, s)
255-
if err != nil {
256-
return err
257-
}
258-
dst.Set(reflect.ValueOf(t))
259-
return nil
260-
}
261-
}
262-
}
263-
264-
if val, ok := prop["arrayValue"]; ok {
265-
if dst.Kind() != reflect.Slice {
266-
return fmt.Errorf("cannot decode array into non-slice type: %s", dst.Type())
267-
}
268-
269-
arrayMap, ok := val.(map[string]any)
270-
if !ok {
271-
return errors.New("invalid arrayValue format")
272-
}
273-
274-
valuesAny, ok := arrayMap["values"]
275-
if !ok {
276-
// Empty array
277-
dst.Set(reflect.MakeSlice(dst.Type(), 0, 0))
278-
return nil
279-
}
280-
281-
values, ok := valuesAny.([]any)
282-
if !ok {
283-
return errors.New("invalid arrayValue.values format")
284-
}
285-
286-
// Create slice with appropriate capacity
287-
slice := reflect.MakeSlice(dst.Type(), len(values), len(values))
288-
289-
// Decode each element
290-
for i, elemAny := range values {
291-
elemMap, ok := elemAny.(map[string]any)
292-
if !ok {
293-
return fmt.Errorf("invalid array element %d format", i)
294-
}
295-
296-
elemValue := slice.Index(i)
297-
if err := decodeValue(elemMap, elemValue); err != nil {
298-
return fmt.Errorf("failed to decode array element %d: %w", i, err)
299-
}
300-
}
301-
302-
dst.Set(slice)
303-
return nil
304-
}
305-
306-
if _, ok := prop["nullValue"]; ok {
307-
// Set to zero value
308-
dst.Set(reflect.Zero(dst.Type()))
309-
return nil
310-
}
311-
312-
return fmt.Errorf("unsupported property type for %s", dst.Type())
313-
}
5+
// Entity encoding/decoding errors.
6+
var (
7+
errNotStruct = errors.New("src must be a struct or pointer to struct")
8+
errNotStructPtr = errors.New("dst must be a pointer to struct")
9+
errInvalidEntity = errors.New("invalid entity format")
10+
)

0 commit comments

Comments
 (0)