Repository: googleapis/google-cloud-go (Go)

Issue:
spanner: SelectAll does not use the same field matching rules as Row.ToStruct
Assume a table like

```
		CREATE TABLE test (
			id INT64 NOT NULL,
			name STRING(MAX),
		) PRIMARY KEY (id);
```

**Expected behavior**

`SelectAll`, when passed a slice of

```
struct {
		ID   int64 
		Name string
}
``` 

should behave like `Row.ToStruct`, and perform case-insensitive comparison between the column name and the field name. In this case, this means mapping the `id` column to the `ID` field, and the `name` column to the `Name` field.

**Actual behavior**

Contrary to `Row.ToStruct`, `SelectAll` does not perform a case-insensitive compare, and thus returns the error `Go struct {ID:0 Name:}(type reflect.Value) has no or duplicate fields for Cloud Spanner STRUCT field id`.

Adding field tags like

```
struct {
		ID   int64  `spanner:"id"`
		Name string `spanner:"name"`
}
```

makes the problem go away, but `Row.ToStruct` does not require them.



Discussion:

Patch:
diff --git a/spanner/row.go b/spanner/row.go
index 814e1268fe9a..1f541a1858a3 100644
--- a/spanner/row.go
+++ b/spanner/row.go
@@ -19,6 +19,7 @@ package spanner
 import (
 	"fmt"
 	"reflect"
+	"strings"
 
 	sppb "cloud.google.com/go/spanner/apiv1/spannerpb"
 	proto3 "github.com/golang/protobuf/ptypes/struct"
@@ -518,7 +519,7 @@ func structPointers(sliceItem reflect.Value, cols []*sppb.StructType_Field, leni
 		}
 
 		var fieldVal reflect.Value
-		if v, ok := fieldTag[colName.GetName()]; ok {
+		if v, ok := fieldTag[strings.ToLower(colName.GetName())]; ok {
 			fieldVal = v
 		} else {
 			if !lenient {
@@ -565,7 +566,7 @@ func initFieldTag(sliceItem reflect.Value, fieldTagMap *map[string]reflect.Value
 			continue
 		}
 		if name == "" {
-			name = fieldType.Name
+			name = strings.ToLower(fieldType.Name)
 		}
 		(*fieldTagMap)[name] = sliceItem.Field(i)
 	}


Test patch:
diff --git a/spanner/row_test.go b/spanner/row_test.go
index b127ac1d9fa0..87d795bdb0e6 100644
--- a/spanner/row_test.go
+++ b/spanner/row_test.go
@@ -2003,7 +2003,8 @@ func TestSelectAll(t *testing.T) {
 	}
 	type testStruct struct {
 		Col1 int64
-		Col2 float64
+		// declaring second column in upper case here to verify SelectAll does case-insensitive matching
+		COL2 float64
 		Col3 string
 		Col4 time.Time
 	}
@@ -2087,8 +2088,8 @@ func TestSelectAll(t *testing.T) {
 				},
 			},
 			want: &[]testStruct{
-				{Col1: 1, Col2: 1.1, Col3: "value", Col4: tm},
-				{Col1: 2, Col2: 2.2, Col3: "value2", Col4: tm.Add(24 * time.Hour)},
+				{Col1: 1, COL2: 1.1, Col3: "value", Col4: tm},
+				{Col1: 2, COL2: 2.2, Col3: "value2", Col4: tm.Add(24 * time.Hour)},
 			},
 		},
 		{
@@ -2117,8 +2118,8 @@ func TestSelectAll(t *testing.T) {
 				},
 			},
 			want: &[]*testStruct{
-				{Col1: 1, Col2: 1.1, Col3: "value"},
-				{Col1: 2, Col2: 2.2, Col3: "value2"},
+				{Col1: 1, COL2: 1.1, Col3: "value"},
+				{Col1: 2, COL2: 2.2, Col3: "value2"},
 			}},
 		{
 			name: "success: when spanner row contains more columns than declared in Go struct but called WithLenient",
@@ -2142,13 +2143,13 @@ func TestSelectAll(t *testing.T) {
 				options: []DecodeOptions{WithLenient()},
 			},
 			want: &[]*testStruct{
-				{Col1: 1, Col2: 1.1, Col3: "value", Col4: tm},
+				{Col1: 1, COL2: 1.1, Col3: "value", Col4: tm},
 			},
 		},
 		{
 			name: "success: using prefilled destination should append to the destination",
 			args: args{
-				destination: &[]*testStruct{{Col1: 3, Col2: 3.3, Col3: "value3"}},
+				destination: &[]*testStruct{{Col1: 3, COL2: 3.3, Col3: "value3"}},
 				mock: func(mockIterator *mockRowIterator) {
 					mockIterator.On("Next").Once().Return(&Row{
 						[]*sppb.StructType_Field{
@@ -2171,14 +2172,14 @@ func TestSelectAll(t *testing.T) {
 				},
 			},
 			want: &[]*testStruct{
-				{Col1: 3, Col2: 3.3, Col3: "value3"},
-				{Col1: 1, Col2: 1.1, Col3: "value"},
-				{Col1: 2, Col2: 2.2, Col3: "value2"},
+				{Col1: 3, COL2: 3.3, Col3: "value3"},
+				{Col1: 1, COL2: 1.1, Col3: "value"},
+				{Col1: 2, COL2: 2.2, Col3: "value2"},
 			}},
 		{
 			name: "failure: in case of error destination will have the partial result",
 			args: args{
-				destination: &[]*testStruct{{Col1: 3, Col2: 3.3, Col3: "value3"}},
+				destination: &[]*testStruct{{Col1: 3, COL2: 3.3, Col3: "value3"}},
 				mock: func(mockIterator *mockRowIterator) {
 					mockIterator.On("Next").Once().Return(&Row{
 						[]*sppb.StructType_Field{
@@ -2194,15 +2195,15 @@ func TestSelectAll(t *testing.T) {
 				},
 			},
 			want: &[]*testStruct{
-				{Col1: 3, Col2: 3.3, Col3: "value3"},
-				{Col1: 1, Col2: 1.1, Col3: "value"},
+				{Col1: 3, COL2: 3.3, Col3: "value3"},
+				{Col1: 1, COL2: 1.1, Col3: "value"},
 			},
 			wantErr: true,
 		},
 		{
 			name: "failure: when spanner row contains more columns than declared in Go struct",
 			args: args{
-				destination: &[]*testStruct{{Col1: 3, Col2: 3.3, Col3: "value3"}},
+				destination: &[]*testStruct{{Col1: 3, COL2: 3.3, Col3: "value3"}},
 				mock: func(mockIterator *mockRowIterator) {
 					mockIterator.On("Next").Once().Return(&Row{
 						[]*sppb.StructType_Field{
@@ -2219,7 +2220,7 @@ func TestSelectAll(t *testing.T) {
 				},
 			},
 			want: &[]*testStruct{
-				{Col1: 3, Col2: 3.3, Col3: "value3"},
+				{Col1: 3, COL2: 3.3, Col3: "value3"},
 			},
 			wantErr: true,
 		},