blob: 14f1ef5f6b4e2dda1130bde6135aec1d5687c30a [file] [log] [blame]
// Copyright 2020 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package spanutil
import (
"fmt"
"reflect"
"cloud.google.com/go/spanner"
"github.com/golang/protobuf/proto"
"google.golang.org/protobuf/types/known/timestamppb"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/resultdb/pbutil"
pb "go.chromium.org/luci/resultdb/proto/v1"
)
// Value can be converted to a Spanner value.
// Typically if type T implements Value, then *T implements Ptr.
type Value interface {
// ToSpanner returns a value of a type supported by Spanner client.
ToSpanner() interface{}
}
// Ptr can be used a destination of reading a Spanner cell.
// Typically if type *T implements Ptr, then T implements Value.
type Ptr interface {
// SpannerPtr returns to a pointer of a type supported by Spanner client.
// SpannerPtr must use one of typed buffers in b.
SpannerPtr(b *Buffer) interface{}
// FromSpanner replaces Ptr value with the value in the typed buffer returned
// by SpannerPtr.
FromSpanner(b *Buffer) error
}
// Buffer can convert a value from a Spanner type to a Go type.
// Supported types:
// - Value and Ptr
// - string
// - timestamppb.Timestamp
// - pb.BigQueryExport
// - pb.ExonerationReason
// - pb.InvocationState
// - pb.TestStatus
// - pb.Variant
// - pb.StringPair
// - proto.Message
// TODO(nodir): move to buffer.go
type Buffer struct {
NullString spanner.NullString
NullTime spanner.NullTime
Int64 int64
StringSlice []string
ByteSlice []byte
ByteSlice2 [][]byte
}
// FromSpanner is a shortcut for (&Buffer{}).FromSpanner.
// Appropriate when FromSpanner is called only once, whereas Buffer is reusable
// throughout function.
func FromSpanner(row *spanner.Row, ptrs ...interface{}) error {
return (&Buffer{}).FromSpanner(row, ptrs...)
}
// FromSpanner reads values from row to ptrs, converting types from Spanner
// to Go along the way.
func (b *Buffer) FromSpanner(row *spanner.Row, ptrs ...interface{}) error {
if len(ptrs) != row.Size() {
panic("len(ptrs) != row.Size()")
}
for i, goPtr := range ptrs {
if err := b.fromSpanner(row, i, goPtr); err != nil {
return err
}
}
return nil
}
func (b *Buffer) fromSpanner(row *spanner.Row, col int, goPtr interface{}) error {
b.StringSlice = b.StringSlice[:0]
b.ByteSlice = b.ByteSlice[:0]
b.ByteSlice2 = b.ByteSlice2[:0]
var spanPtr interface{}
switch goPtr := goPtr.(type) {
case Ptr:
spanPtr = goPtr.SpannerPtr(b)
case *string:
spanPtr = &b.NullString
case **timestamppb.Timestamp:
spanPtr = &b.NullTime
case *pb.ExonerationReason:
spanPtr = &b.Int64
case *pb.TestStatus:
spanPtr = &b.Int64
case *pb.Invocation_State:
spanPtr = &b.Int64
case **pb.Variant:
spanPtr = &b.StringSlice
case *[]*pb.StringPair:
spanPtr = &b.StringSlice
case *[]*pb.BigQueryExport:
spanPtr = &b.ByteSlice2
case proto.Message:
spanPtr = &b.ByteSlice
default:
spanPtr = goPtr
}
if err := row.Column(col, spanPtr); err != nil {
return errors.Annotate(err, "failed to read column %q", row.ColumnName(col)).Err()
}
if spanPtr == goPtr {
return nil
}
var err error
switch goPtr := goPtr.(type) {
case Ptr:
if err := goPtr.FromSpanner(b); err != nil {
return err
}
case *string:
*goPtr = ""
if b.NullString.Valid {
*goPtr = b.NullString.StringVal
}
case **timestamppb.Timestamp:
*goPtr = nil
if b.NullTime.Valid {
*goPtr = pbutil.MustTimestampProto(b.NullTime.Time)
}
case *pb.ExonerationReason:
*goPtr = pb.ExonerationReason(b.Int64)
case *pb.Invocation_State:
*goPtr = pb.Invocation_State(b.Int64)
case *pb.TestStatus:
*goPtr = pb.TestStatus(b.Int64)
case **pb.Variant:
if *goPtr, err = pbutil.VariantFromStrings(b.StringSlice); err != nil {
// If it was written to Spanner, it should have been validated.
panic(err)
}
case *[]*pb.StringPair:
*goPtr = make([]*pb.StringPair, len(b.StringSlice))
for i, p := range b.StringSlice {
if (*goPtr)[i], err = pbutil.StringPairFromString(p); err != nil {
// If it was written to Spanner, it should have been validated.
panic(err)
}
}
case *[]*pb.BigQueryExport:
*goPtr = make([]*pb.BigQueryExport, len(b.ByteSlice2))
for i, p := range b.ByteSlice2 {
(*goPtr)[i] = &pb.BigQueryExport{}
if err := proto.Unmarshal(p, (*goPtr)[i]); err != nil {
// If it was written to Spanner, it should have been validated.
panic(err)
}
}
case proto.Message:
if reflect.ValueOf(goPtr).IsNil() {
return errors.Reason("nil pointer encountered").Err()
}
if err := proto.Unmarshal(b.ByteSlice, goPtr); err != nil {
// If it was written to Spanner, it should have been validated.
panic(err)
}
default:
panic(fmt.Sprintf("impossible %q", goPtr))
}
return nil
}
// ToSpanner converts values from Go types to Spanner types. In addition to
// supported types in FromSpanner, also supports []interface{} and
// map[string]interface{}.
func ToSpanner(v interface{}) interface{} {
switch v := v.(type) {
case Value:
return v.ToSpanner()
case *timestamppb.Timestamp:
if v == nil {
return spanner.NullTime{}
}
ret := spanner.NullTime{Valid: true}
ret.Time = v.AsTime()
// ptypes.Timestamp always returns a timestamp, even
// if the returned err is non-nil, see its documentation.
// The error is returned only if the timestamp violates its
// own invariants, which will be caught on the attempt to
// insert this value into Spanner.
// This is consistent with the behavior of spanner.Insert() and
// other mutating functions that ignore invalid time.Time
// until it is time to apply the mutation.
// Not returning an error here significantly simplifies usage
// of this function and functions based on this one.
return ret
case pb.ExonerationReason:
return int64(v)
case pb.Invocation_State:
return int64(v)
case pb.TestStatus:
return int64(v)
case *pb.Variant:
return pbutil.VariantToStrings(v)
case []*pb.StringPair:
return pbutil.StringPairsToStrings(v...)
case []*pb.BigQueryExport:
var err error
bqExportsBytes := make([][]byte, len(v))
for i, bqExport := range v {
if bqExportsBytes[i], err = proto.Marshal(bqExport); err != nil {
panic(err)
}
}
return bqExportsBytes
case proto.Message:
if isMessageNil(v) {
// Do not store empty messages.
return []byte(nil)
}
ret, err := proto.Marshal(v)
if err != nil {
panic(err)
}
return ret
case []interface{}:
ret := make([]interface{}, len(v))
for i, el := range v {
ret[i] = ToSpanner(el)
}
return ret
case map[string]interface{}:
ret := make(map[string]interface{}, len(v))
for key, el := range v {
ret[key] = ToSpanner(el)
}
return ret
default:
return v
}
}
// ToSpannerSlice converts a slice of Go values to a slice of Spanner values.
// See also ToSpanner.
func ToSpannerSlice(values ...interface{}) []interface{} {
return ToSpanner(values).([]interface{})
}
// ToSpannerMap converts a map of Go values to a map of Spanner values.
// See also ToSpanner.
func ToSpannerMap(values map[string]interface{}) map[string]interface{} {
return ToSpanner(values).(map[string]interface{})
}
// UpdateMap is a shortcut for spanner.UpdateMap with ToSpannerMap applied to
// in.
func UpdateMap(table string, in map[string]interface{}) *spanner.Mutation {
return spanner.UpdateMap(table, ToSpannerMap(in))
}
// InsertMap is a shortcut for spanner.InsertMap with ToSpannerMap applied to
// in.
func InsertMap(table string, in map[string]interface{}) *spanner.Mutation {
return spanner.InsertMap(table, ToSpannerMap(in))
}
// InsertOrUpdateMap is a shortcut for spanner.InsertOrUpdateMap with ToSpannerMap applied to
// in.
func InsertOrUpdateMap(table string, in map[string]interface{}) *spanner.Mutation {
return spanner.InsertOrUpdateMap(table, ToSpannerMap(in))
}