Skip to content

Commit 6625d12

Browse files
authored
fix(datastore): Ignore field mismatch errors (#8694)
* fix(datastore): Ignore field mismatch errors * fix(datastore): Refactor * feat(datastore): Addressing review comments * feat(datastore): Resolving vet failures * feat(datastore): Updating comment
1 parent c366c90 commit 6625d12

File tree

5 files changed

+253
-16
lines changed

5 files changed

+253
-16
lines changed

datastore/datastore.go

+49-3
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ type Client struct {
7474
dataset string // Called dataset by the datastore API, synonym for project ID.
7575
databaseID string // Default value is empty string
7676
readSettings *readSettings
77+
config *datastoreConfig
7778
}
7879

7980
// NewClient creates a new Client for a given dataset. If the project ID is
@@ -152,12 +153,15 @@ func NewClientWithDatabase(ctx context.Context, projectID, databaseID string, op
152153
if err != nil {
153154
return nil, fmt.Errorf("dialing: %w", err)
154155
}
156+
157+
config := newDatastoreConfig(o...)
155158
return &Client{
156159
connPool: connPool,
157160
client: newDatastoreClient(connPool, projectID, databaseID),
158161
dataset: projectID,
159162
readSettings: &readSettings{},
160163
databaseID: databaseID,
164+
config: &config,
161165
}, nil
162166
}
163167

@@ -362,6 +366,48 @@ func checkMultiArg(v reflect.Value) (m multiArgType, elemType reflect.Type) {
362366
return multiArgTypeInvalid, nil
363367
}
364368

369+
// processFieldMismatchError ignore FieldMismatchErr if WithIgnoreFieldMismatch client option is provided by user
370+
func (c *Client) processFieldMismatchError(err error) error {
371+
if c.config == nil || !c.config.ignoreFieldMismatchErrors {
372+
return err
373+
}
374+
return ignoreFieldMismatchErrs(err)
375+
}
376+
377+
func ignoreFieldMismatchErrs(err error) error {
378+
if err == nil {
379+
return err
380+
}
381+
382+
multiErr, isMultiErr := err.(MultiError)
383+
if isMultiErr {
384+
foundErr := false
385+
for i, e := range multiErr {
386+
multiErr[i] = ignoreFieldMismatchErr(e)
387+
if multiErr[i] != nil {
388+
foundErr = true
389+
}
390+
}
391+
if !foundErr {
392+
return nil
393+
}
394+
return multiErr
395+
}
396+
397+
return ignoreFieldMismatchErr(err)
398+
}
399+
400+
func ignoreFieldMismatchErr(err error) error {
401+
if err == nil {
402+
return err
403+
}
404+
_, isFieldMismatchErr := err.(*ErrFieldMismatch)
405+
if isFieldMismatchErr {
406+
return nil
407+
}
408+
return err
409+
}
410+
365411
// Close closes the Client. Call Close to clean up resources when done with the
366412
// Client.
367413
func (c *Client) Close() error {
@@ -402,9 +448,9 @@ func (c *Client) Get(ctx context.Context, key *Key, dst interface{}) (err error)
402448
// as transaction id which can be ignored
403449
_, err = c.get(ctx, []*Key{key}, []interface{}{dst}, opts)
404450
if me, ok := err.(MultiError); ok {
405-
return me[0]
451+
return c.processFieldMismatchError(me[0])
406452
}
407-
return err
453+
return c.processFieldMismatchError(err)
408454
}
409455

410456
// GetMulti is a batch version of Get.
@@ -436,7 +482,7 @@ func (c *Client) GetMulti(ctx context.Context, keys []*Key, dst interface{}) (er
436482
// Since opts does not contain Transaction option, 'get' call below will return nil
437483
// as transaction id which can be ignored
438484
_, err = c.get(ctx, keys, dst, opts)
439-
return err
485+
return c.processFieldMismatchError(err)
440486
}
441487

442488
func (c *Client) get(ctx context.Context, keys []*Key, dst interface{}, opts *pb.ReadOptions) ([]byte, error) {

datastore/integration_test.go

+134-8
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@ type replayInfo struct {
6666
var (
6767
record = flag.Bool("record", false, "record RPCs")
6868

69-
newTestClient = func(ctx context.Context, t *testing.T) *Client {
70-
return newClient(ctx, t, nil)
69+
newTestClient = func(ctx context.Context, t *testing.T, opts ...option.ClientOption) *Client {
70+
return newClient(ctx, t, nil, opts...)
7171
}
7272
testParams map[string]interface{}
7373

@@ -109,8 +109,8 @@ func testMain(m *testing.M) int {
109109
log.Fatalf("closing recorder: %v", err)
110110
}
111111
}()
112-
newTestClient = func(ctx context.Context, t *testing.T) *Client {
113-
return newClient(ctx, t, rec.DialOptions())
112+
newTestClient = func(ctx context.Context, t *testing.T, opts ...option.ClientOption) *Client {
113+
return newClient(ctx, t, rec.DialOptions(), opts...)
114114
}
115115
log.Printf("recording to %s", replayFilename)
116116
}
@@ -172,7 +172,7 @@ func initReplay() {
172172
log.Fatal(err)
173173
}
174174

175-
newTestClient = func(ctx context.Context, t *testing.T) *Client {
175+
newTestClient = func(ctx context.Context, t *testing.T, opts ...option.ClientOption) *Client {
176176
grpcHeadersEnforcer := &testutil.HeadersEnforcer{
177177
OnFailure: t.Fatalf,
178178
Checkers: []*testutil.HeaderChecker{
@@ -181,7 +181,8 @@ func initReplay() {
181181
},
182182
}
183183

184-
opts := append(grpcHeadersEnforcer.CallOptions(), option.WithGRPCConn(conn))
184+
opts = append(opts, grpcHeadersEnforcer.CallOptions()...)
185+
opts = append(opts, option.WithGRPCConn(conn))
185186
client, err := NewClientWithDatabase(ctx, ri.ProjectID, testParams["databaseID"].(string), opts...)
186187
if err != nil {
187188
t.Fatalf("NewClientWithDatabase: %v", err)
@@ -191,7 +192,7 @@ func initReplay() {
191192
log.Printf("replaying from %s", replayFilename)
192193
}
193194

194-
func newClient(ctx context.Context, t *testing.T, dialOpts []grpc.DialOption) *Client {
195+
func newClient(ctx context.Context, t *testing.T, dialOpts []grpc.DialOption, opts ...option.ClientOption) *Client {
195196
if testing.Short() {
196197
t.Skip("Integration tests skipped in short mode")
197198
}
@@ -207,7 +208,8 @@ func newClient(ctx context.Context, t *testing.T, dialOpts []grpc.DialOption) *C
207208
xGoogReqParamsHeaderChecker,
208209
},
209210
}
210-
opts := append(grpcHeadersEnforcer.CallOptions(), option.WithTokenSource(ts))
211+
opts = append(opts, grpcHeadersEnforcer.CallOptions()...)
212+
opts = append(opts, option.WithTokenSource(ts))
211213
for _, opt := range dialOpts {
212214
opts = append(opts, option.WithGRPCDialOption(opt))
213215
}
@@ -264,6 +266,130 @@ func TestIntegration_Basics(t *testing.T) {
264266
}
265267
}
266268

269+
type OldX struct {
270+
I int
271+
J int
272+
}
273+
type NewX struct {
274+
I int
275+
j int
276+
}
277+
278+
func TestIntegration_IgnoreFieldMismatch(t *testing.T) {
279+
ctx := context.Background()
280+
client := newTestClient(ctx, t, WithIgnoreFieldMismatch())
281+
t.Cleanup(func() {
282+
client.Close()
283+
})
284+
285+
// Save entities with an extra field
286+
keys := []*Key{
287+
NameKey("X", "x1", nil),
288+
NameKey("X", "x2", nil),
289+
}
290+
entitiesOld := []OldX{
291+
{I: 10, J: 20},
292+
{I: 30, J: 40},
293+
}
294+
_, gotErr := client.PutMulti(ctx, keys, entitiesOld)
295+
if gotErr != nil {
296+
t.Fatalf("Failed to save: %v\n", gotErr)
297+
}
298+
299+
var wants []NewX
300+
for _, oldX := range entitiesOld {
301+
wants = append(wants, []NewX{{I: oldX.I}}...)
302+
}
303+
304+
t.Cleanup(func() {
305+
client.DeleteMulti(ctx, keys)
306+
})
307+
308+
tests := []struct {
309+
desc string
310+
client *Client
311+
wantErr error
312+
}{
313+
{
314+
desc: "Without IgnoreFieldMismatch option",
315+
client: newTestClient(ctx, t),
316+
wantErr: &ErrFieldMismatch{
317+
StructType: reflect.TypeOf(NewX{}),
318+
FieldName: "J",
319+
Reason: "no such struct field",
320+
},
321+
},
322+
{
323+
desc: "With IgnoreFieldMismatch option",
324+
client: newTestClient(ctx, t, WithIgnoreFieldMismatch()),
325+
},
326+
}
327+
for _, test := range tests {
328+
t.Run(test.desc, func(t *testing.T) {
329+
defer test.client.Close()
330+
// FieldMismatch error in Next
331+
query := NewQuery("X").FilterField("I", ">=", 10)
332+
it := test.client.Run(ctx, query)
333+
resIndex := 0
334+
for {
335+
var newX NewX
336+
_, err := it.Next(&newX)
337+
if err == iterator.Done {
338+
break
339+
}
340+
341+
compareIgnoreFieldMismatchResults(t, []NewX{wants[resIndex]}, []NewX{newX}, test.wantErr, err, "Next")
342+
resIndex++
343+
}
344+
345+
// FieldMismatch error in Get
346+
var getX NewX
347+
gotErr = test.client.Get(ctx, keys[0], &getX)
348+
compareIgnoreFieldMismatchResults(t, []NewX{wants[0]}, []NewX{getX}, test.wantErr, gotErr, "Get")
349+
350+
// FieldMismatch error in GetAll
351+
var getAllX []NewX
352+
_, gotErr = test.client.GetAll(ctx, query, &getAllX)
353+
compareIgnoreFieldMismatchResults(t, wants, getAllX, test.wantErr, gotErr, "GetAll")
354+
355+
// FieldMismatch error in GetMulti
356+
getMultiX := make([]NewX, len(keys))
357+
gotErr = test.client.GetMulti(ctx, keys, getMultiX)
358+
compareIgnoreFieldMismatchResults(t, wants, getMultiX, test.wantErr, gotErr, "GetMulti")
359+
360+
tx, err := test.client.NewTransaction(ctx)
361+
if err != nil {
362+
t.Fatalf("tx.GetMulti got: %v, want: nil\n", err)
363+
}
364+
365+
// FieldMismatch error in tx.Get
366+
var txGetX NewX
367+
err = tx.Get(keys[0], &txGetX)
368+
compareIgnoreFieldMismatchResults(t, []NewX{wants[0]}, []NewX{txGetX}, test.wantErr, err, "tx.Get")
369+
370+
// FieldMismatch error in tx.GetMulti
371+
txGetMultiX := make([]NewX, len(keys))
372+
err = tx.GetMulti(keys, txGetMultiX)
373+
compareIgnoreFieldMismatchResults(t, wants, txGetMultiX, test.wantErr, err, "tx.GetMulti")
374+
375+
tx.Commit()
376+
377+
})
378+
}
379+
380+
}
381+
382+
func compareIgnoreFieldMismatchResults(t *testing.T, wantX []NewX, gotX []NewX, wantErr error, gotErr error, errPrefix string) {
383+
if !equalErrs(gotErr, wantErr) {
384+
t.Errorf("%v: error got: %v, want: %v", errPrefix, gotErr, wantErr)
385+
}
386+
for resIndex := 0; resIndex < len(wantX) && gotErr == nil; resIndex++ {
387+
if wantX[resIndex].I != gotX[resIndex].I {
388+
t.Fatalf("%v %v: got: %v, want: %v\n", errPrefix, resIndex, wantX[resIndex].I, gotX[resIndex].I)
389+
}
390+
}
391+
}
392+
267393
func TestIntegration_GetWithReadTime(t *testing.T) {
268394
ctx, cancel := context.WithTimeout(context.Background(), time.Second*20)
269395
client := newTestClient(ctx, t)

datastore/option.go

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Copyright 2024 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package datastore
16+
17+
import (
18+
"google.golang.org/api/option"
19+
"google.golang.org/api/option/internaloption"
20+
)
21+
22+
// datastoreConfig contains the Datastore client option configuration that can be
23+
// set through datastoreClientOptions.
24+
type datastoreConfig struct {
25+
ignoreFieldMismatchErrors bool
26+
}
27+
28+
// newDatastoreConfig generates a new datastoreConfig with all the given
29+
// datastoreClientOptions applied.
30+
func newDatastoreConfig(opts ...option.ClientOption) datastoreConfig {
31+
var conf datastoreConfig
32+
for _, opt := range opts {
33+
if datastoreOpt, ok := opt.(datastoreClientOption); ok {
34+
datastoreOpt.applyDatastoreOpt(&conf)
35+
}
36+
}
37+
return conf
38+
}
39+
40+
// A datastoreClientOption is an option for a Google Datastore client.
41+
type datastoreClientOption interface {
42+
option.ClientOption
43+
applyDatastoreOpt(*datastoreConfig)
44+
}
45+
46+
// WithIgnoreFieldMismatch allows ignoring ErrFieldMismatch error while
47+
// reading or querying data.
48+
// WARNING: Ignoring ErrFieldMismatch can cause data loss while writing
49+
// back to Datastore. E.g.
50+
// if entity written to Datastore is {X: 1, Y:2} and it is read into
51+
// type NewStruct struct{X int}, then {X:1} is returned.
52+
// Now, if this is written back to Datastore, there will be no Y field
53+
// left for this entity in Datastore
54+
func WithIgnoreFieldMismatch() option.ClientOption {
55+
return &withIgnoreFieldMismatch{ignoreFieldMismatchErrors: true}
56+
}
57+
58+
type withIgnoreFieldMismatch struct {
59+
internaloption.EmbeddableAdapter
60+
ignoreFieldMismatchErrors bool
61+
}
62+
63+
func (w *withIgnoreFieldMismatch) applyDatastoreOpt(c *datastoreConfig) {
64+
c.ignoreFieldMismatchErrors = true
65+
}

datastore/query.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -825,7 +825,7 @@ func (c *Client) GetAllWithOptions(ctx context.Context, q *Query, dst interface{
825825
}
826826
res.Keys = append(res.Keys, k)
827827
}
828-
return res, errFieldMismatch
828+
return res, c.processFieldMismatchError(errFieldMismatch)
829829
}
830830

831831
// Run runs the given query in the given context
@@ -1061,7 +1061,7 @@ func (t *Iterator) Next(dst interface{}) (k *Key, err error) {
10611061
if dst != nil && !t.keysOnly {
10621062
err = loadEntityProto(dst, e)
10631063
}
1064-
return k, err
1064+
return k, t.client.processFieldMismatchError(err)
10651065
}
10661066

10671067
func (t *Iterator) next() (*Key, *pb.Entity, error) {

datastore/transaction.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -571,7 +571,7 @@ func (t *Transaction) get(spanName string, keys []*Key, dst interface{}) (err er
571571
if txnID != nil && err == nil {
572572
t.setToInProgress(txnID)
573573
}
574-
return err
574+
return t.client.processFieldMismatchError(err)
575575
}
576576

577577
// Get is the transaction-specific version of the package function Get.
@@ -582,9 +582,9 @@ func (t *Transaction) get(spanName string, keys []*Key, dst interface{}) (err er
582582
func (t *Transaction) Get(key *Key, dst interface{}) (err error) {
583583
err = t.get("cloud.google.com/go/datastore.Transaction.Get", []*Key{key}, []interface{}{dst})
584584
if me, ok := err.(MultiError); ok {
585-
return me[0]
585+
return t.client.processFieldMismatchError(me[0])
586586
}
587-
return err
587+
return t.client.processFieldMismatchError(err)
588588
}
589589

590590
// GetMulti is a batch version of Get.

0 commit comments

Comments
 (0)