spanner: Support encoding custom types

Support encoding custom types that point back to supported basic
types.

Fixes #853.

Change-Id: I669a717dae021400ce3b5b2c3a441bdedd82cfc2
Reviewed-on: https://code-review.googlesource.com/c/gocloud/+/49873
Reviewed-by: kokoro <noreply+kokoro@google.com>
Reviewed-by: Tyler Bui-Palsulich <tbp@google.com>
diff --git a/spanner/value.go b/spanner/value.go
index 74a0d70..25f2f58 100644
--- a/spanner/value.go
+++ b/spanner/value.go
@@ -1899,6 +1899,16 @@
 	case []GenericColumnValue:
 		return nil, nil, errEncoderUnsupportedType(v)
 	default:
+		// Check if the value is a variant of a base type.
+		decodableType := getDecodableSpannerType(v)
+		if decodableType != spannerTypeUnknown && decodableType != spannerTypeInvalid {
+			converted, err := convertCustomTypeValue(decodableType, v)
+			if err != nil {
+				return nil, nil, err
+			}
+			return encodeValue(converted)
+		}
+
 		if !isStructOrArrayOfStructValue(v) {
 			return nil, nil, errEncoderUnsupportedType(v)
 		}
@@ -1918,6 +1928,130 @@
 	return pb, pt, nil
 }
 
+func convertCustomTypeValue(sourceType decodableSpannerType, v interface{}) (interface{}, error) {
+	// destination will be initialized to a base type. The input value will be
+	// converted to this type and copied to destination.
+	var destination reflect.Value
+	switch sourceType {
+	case spannerTypeInvalid:
+		return nil, fmt.Errorf("cannot encode a value to type spannerTypeInvalid")
+	case spannerTypeNonNullString:
+		destination = reflect.Indirect(reflect.New(reflect.TypeOf("")))
+	case spannerTypeNullString:
+		destination = reflect.Indirect(reflect.New(reflect.TypeOf(NullString{})))
+	case spannerTypeByteArray:
+		// Return a nil array directly if the input value is nil instead of
+		// creating an empty slice and returning that.
+		if reflect.ValueOf(v).IsNil() {
+			return []byte(nil), nil
+		}
+		destination = reflect.MakeSlice(reflect.TypeOf([]byte{}), reflect.ValueOf(v).Len(), reflect.ValueOf(v).Cap())
+	case spannerTypeNonNullInt64:
+		destination = reflect.Indirect(reflect.New(reflect.TypeOf(int64(0))))
+	case spannerTypeNullInt64:
+		destination = reflect.Indirect(reflect.New(reflect.TypeOf(NullInt64{})))
+	case spannerTypeNonNullBool:
+		destination = reflect.Indirect(reflect.New(reflect.TypeOf(false)))
+	case spannerTypeNullBool:
+		destination = reflect.Indirect(reflect.New(reflect.TypeOf(NullBool{})))
+	case spannerTypeNonNullFloat64:
+		destination = reflect.Indirect(reflect.New(reflect.TypeOf(float64(0.0))))
+	case spannerTypeNullFloat64:
+		destination = reflect.Indirect(reflect.New(reflect.TypeOf(NullFloat64{})))
+	case spannerTypeNonNullTime:
+		destination = reflect.Indirect(reflect.New(reflect.TypeOf(time.Time{})))
+	case spannerTypeNullTime:
+		destination = reflect.Indirect(reflect.New(reflect.TypeOf(NullTime{})))
+	case spannerTypeNonNullDate:
+		destination = reflect.Indirect(reflect.New(reflect.TypeOf(civil.Date{})))
+	case spannerTypeNullDate:
+		destination = reflect.Indirect(reflect.New(reflect.TypeOf(NullDate{})))
+	case spannerTypeArrayOfNonNullString:
+		if reflect.ValueOf(v).IsNil() {
+			return []string(nil), nil
+		}
+		destination = reflect.MakeSlice(reflect.TypeOf([]string{}), reflect.ValueOf(v).Len(), reflect.ValueOf(v).Cap())
+	case spannerTypeArrayOfNullString:
+		if reflect.ValueOf(v).IsNil() {
+			return []NullString(nil), nil
+		}
+		destination = reflect.MakeSlice(reflect.TypeOf([]NullString{}), reflect.ValueOf(v).Len(), reflect.ValueOf(v).Cap())
+	case spannerTypeArrayOfByteArray:
+		if reflect.ValueOf(v).IsNil() {
+			return [][]byte(nil), nil
+		}
+		destination = reflect.MakeSlice(reflect.TypeOf([][]byte{}), reflect.ValueOf(v).Len(), reflect.ValueOf(v).Cap())
+	case spannerTypeArrayOfNonNullInt64:
+		if reflect.ValueOf(v).IsNil() {
+			return []int64(nil), nil
+		}
+		destination = reflect.MakeSlice(reflect.TypeOf([]int64{}), reflect.ValueOf(v).Len(), reflect.ValueOf(v).Cap())
+	case spannerTypeArrayOfNullInt64:
+		if reflect.ValueOf(v).IsNil() {
+			return []NullInt64(nil), nil
+		}
+		destination = reflect.MakeSlice(reflect.TypeOf([]NullInt64{}), reflect.ValueOf(v).Len(), reflect.ValueOf(v).Cap())
+	case spannerTypeArrayOfNonNullBool:
+		if reflect.ValueOf(v).IsNil() {
+			return []bool(nil), nil
+		}
+		destination = reflect.MakeSlice(reflect.TypeOf([]bool{}), reflect.ValueOf(v).Len(), reflect.ValueOf(v).Cap())
+	case spannerTypeArrayOfNullBool:
+		if reflect.ValueOf(v).IsNil() {
+			return []NullBool(nil), nil
+		}
+		destination = reflect.MakeSlice(reflect.TypeOf([]NullBool{}), reflect.ValueOf(v).Len(), reflect.ValueOf(v).Cap())
+	case spannerTypeArrayOfNonNullFloat64:
+		if reflect.ValueOf(v).IsNil() {
+			return []float64(nil), nil
+		}
+		destination = reflect.MakeSlice(reflect.TypeOf([]float64{}), reflect.ValueOf(v).Len(), reflect.ValueOf(v).Cap())
+	case spannerTypeArrayOfNullFloat64:
+		if reflect.ValueOf(v).IsNil() {
+			return []NullFloat64(nil), nil
+		}
+		destination = reflect.MakeSlice(reflect.TypeOf([]NullFloat64{}), reflect.ValueOf(v).Len(), reflect.ValueOf(v).Cap())
+	case spannerTypeArrayOfNonNullTime:
+		if reflect.ValueOf(v).IsNil() {
+			return []time.Time(nil), nil
+		}
+		destination = reflect.MakeSlice(reflect.TypeOf([]time.Time{}), reflect.ValueOf(v).Len(), reflect.ValueOf(v).Cap())
+	case spannerTypeArrayOfNullTime:
+		if reflect.ValueOf(v).IsNil() {
+			return []NullTime(nil), nil
+		}
+		destination = reflect.MakeSlice(reflect.TypeOf([]NullTime{}), reflect.ValueOf(v).Len(), reflect.ValueOf(v).Cap())
+	case spannerTypeArrayOfNonNullDate:
+		if reflect.ValueOf(v).IsNil() {
+			return []civil.Date(nil), nil
+		}
+		destination = reflect.MakeSlice(reflect.TypeOf([]civil.Date{}), reflect.ValueOf(v).Len(), reflect.ValueOf(v).Cap())
+	case spannerTypeArrayOfNullDate:
+		if reflect.ValueOf(v).IsNil() {
+			return []NullDate(nil), nil
+		}
+		destination = reflect.MakeSlice(reflect.TypeOf([]NullDate{}), reflect.ValueOf(v).Len(), reflect.ValueOf(v).Cap())
+	default:
+		// This should not be possible.
+		return nil, fmt.Errorf("unknown decodable type found: %v", sourceType)
+	}
+	// destination has been initialized. Convert and copy the input value to
+	// destination. That must be done per element if the input type is a slice
+	// or an array.
+	if destination.Kind() == reflect.Slice || destination.Kind() == reflect.Array {
+		sourceSlice := reflect.ValueOf(v)
+		for i := 0; i < destination.Len(); i++ {
+			source := reflect.Indirect(sourceSlice.Index(i))
+			destination.Index(i).Set(source.Convert(destination.Type().Elem()))
+		}
+	} else {
+		source := reflect.Indirect(reflect.ValueOf(v))
+		destination.Set(source.Convert(destination.Type()))
+	}
+	// Return the converted value.
+	return destination.Interface(), nil
+}
+
 // Encodes a Go struct value/ptr in v to the spanner Value and Type protos. v
 // itself must be non-nil.
 func encodeStruct(v interface{}) (*proto3.Value, *sppb.Type, error) {
diff --git a/spanner/value_test.go b/spanner/value_test.go
index 24016bd..4bd942f 100644
--- a/spanner/value_test.go
+++ b/spanner/value_test.go
@@ -59,6 +59,21 @@
 
 // Test encoding Values.
 func TestEncodeValue(t *testing.T) {
+	type CustomString string
+	type CustomBytes []byte
+	type CustomInt64 int64
+	type CustomBool bool
+	type CustomFloat64 float64
+	type CustomTime time.Time
+	type CustomDate civil.Date
+
+	type CustomNullString NullString
+	type CustomNullInt64 NullInt64
+	type CustomNullBool NullBool
+	type CustomNullFloat64 NullFloat64
+	type CustomNullTime NullTime
+	type CustomNullDate NullDate
+
 	var (
 		tString = stringType()
 		tInt    = intType()
@@ -72,60 +87,61 @@
 		in       interface{}
 		want     *proto3.Value
 		wantType *sppb.Type
+		name     string
 	}{
-		// STRING / STRING ARRAY
-		{"abc", stringProto("abc"), tString},
-		{NullString{"abc", true}, stringProto("abc"), tString},
-		{NullString{"abc", false}, nullProto(), tString},
-		{[]string(nil), nullProto(), listType(tString)},
-		{[]string{"abc", "bcd"}, listProto(stringProto("abc"), stringProto("bcd")), listType(tString)},
-		{[]NullString{{"abcd", true}, {"xyz", false}}, listProto(stringProto("abcd"), nullProto()), listType(tString)},
+		// STRING / STRING ARRAY:
+		{"abc", stringProto("abc"), tString, "string"},
+		{NullString{"abc", true}, stringProto("abc"), tString, "NullString with value"},
+		{NullString{"abc", false}, nullProto(), tString, "NullString with null"},
+		{[]string(nil), nullProto(), listType(tString), "null []string"},
+		{[]string{"abc", "bcd"}, listProto(stringProto("abc"), stringProto("bcd")), listType(tString), "[]string"},
+		{[]NullString{{"abcd", true}, {"xyz", false}}, listProto(stringProto("abcd"), nullProto()), listType(tString), "[]NullString"},
 		// BYTES / BYTES ARRAY
-		{[]byte("foo"), bytesProto([]byte("foo")), tBytes},
-		{[]byte(nil), nullProto(), tBytes},
-		{[][]byte{nil, []byte("ab")}, listProto(nullProto(), bytesProto([]byte("ab"))), listType(tBytes)},
-		{[][]byte(nil), nullProto(), listType(tBytes)},
+		{[]byte("foo"), bytesProto([]byte("foo")), tBytes, "[]byte with value"},
+		{[]byte(nil), nullProto(), tBytes, "null []byte"},
+		{[][]byte{nil, []byte("ab")}, listProto(nullProto(), bytesProto([]byte("ab"))), listType(tBytes), "[][]byte"},
+		{[][]byte(nil), nullProto(), listType(tBytes), "null [][]byte"},
 		// INT64 / INT64 ARRAY
-		{7, intProto(7), tInt},
-		{[]int(nil), nullProto(), listType(tInt)},
-		{[]int{31, 127}, listProto(intProto(31), intProto(127)), listType(tInt)},
-		{int64(81), intProto(81), tInt},
-		{[]int64(nil), nullProto(), listType(tInt)},
-		{[]int64{33, 129}, listProto(intProto(33), intProto(129)), listType(tInt)},
-		{NullInt64{11, true}, intProto(11), tInt},
-		{NullInt64{11, false}, nullProto(), tInt},
-		{[]NullInt64{{35, true}, {131, false}}, listProto(intProto(35), nullProto()), listType(tInt)},
+		{7, intProto(7), tInt, "int"},
+		{[]int(nil), nullProto(), listType(tInt), "null []int"},
+		{[]int{31, 127}, listProto(intProto(31), intProto(127)), listType(tInt), "[]int"},
+		{int64(81), intProto(81), tInt, "int64"},
+		{[]int64(nil), nullProto(), listType(tInt), "null []int64"},
+		{[]int64{33, 129}, listProto(intProto(33), intProto(129)), listType(tInt), "[]int64"},
+		{NullInt64{11, true}, intProto(11), tInt, "NullInt64 with value"},
+		{NullInt64{11, false}, nullProto(), tInt, "NullInt64 with null"},
+		{[]NullInt64{{35, true}, {131, false}}, listProto(intProto(35), nullProto()), listType(tInt), "[]NullInt64"},
 		// BOOL / BOOL ARRAY
-		{true, boolProto(true), tBool},
-		{NullBool{true, true}, boolProto(true), tBool},
-		{NullBool{true, false}, nullProto(), tBool},
-		{[]bool{true, false}, listProto(boolProto(true), boolProto(false)), listType(tBool)},
-		{[]NullBool{{true, true}, {true, false}}, listProto(boolProto(true), nullProto()), listType(tBool)},
+		{true, boolProto(true), tBool, "bool"},
+		{NullBool{true, true}, boolProto(true), tBool, "NullBool with value"},
+		{NullBool{true, false}, nullProto(), tBool, "NullBool with null"},
+		{[]bool{true, false}, listProto(boolProto(true), boolProto(false)), listType(tBool), "[]bool"},
+		{[]NullBool{{true, true}, {true, false}}, listProto(boolProto(true), nullProto()), listType(tBool), "[]NullBool"},
 		// FLOAT64 / FLOAT64 ARRAY
-		{3.14, floatProto(3.14), tFloat},
-		{NullFloat64{3.1415, true}, floatProto(3.1415), tFloat},
-		{NullFloat64{math.Inf(1), true}, floatProto(math.Inf(1)), tFloat},
-		{NullFloat64{3.14159, false}, nullProto(), tFloat},
-		{[]float64(nil), nullProto(), listType(tFloat)},
-		{[]float64{3.141, 0.618, math.Inf(-1)}, listProto(floatProto(3.141), floatProto(0.618), floatProto(math.Inf(-1))), listType(tFloat)},
-		{[]NullFloat64{{3.141, true}, {0.618, false}}, listProto(floatProto(3.141), nullProto()), listType(tFloat)},
+		{3.14, floatProto(3.14), tFloat, "float"},
+		{NullFloat64{3.1415, true}, floatProto(3.1415), tFloat, "NullFloat64 with value"},
+		{NullFloat64{math.Inf(1), true}, floatProto(math.Inf(1)), tFloat, "NullFloat64 with infinity"},
+		{NullFloat64{3.14159, false}, nullProto(), tFloat, "NullFloat64 with null"},
+		{[]float64(nil), nullProto(), listType(tFloat), "null []float64"},
+		{[]float64{3.141, 0.618, math.Inf(-1)}, listProto(floatProto(3.141), floatProto(0.618), floatProto(math.Inf(-1))), listType(tFloat), "[]float64"},
+		{[]NullFloat64{{3.141, true}, {0.618, false}}, listProto(floatProto(3.141), nullProto()), listType(tFloat), "[]NullFloat64"},
 		// TIMESTAMP / TIMESTAMP ARRAY
-		{t1, timeProto(t1), tTime},
-		{NullTime{t1, true}, timeProto(t1), tTime},
-		{NullTime{t1, false}, nullProto(), tTime},
-		{[]time.Time(nil), nullProto(), listType(tTime)},
-		{[]time.Time{t1, t2, t3, t4}, listProto(timeProto(t1), timeProto(t2), timeProto(t3), timeProto(t4)), listType(tTime)},
-		{[]NullTime{{t1, true}, {t1, false}}, listProto(timeProto(t1), nullProto()), listType(tTime)},
+		{t1, timeProto(t1), tTime, "time"},
+		{NullTime{t1, true}, timeProto(t1), tTime, "NullTime with value"},
+		{NullTime{t1, false}, nullProto(), tTime, "NullTime with null"},
+		{[]time.Time(nil), nullProto(), listType(tTime), "null []time"},
+		{[]time.Time{t1, t2, t3, t4}, listProto(timeProto(t1), timeProto(t2), timeProto(t3), timeProto(t4)), listType(tTime), "[]time"},
+		{[]NullTime{{t1, true}, {t1, false}}, listProto(timeProto(t1), nullProto()), listType(tTime), "[]NullTime"},
 		// DATE / DATE ARRAY
-		{d1, dateProto(d1), tDate},
-		{NullDate{d1, true}, dateProto(d1), tDate},
-		{NullDate{civil.Date{}, false}, nullProto(), tDate},
-		{[]civil.Date(nil), nullProto(), listType(tDate)},
-		{[]civil.Date{d1, d2}, listProto(dateProto(d1), dateProto(d2)), listType(tDate)},
-		{[]NullDate{{d1, true}, {civil.Date{}, false}}, listProto(dateProto(d1), nullProto()), listType(tDate)},
+		{d1, dateProto(d1), tDate, "date"},
+		{NullDate{d1, true}, dateProto(d1), tDate, "NullDate with value"},
+		{NullDate{civil.Date{}, false}, nullProto(), tDate, "NullDate with null"},
+		{[]civil.Date(nil), nullProto(), listType(tDate), "null []date"},
+		{[]civil.Date{d1, d2}, listProto(dateProto(d1), dateProto(d2)), listType(tDate), "[]date"},
+		{[]NullDate{{d1, true}, {civil.Date{}, false}}, listProto(dateProto(d1), nullProto()), listType(tDate), "[]NullDate"},
 		// GenericColumnValue
-		{GenericColumnValue{tString, stringProto("abc")}, stringProto("abc"), tString},
-		{GenericColumnValue{tString, nullProto()}, nullProto(), tString},
+		{GenericColumnValue{tString, stringProto("abc")}, stringProto("abc"), tString, "GenericColumnValue with value"},
+		{GenericColumnValue{tString, nullProto()}, nullProto(), tString, "GenericColumnValue with null"},
 		// not actually valid (stringProto inside int list), but demonstrates pass-through.
 		{
 			GenericColumnValue{
@@ -134,19 +150,72 @@
 			},
 			listProto(intProto(5), nullProto(), stringProto("bcd")),
 			listType(tInt),
+			"pass-through",
 		},
 		// placeholder
-		{CommitTimestamp, stringProto(commitTimestampPlaceholderString), tTime},
+		{CommitTimestamp, stringProto(commitTimestampPlaceholderString), tTime, "CommitTimestampPlaceholder"},
+		// CUSTOM STRING / CUSTOM STRING ARRAY
+		{CustomString("abc"), stringProto("abc"), tString, "CustomString"},
+		{CustomNullString{"abc", true}, stringProto("abc"), tString, "CustomNullString with value"},
+		{CustomNullString{"abc", false}, nullProto(), tString, "CustomNullString with null"},
+		{[]CustomString(nil), nullProto(), listType(tString), "null []CustomString"},
+		{[]CustomString{"abc", "bcd"}, listProto(stringProto("abc"), stringProto("bcd")), listType(tString), "[]CustomString"},
+		{[]CustomNullString(nil), nullProto(), listType(tString), "null []NullCustomString"},
+		{[]CustomNullString{{"abcd", true}, {"xyz", false}}, listProto(stringProto("abcd"), nullProto()), listType(tString), "[]NullCustomString"},
+		// CUSTOM BYTES / CUSTOM BYTES ARRAY
+		{CustomBytes("foo"), bytesProto([]byte("foo")), tBytes, "CustomBytes with value"},
+		{CustomBytes(nil), nullProto(), tBytes, "null CustomBytes"},
+		{[]CustomBytes{nil, CustomBytes("ab")}, listProto(nullProto(), bytesProto([]byte("ab"))), listType(tBytes), "[]CustomBytes"},
+		{[]CustomBytes(nil), nullProto(), listType(tBytes), "null []CustomBytes"},
+		// CUSTOM INT64 / CUSTOM INT64 ARRAY
+		{CustomInt64(81), intProto(81), tInt, "CustomInt64"},
+		{[]CustomInt64(nil), nullProto(), listType(tInt), "null []CustomInt64"},
+		{[]CustomInt64{33, 129}, listProto(intProto(33), intProto(129)), listType(tInt), "[]CustomInt64"},
+		{CustomNullInt64{11, true}, intProto(11), tInt, "CustomNullInt64 with value"},
+		{CustomNullInt64{11, false}, nullProto(), tInt, "CustomNullInt64 with null"},
+		{[]CustomNullInt64(nil), nullProto(), listType(tInt), "null []CustomNullInt64"},
+		{[]CustomNullInt64{{35, true}, {131, false}}, listProto(intProto(35), nullProto()), listType(tInt), "[]CustomNullInt64"},
+		// CUSTOM BOOL / CUSTOM BOOL ARRAY
+		{CustomBool(true), boolProto(true), tBool, "CustomBool"},
+		{CustomNullBool{true, true}, boolProto(true), tBool, "CustomNullBool with value"},
+		{CustomNullBool{true, false}, nullProto(), tBool, "CustomNullBool with null"},
+		{[]CustomBool{true, false}, listProto(boolProto(true), boolProto(false)), listType(tBool), "[]CustomBool"},
+		{[]CustomNullBool{{true, true}, {true, false}}, listProto(boolProto(true), nullProto()), listType(tBool), "[]CustomNullBool"},
+		// FLOAT64 / FLOAT64 ARRAY
+		{CustomFloat64(3.14), floatProto(3.14), tFloat, "CustomFloat64"},
+		{CustomNullFloat64{3.1415, true}, floatProto(3.1415), tFloat, "CustomNullFloat64 with value"},
+		{CustomNullFloat64{math.Inf(1), true}, floatProto(math.Inf(1)), tFloat, "CustomNullFloat64 with infinity"},
+		{CustomNullFloat64{3.14159, false}, nullProto(), tFloat, "CustomNullFloat64 with null"},
+		{[]CustomFloat64(nil), nullProto(), listType(tFloat), "null []CustomFloat64"},
+		{[]CustomFloat64{3.141, 0.618, CustomFloat64(math.Inf(-1))}, listProto(floatProto(3.141), floatProto(0.618), floatProto(math.Inf(-1))), listType(tFloat), "[]CustomFloat64"},
+		{[]CustomNullFloat64(nil), nullProto(), listType(tFloat), "null []CustomNullFloat64"},
+		{[]CustomNullFloat64{{3.141, true}, {0.618, false}}, listProto(floatProto(3.141), nullProto()), listType(tFloat), "[]CustomNullFloat64"},
+		// CUSTOM TIMESTAMP / CUSTOM TIMESTAMP ARRAY
+		{CustomTime(t1), timeProto(t1), tTime, "CustomTime"},
+		{CustomNullTime{t1, true}, timeProto(t1), tTime, "CustomNullTime with value"},
+		{CustomNullTime{t1, false}, nullProto(), tTime, "CustomNullTime with null"},
+		{[]CustomTime(nil), nullProto(), listType(tTime), "null []CustomTime"},
+		{[]CustomTime{CustomTime(t1), CustomTime(t2), CustomTime(t3), CustomTime(t4)}, listProto(timeProto(t1), timeProto(t2), timeProto(t3), timeProto(t4)), listType(tTime), "[]CustomTime"},
+		{[]CustomNullTime(nil), nullProto(), listType(tTime), "null []CustomNullTime"},
+		{[]CustomNullTime{{t1, true}, {t1, false}}, listProto(timeProto(t1), nullProto()), listType(tTime), "[]CustomNullTime"},
+		// CUSTOM DATE / CUSTOM DATE ARRAY
+		{CustomDate(d1), dateProto(d1), tDate, "CustomDate"},
+		{CustomNullDate{d1, true}, dateProto(d1), tDate, "CustomNullDate with value"},
+		{CustomNullDate{civil.Date{}, false}, nullProto(), tDate, "CustomNullDate with null"},
+		{[]CustomDate(nil), nullProto(), listType(tDate), "null []CustomDate"},
+		{[]CustomDate{CustomDate(d1), CustomDate(d2)}, listProto(dateProto(d1), dateProto(d2)), listType(tDate), "[]CustomDate"},
+		{[]CustomNullDate(nil), nullProto(), listType(tDate), "null []CustomNullDate"},
+		{[]CustomNullDate{{d1, true}, {civil.Date{}, false}}, listProto(dateProto(d1), nullProto()), listType(tDate), "[]NullDate"},
 	} {
 		got, gotType, err := encodeValue(test.in)
 		if err != nil {
-			t.Fatalf("#%d: got error during encoding: %v, want nil", i, err)
+			t.Fatalf("#%d (%s): got error during encoding: %v, want nil", i, test.name, err)
 		}
 		if !testEqual(got, test.want) {
-			t.Errorf("#%d: got encode result: %v, want %v", i, got, test.want)
+			t.Errorf("#%d (%s): got encode result: %v, want %v", i, test.name, got, test.want)
 		}
 		if !testEqual(gotType, test.wantType) {
-			t.Errorf("#%d: got encode type: %v, want %v", i, gotType, test.wantType)
+			t.Errorf("#%d (%s): got encode type: %v, want %v", i, test.name, gotType, test.wantType)
 		}
 	}
 }
@@ -519,6 +588,21 @@
 }
 
 func TestEncodeStructValueBasicFields(t *testing.T) {
+	type CustomString string
+	type CustomBytes []byte
+	type CustomInt64 int64
+	type CustomBool bool
+	type CustomFloat64 float64
+	type CustomTime time.Time
+	type CustomDate civil.Date
+
+	type CustomNullString NullString
+	type CustomNullInt64 NullInt64
+	type CustomNullBool NullBool
+	type CustomNullFloat64 NullFloat64
+	type CustomNullTime NullTime
+	type CustomNullDate NullDate
+
 	StructTypeProto := structType(
 		mkField("Stringf", stringType()),
 		mkField("Intf", intType()),
@@ -551,6 +635,27 @@
 			StructTypeProto,
 		},
 		{
+			"Basic custom types.",
+			struct {
+				Stringf CustomString
+				Intf    CustomInt64
+				Boolf   CustomBool
+				Floatf  CustomFloat64
+				Bytef   CustomBytes
+				Timef   CustomTime
+				Datef   CustomDate
+			}{"abc", 300, false, 3.45, []byte("foo"), CustomTime(t1), CustomDate(d1)},
+			listProto(
+				stringProto("abc"),
+				intProto(300),
+				boolProto(false),
+				floatProto(3.45),
+				bytesProto([]byte("foo")),
+				timeProto(t1),
+				dateProto(d1)),
+			StructTypeProto,
+		},
+		{
 			"Basic types null values.",
 			struct {
 				Stringf NullString
@@ -579,12 +684,56 @@
 				nullProto()),
 			StructTypeProto,
 		},
+		{
+			"Basic custom types null values.",
+			struct {
+				Stringf CustomNullString
+				Intf    CustomNullInt64
+				Boolf   CustomNullBool
+				Floatf  CustomNullFloat64
+				Bytef   CustomBytes
+				Timef   CustomNullTime
+				Datef   CustomNullDate
+			}{
+				CustomNullString{"abc", false},
+				CustomNullInt64{4, false},
+				CustomNullBool{false, false},
+				CustomNullFloat64{5.6, false},
+				nil,
+				CustomNullTime{t1, false},
+				CustomNullDate{d1, false},
+			},
+			listProto(
+				nullProto(),
+				nullProto(),
+				nullProto(),
+				nullProto(),
+				nullProto(),
+				nullProto(),
+				nullProto()),
+			StructTypeProto,
+		},
 	} {
 		encodeStructValue(test, t)
 	}
 }
 
 func TestEncodeStructValueArrayFields(t *testing.T) {
+	type CustomString string
+	type CustomBytes []byte
+	type CustomInt64 int64
+	type CustomBool bool
+	type CustomFloat64 float64
+	type CustomTime time.Time
+	type CustomDate civil.Date
+
+	type CustomNullString NullString
+	type CustomNullInt64 NullInt64
+	type CustomNullBool NullBool
+	type CustomNullFloat64 NullFloat64
+	type CustomNullTime NullTime
+	type CustomNullDate NullDate
+
 	StructTypeProto := structType(
 		mkField("Stringf", listType(stringType())),
 		mkField("Intf", listType(intType())),
@@ -629,6 +778,38 @@
 			StructTypeProto,
 		},
 		{
+			"Arrays of basic custom types with non-nullable elements",
+			struct {
+				Stringf []CustomString
+				Intf    []CustomInt64
+				Int64f  []CustomInt64
+				Boolf   []CustomBool
+				Floatf  []CustomFloat64
+				Bytef   []CustomBytes
+				Timef   []CustomTime
+				Datef   []CustomDate
+			}{
+				[]CustomString{"abc", "def"},
+				[]CustomInt64{4, 67},
+				[]CustomInt64{5, 68},
+				[]CustomBool{false, true},
+				[]CustomFloat64{3.45, 0.93},
+				[]CustomBytes{[]byte("foo"), nil},
+				[]CustomTime{CustomTime(t1), CustomTime(t2)},
+				[]CustomDate{CustomDate(d1), CustomDate(d2)},
+			},
+			listProto(
+				listProto(stringProto("abc"), stringProto("def")),
+				listProto(intProto(4), intProto(67)),
+				listProto(intProto(5), intProto(68)),
+				listProto(boolProto(false), boolProto(true)),
+				listProto(floatProto(3.45), floatProto(0.93)),
+				listProto(bytesProto([]byte("foo")), nullProto()),
+				listProto(timeProto(t1), timeProto(t2)),
+				listProto(dateProto(d1), dateProto(d2))),
+			StructTypeProto,
+		},
+		{
 			"Arrays of basic types with nullable elements.",
 			struct {
 				Stringf []NullString
@@ -661,6 +842,38 @@
 			StructTypeProto,
 		},
 		{
+			"Arrays of basic custom types with nullable elements.",
+			struct {
+				Stringf []CustomNullString
+				Intf    []CustomNullInt64
+				Int64f  []CustomNullInt64
+				Boolf   []CustomNullBool
+				Floatf  []CustomNullFloat64
+				Bytef   []CustomBytes
+				Timef   []CustomNullTime
+				Datef   []CustomNullDate
+			}{
+				[]CustomNullString{{"abc", false}, {"def", true}},
+				[]CustomNullInt64{{4, false}, {67, true}},
+				[]CustomNullInt64{{5, false}, {68, true}},
+				[]CustomNullBool{{true, false}, {false, true}},
+				[]CustomNullFloat64{{3.45, false}, {0.93, true}},
+				[]CustomBytes{[]byte("foo"), nil},
+				[]CustomNullTime{{t1, false}, {t2, true}},
+				[]CustomNullDate{{d1, false}, {d2, true}},
+			},
+			listProto(
+				listProto(nullProto(), stringProto("def")),
+				listProto(nullProto(), intProto(67)),
+				listProto(nullProto(), intProto(68)),
+				listProto(nullProto(), boolProto(false)),
+				listProto(nullProto(), floatProto(0.93)),
+				listProto(bytesProto([]byte("foo")), nullProto()),
+				listProto(nullProto(), timeProto(t2)),
+				listProto(nullProto(), dateProto(d2))),
+			StructTypeProto,
+		},
+		{
 			"Null arrays of basic types.",
 			struct {
 				Stringf []NullString
@@ -692,6 +905,38 @@
 				nullProto()),
 			StructTypeProto,
 		},
+		{
+			"Null arrays of basic custom types.",
+			struct {
+				Stringf []CustomNullString
+				Intf    []CustomNullInt64
+				Int64f  []CustomNullInt64
+				Boolf   []CustomNullBool
+				Floatf  []CustomNullFloat64
+				Bytef   []CustomBytes
+				Timef   []CustomNullTime
+				Datef   []CustomNullDate
+			}{
+				nil,
+				nil,
+				nil,
+				nil,
+				nil,
+				nil,
+				nil,
+				nil,
+			},
+			listProto(
+				nullProto(),
+				nullProto(),
+				nullProto(),
+				nullProto(),
+				nullProto(),
+				nullProto(),
+				nullProto(),
+				nullProto()),
+			StructTypeProto,
+		},
 	} {
 		encodeStructValue(test, t)
 	}