Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(spanner): add ToStructLenient method to decode to struct fields with no error return in case of un-matched row's column with struct's exported fields #5153

Merged
merged 3 commits into from Nov 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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.
//
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
//
//
// 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.

// 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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func (r *Row) ToStructLenient(p interface{}) error {
// If ToStruct 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 {

This warning is still true for this method. Although this method is less likely to return an error, it can still fail if for example the type of one of the fields is not compatible with the datatype of the corresponding column.

// 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,
)
}