go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/resultdb/internal/spanutil/buffer.go (about)

     1  // Copyright 2020 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package spanutil
    16  
    17  import (
    18  	"fmt"
    19  	"reflect"
    20  
    21  	"cloud.google.com/go/spanner"
    22  	"github.com/golang/protobuf/proto"
    23  	"google.golang.org/protobuf/types/known/timestamppb"
    24  
    25  	"go.chromium.org/luci/common/errors"
    26  	"go.chromium.org/luci/resultdb/pbutil"
    27  	pb "go.chromium.org/luci/resultdb/proto/v1"
    28  )
    29  
    30  // Value can be converted to a Spanner value.
    31  // Typically if type T implements Value, then *T implements Ptr.
    32  type Value interface {
    33  	// ToSpanner returns a value of a type supported by Spanner client.
    34  	ToSpanner() any
    35  }
    36  
    37  // Ptr can be used a destination of reading a Spanner cell.
    38  // Typically if type *T implements Ptr, then T implements Value.
    39  type Ptr interface {
    40  	// SpannerPtr returns to a pointer of a type supported by Spanner client.
    41  	// SpannerPtr must use one of typed buffers in b.
    42  	SpannerPtr(b *Buffer) any
    43  	// FromSpanner replaces Ptr value with the value in the typed buffer returned
    44  	// by SpannerPtr.
    45  	FromSpanner(b *Buffer) error
    46  }
    47  
    48  // Buffer can convert a value from a Spanner type to a Go type.
    49  // Supported types:
    50  //   - Value and Ptr
    51  //   - string
    52  //   - timestamppb.Timestamp
    53  //   - pb.BigQueryExport
    54  //   - pb.ExonerationReason
    55  //   - pb.InvocationState
    56  //   - pb.TestStatus
    57  //   - pb.Variant
    58  //   - pb.StringPair
    59  //   - proto.Message
    60  //
    61  // TODO(nodir): move to buffer.go
    62  type Buffer struct {
    63  	NullString  spanner.NullString
    64  	NullTime    spanner.NullTime
    65  	Int64       int64
    66  	StringSlice []string
    67  	ByteSlice   []byte
    68  	ByteSlice2  [][]byte
    69  }
    70  
    71  // FromSpanner is a shortcut for (&Buffer{}).FromSpanner.
    72  // Appropriate when FromSpanner is called only once, whereas Buffer is reusable
    73  // throughout function.
    74  func FromSpanner(row *spanner.Row, ptrs ...any) error {
    75  	return (&Buffer{}).FromSpanner(row, ptrs...)
    76  }
    77  
    78  // FromSpanner reads values from row to ptrs, converting types from Spanner
    79  // to Go along the way.
    80  func (b *Buffer) FromSpanner(row *spanner.Row, ptrs ...any) error {
    81  	if len(ptrs) != row.Size() {
    82  		panic("len(ptrs) != row.Size()")
    83  	}
    84  
    85  	for i, goPtr := range ptrs {
    86  		if err := b.fromSpanner(row, i, goPtr); err != nil {
    87  			return err
    88  		}
    89  	}
    90  	return nil
    91  }
    92  
    93  func (b *Buffer) fromSpanner(row *spanner.Row, col int, goPtr any) error {
    94  	b.StringSlice = b.StringSlice[:0]
    95  	b.ByteSlice = b.ByteSlice[:0]
    96  	b.ByteSlice2 = b.ByteSlice2[:0]
    97  
    98  	var spanPtr any
    99  	switch goPtr := goPtr.(type) {
   100  	case Ptr:
   101  		spanPtr = goPtr.SpannerPtr(b)
   102  	case *string:
   103  		spanPtr = &b.NullString
   104  	case **timestamppb.Timestamp:
   105  		spanPtr = &b.NullTime
   106  	case *pb.ExonerationReason:
   107  		spanPtr = &b.Int64
   108  	case *pb.TestStatus:
   109  		spanPtr = &b.Int64
   110  	case *pb.SkipReason:
   111  		spanPtr = &b.Int64
   112  	case *pb.Invocation_State:
   113  		spanPtr = &b.Int64
   114  	case **pb.Variant:
   115  		spanPtr = &b.StringSlice
   116  	case *[]*pb.StringPair:
   117  		spanPtr = &b.StringSlice
   118  	case *[]*pb.BigQueryExport:
   119  		spanPtr = &b.ByteSlice2
   120  	case proto.Message:
   121  		spanPtr = &b.ByteSlice
   122  	default:
   123  		spanPtr = goPtr
   124  	}
   125  
   126  	if err := row.Column(col, spanPtr); err != nil {
   127  		return errors.Annotate(err, "failed to read column %q", row.ColumnName(col)).Err()
   128  	}
   129  
   130  	if spanPtr == goPtr {
   131  		return nil
   132  	}
   133  
   134  	var err error
   135  	switch goPtr := goPtr.(type) {
   136  	case Ptr:
   137  		if err := goPtr.FromSpanner(b); err != nil {
   138  			return err
   139  		}
   140  
   141  	case *string:
   142  		*goPtr = ""
   143  		if b.NullString.Valid {
   144  			*goPtr = b.NullString.StringVal
   145  		}
   146  
   147  	case **timestamppb.Timestamp:
   148  		*goPtr = nil
   149  		if b.NullTime.Valid {
   150  			*goPtr = pbutil.MustTimestampProto(b.NullTime.Time)
   151  		}
   152  
   153  	case *pb.ExonerationReason:
   154  		*goPtr = pb.ExonerationReason(b.Int64)
   155  
   156  	case *pb.Invocation_State:
   157  		*goPtr = pb.Invocation_State(b.Int64)
   158  
   159  	case *pb.TestStatus:
   160  		*goPtr = pb.TestStatus(b.Int64)
   161  
   162  	case *pb.SkipReason:
   163  		*goPtr = pb.SkipReason(b.Int64)
   164  
   165  	case **pb.Variant:
   166  		if *goPtr, err = pbutil.VariantFromStrings(b.StringSlice); err != nil {
   167  			// If it was written to Spanner, it should have been validated.
   168  			panic(err)
   169  		}
   170  
   171  	case *[]*pb.StringPair:
   172  		*goPtr = make([]*pb.StringPair, len(b.StringSlice))
   173  		for i, p := range b.StringSlice {
   174  			if (*goPtr)[i], err = pbutil.StringPairFromString(p); err != nil {
   175  				// If it was written to Spanner, it should have been validated.
   176  				panic(err)
   177  			}
   178  		}
   179  
   180  	case *[]*pb.BigQueryExport:
   181  		*goPtr = make([]*pb.BigQueryExport, len(b.ByteSlice2))
   182  		for i, p := range b.ByteSlice2 {
   183  			(*goPtr)[i] = &pb.BigQueryExport{}
   184  			if err := proto.Unmarshal(p, (*goPtr)[i]); err != nil {
   185  				// If it was written to Spanner, it should have been validated.
   186  				panic(err)
   187  			}
   188  		}
   189  
   190  	case proto.Message:
   191  		if reflect.ValueOf(goPtr).IsNil() {
   192  			return errors.Reason("nil pointer encountered").Err()
   193  		}
   194  		if err := proto.Unmarshal(b.ByteSlice, goPtr); err != nil {
   195  			// If it was written to Spanner, it should have been validated.
   196  			panic(err)
   197  		}
   198  
   199  	default:
   200  		panic(fmt.Sprintf("impossible %q", goPtr))
   201  	}
   202  	return nil
   203  }
   204  
   205  // ToSpanner converts values from Go types to Spanner types. In addition to
   206  // supported types in FromSpanner, also supports []any and
   207  // map[string]any.
   208  func ToSpanner(v any) any {
   209  	switch v := v.(type) {
   210  	case Value:
   211  		return v.ToSpanner()
   212  
   213  	case *timestamppb.Timestamp:
   214  		if v == nil {
   215  			return spanner.NullTime{}
   216  		}
   217  		ret := spanner.NullTime{Valid: true}
   218  		ret.Time = v.AsTime()
   219  		// ptypes.Timestamp always returns a timestamp, even
   220  		// if the returned err is non-nil, see its documentation.
   221  		// The error is returned only if the timestamp violates its
   222  		// own invariants, which will be caught on the attempt to
   223  		// insert this value into Spanner.
   224  		// This is consistent with the behavior of spanner.Insert() and
   225  		// other mutating functions that ignore invalid time.Time
   226  		// until it is time to apply the mutation.
   227  		// Not returning an error here significantly simplifies usage
   228  		// of this function and functions based on this one.
   229  		return ret
   230  
   231  	case pb.ExonerationReason:
   232  		return int64(v)
   233  
   234  	case pb.Invocation_State:
   235  		return int64(v)
   236  
   237  	case pb.TestStatus:
   238  		return int64(v)
   239  
   240  	case pb.SkipReason:
   241  		return int64(v)
   242  
   243  	case *pb.Variant:
   244  		return pbutil.VariantToStrings(v)
   245  
   246  	case []*pb.StringPair:
   247  		return pbutil.StringPairsToStrings(v...)
   248  
   249  	case []*pb.BigQueryExport:
   250  		var err error
   251  		bqExportsBytes := make([][]byte, len(v))
   252  		for i, bqExport := range v {
   253  			if bqExportsBytes[i], err = proto.Marshal(bqExport); err != nil {
   254  				panic(err)
   255  			}
   256  		}
   257  		return bqExportsBytes
   258  
   259  	case proto.Message:
   260  		if isMessageNil(v) {
   261  			// Do not store empty messages.
   262  			return []byte(nil)
   263  		}
   264  
   265  		ret, err := proto.Marshal(v)
   266  		if err != nil {
   267  			panic(err)
   268  		}
   269  		return ret
   270  
   271  	case []any:
   272  		ret := make([]any, len(v))
   273  		for i, el := range v {
   274  			ret[i] = ToSpanner(el)
   275  		}
   276  		return ret
   277  
   278  	case map[string]any:
   279  		ret := make(map[string]any, len(v))
   280  		for key, el := range v {
   281  			ret[key] = ToSpanner(el)
   282  		}
   283  		return ret
   284  
   285  	default:
   286  		return v
   287  	}
   288  }
   289  
   290  // ToSpannerSlice converts a slice of Go values to a slice of Spanner values.
   291  // See also ToSpanner.
   292  func ToSpannerSlice(values ...any) []any {
   293  	return ToSpanner(values).([]any)
   294  }
   295  
   296  // ToSpannerMap converts a map of Go values to a map of Spanner values.
   297  // See also ToSpanner.
   298  func ToSpannerMap(values map[string]any) map[string]any {
   299  	return ToSpanner(values).(map[string]any)
   300  }
   301  
   302  // UpdateMap is a shortcut for spanner.UpdateMap with ToSpannerMap applied to
   303  // in.
   304  func UpdateMap(table string, in map[string]any) *spanner.Mutation {
   305  	return spanner.UpdateMap(table, ToSpannerMap(in))
   306  }
   307  
   308  // InsertMap is a shortcut for spanner.InsertMap with ToSpannerMap applied to
   309  // in.
   310  func InsertMap(table string, in map[string]any) *spanner.Mutation {
   311  	return spanner.InsertMap(table, ToSpannerMap(in))
   312  }
   313  
   314  // InsertOrUpdateMap is a shortcut for spanner.InsertOrUpdateMap with ToSpannerMap applied to
   315  // in.
   316  func InsertOrUpdateMap(table string, in map[string]any) *spanner.Mutation {
   317  	return spanner.InsertOrUpdateMap(table, ToSpannerMap(in))
   318  }