Skip to content

Commit

Permalink
feat(spanner): add ToStructLenient method to decode to struct fields …
Browse files Browse the repository at this point in the history
…with no error return with un-matched row's column with struct's exported fields. (#5153)

Co-authored-by: Rahul Yadav <irahul@google.com>
  • Loading branch information
rahul2393 and rahul2393 committed Nov 22, 2021
1 parent 37c240e commit 899ffbf
Show file tree
Hide file tree
Showing 6 changed files with 582 additions and 239 deletions.
15 changes: 13 additions & 2 deletions spanner/client_test.go
Expand Up @@ -2198,6 +2198,7 @@ func TestClient_DecodeCustomFieldType(t *testing.T) {
defer iter.Stop()

var results []typesTable
var lenientResults []typesTable
for {
row, err := iter.Next()
if err == iterator.Done {
Expand All @@ -2212,9 +2213,15 @@ func TestClient_DecodeCustomFieldType(t *testing.T) {
t.Fatalf("failed to convert a row to a struct: %v", err)
}
results = append(results, d)

var d2 typesTable
if err := row.ToStructLenient(&d2); err != nil {
t.Fatalf("failed to convert a row to a struct: %v", err)
}
lenientResults = append(lenientResults, d2)
}

if len(results) > 1 {
if len(results) > 1 || len(lenientResults) > 1 {
t.Fatalf("mismatch length of array: got %v, want 1", results)
}

Expand All @@ -2228,7 +2235,11 @@ func TestClient_DecodeCustomFieldType(t *testing.T) {
}
got := results[0]
if !testEqual(got, want) {
t.Fatalf("mismatch result: got %v, want %v", got, want)
t.Fatalf("mismatch result from ToStruct: got %v, want %v", got, want)
}
got = lenientResults[0]
if !testEqual(got, want) {
t.Fatalf("mismatch result from ToStructLenient: got %v, want %v", got, want)
}
}

Expand Down
24 changes: 24 additions & 0 deletions spanner/examples_test.go
Expand Up @@ -404,6 +404,30 @@ func ExampleRow_ToStruct() {
fmt.Println(acct)
}

func ExampleRow_ToStructLenient() {
ctx := context.Background()
client, err := spanner.NewClient(ctx, myDB)
if err != nil {
// TODO: Handle error.
}
row, err := client.Single().ReadRow(ctx, "Accounts", spanner.Key{"alice"}, []string{"accountID", "name", "balance"})
if err != nil {
// TODO: Handle error.
}

type Account struct {
Name string
Balance int64
NickName string
}

var acct Account
if err := row.ToStructLenient(&acct); err != nil {
// TODO: Handle error.
}
fmt.Println(acct)
}

func ExampleReadOnlyTransaction_Read() {
ctx := context.Background()
client, err := spanner.NewClient(ctx, myDB)
Expand Down
47 changes: 47 additions & 0 deletions spanner/row.go
Expand Up @@ -288,6 +288,10 @@ func errToStructArgType(p interface{}) error {
// 2. Otherwise, if the name of a field matches the name of a column (ignoring case),
// decode the column into the field.
//
// 3. The number of columns in the row must match the number of exported fields in the struct.
// There must be exactly one match for each column in the row. The method will return an error
// if a column in the row cannot be assigned to a field in the struct.
//
// The fields of the destination struct can be of any type that is acceptable
// to spanner.Row.Column.
//
Expand All @@ -311,5 +315,48 @@ func (r *Row) ToStruct(p interface{}) error {
&sppb.StructType{Fields: r.fields},
&proto3.ListValue{Values: r.vals},
p,
false,
)
}

// ToStructLenient fetches the columns in a row into the fields of a struct.
// The rules for mapping a row's columns into a struct's exported fields
// are:
//
// 1. If a field has a `spanner: "column_name"` tag, then decode column
// 'column_name' into the field. A special case is the `spanner: "-"`
// tag, which instructs ToStruct to ignore the field during decoding.
//
// 2. Otherwise, if the name of a field matches the name of a column (ignoring case),
// decode the column into the field.
//
// 3. The number of columns in the row and exported fields in the struct do not need to match.
// Any field in the struct that cannot not be assigned a value from the row is assigned its default value.
// Any column in the row that does not have a corresponding field in the struct is ignored.
//
// The fields of the destination struct can be of any type that is acceptable
// to spanner.Row.Column.
//
// Slice and pointer fields will be set to nil if the source column is NULL, and a
// non-nil value if the column is not NULL. To decode NULL values of other types, use
// one of the spanner.NullXXX types as the type of the destination field.
//
// If ToStructLenient returns an error, the contents of p are undefined. Some fields may
// have been successfully populated, while others were not; you should not use any of
// the fields.
func (r *Row) ToStructLenient(p interface{}) error {
// Check if p is a pointer to a struct
if t := reflect.TypeOf(p); t == nil || t.Kind() != reflect.Ptr || t.Elem().Kind() != reflect.Struct {
return errToStructArgType(p)
}
if len(r.vals) != len(r.fields) {
return errFieldsMismatchVals(r)
}
// Call decodeStruct directly to decode the row as a typed proto.ListValue.
return decodeStruct(
&sppb.StructType{Fields: r.fields},
&proto3.ListValue{Values: r.vals},
p,
true,
)
}

0 comments on commit 899ffbf

Please sign in to comment.