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

     1  // Copyright 2022 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 span
    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  
    27  	"go.chromium.org/luci/analysis/pbutil"
    28  	pb "go.chromium.org/luci/analysis/proto/v1"
    29  )
    30  
    31  // Value can be converted to a Spanner value.
    32  // Typically if type T implements Value, then *T implements Ptr.
    33  type Value interface {
    34  	// ToSpanner returns a value of a type supported by Spanner client.
    35  	ToSpanner() any
    36  }
    37  
    38  // Ptr can be used a destination of reading a Spanner cell.
    39  // Typically if type *T implements Ptr, then T implements Value.
    40  type Ptr interface {
    41  	// SpannerPtr returns to a pointer of a type supported by Spanner client.
    42  	// SpannerPtr must use one of typed buffers in b.
    43  	SpannerPtr(b *Buffer) any
    44  	// FromSpanner replaces Ptr value with the value in the typed buffer returned
    45  	// by SpannerPtr.
    46  	FromSpanner(b *Buffer) error
    47  }
    48  
    49  // Buffer can convert a value from a Spanner type to a Go type.
    50  // Supported types:
    51  //   - Value and Ptr
    52  //   - string
    53  //   - timestamppb.Timestamp
    54  //   - atvpb.Status
    55  //   - pb.Variant
    56  //   - pb.StringPair
    57  //   - proto.Message
    58  //   - internal.VerdictStatus
    59  type Buffer struct {
    60  	NullString  spanner.NullString
    61  	NullTime    spanner.NullTime
    62  	Int64       int64
    63  	StringSlice []string
    64  	ByteSlice   []byte
    65  	Int64Slice  []int64
    66  }
    67  
    68  // FromSpanner is a shortcut for (&Buffer{}).FromSpanner.
    69  // Appropriate when FromSpanner is called only once, whereas Buffer is reusable
    70  // throughout function.
    71  func FromSpanner(row *spanner.Row, ptrs ...any) error {
    72  	return (&Buffer{}).FromSpanner(row, ptrs...)
    73  }
    74  
    75  // FromSpanner reads values from row to ptrs, converting types from Spanner
    76  // to Go along the way.
    77  func (b *Buffer) FromSpanner(row *spanner.Row, ptrs ...any) error {
    78  	if len(ptrs) != row.Size() {
    79  		panic("len(ptrs) != row.Size()")
    80  	}
    81  
    82  	for i, goPtr := range ptrs {
    83  		if err := b.fromSpanner(row, i, goPtr); err != nil {
    84  			return err
    85  		}
    86  	}
    87  	return nil
    88  }
    89  
    90  func (b *Buffer) fromSpanner(row *spanner.Row, col int, goPtr any) error {
    91  	b.StringSlice = b.StringSlice[:0]
    92  	b.ByteSlice = b.ByteSlice[:0]
    93  	b.Int64Slice = b.Int64Slice[:0]
    94  
    95  	var spanPtr any
    96  	switch goPtr := goPtr.(type) {
    97  	case Ptr:
    98  		spanPtr = goPtr.SpannerPtr(b)
    99  	case *string:
   100  		spanPtr = &b.NullString
   101  	case **timestamppb.Timestamp:
   102  		spanPtr = &b.NullTime
   103  	case *pb.BuildStatus:
   104  		spanPtr = &b.Int64
   105  	case *[]pb.ExonerationReason:
   106  		spanPtr = &b.Int64Slice
   107  	case *pb.TestResultStatus:
   108  		spanPtr = &b.Int64
   109  	case *pb.ChangelistOwnerKind:
   110  		spanPtr = &b.Int64
   111  	case **pb.Variant:
   112  		spanPtr = &b.StringSlice
   113  	case *[]*pb.StringPair:
   114  		spanPtr = &b.StringSlice
   115  	case proto.Message:
   116  		spanPtr = &b.ByteSlice
   117  	default:
   118  		spanPtr = goPtr
   119  	}
   120  
   121  	if err := row.Column(col, spanPtr); err != nil {
   122  		return errors.Annotate(err, "failed to read column %q", row.ColumnName(col)).Err()
   123  	}
   124  
   125  	if spanPtr == goPtr {
   126  		return nil
   127  	}
   128  
   129  	var err error
   130  	switch goPtr := goPtr.(type) {
   131  	case Ptr:
   132  		if err := goPtr.FromSpanner(b); err != nil {
   133  			return err
   134  		}
   135  
   136  	case *string:
   137  		*goPtr = ""
   138  		if b.NullString.Valid {
   139  			*goPtr = b.NullString.StringVal
   140  		}
   141  
   142  	case **timestamppb.Timestamp:
   143  		*goPtr = nil
   144  		if b.NullTime.Valid {
   145  			*goPtr = pbutil.MustTimestampProto(b.NullTime.Time)
   146  		}
   147  
   148  	case *pb.BuildStatus:
   149  		*goPtr = pb.BuildStatus(b.Int64)
   150  
   151  	case *[]pb.ExonerationReason:
   152  		*goPtr = nil
   153  		// Be careful to preserve NULLs.
   154  		if b.Int64Slice != nil {
   155  			*goPtr = make([]pb.ExonerationReason, len(b.Int64Slice))
   156  			for i, p := range b.Int64Slice {
   157  				(*goPtr)[i] = pb.ExonerationReason(p)
   158  			}
   159  		}
   160  
   161  	case *pb.TestResultStatus:
   162  		*goPtr = pb.TestResultStatus(b.Int64)
   163  
   164  	case *pb.ChangelistOwnerKind:
   165  		*goPtr = pb.ChangelistOwnerKind(b.Int64)
   166  
   167  	case **pb.Variant:
   168  		if *goPtr, err = pbutil.VariantFromStrings(b.StringSlice); err != nil {
   169  			// If it was written to Spanner, it should have been validated.
   170  			panic(err)
   171  		}
   172  
   173  	case *[]*pb.StringPair:
   174  		*goPtr = make([]*pb.StringPair, len(b.StringSlice))
   175  		for i, p := range b.StringSlice {
   176  			if (*goPtr)[i], err = pbutil.StringPairFromString(p); err != nil {
   177  				// If it was written to Spanner, it should have been validated.
   178  				panic(err)
   179  			}
   180  		}
   181  
   182  	case proto.Message:
   183  		if reflect.ValueOf(goPtr).IsNil() {
   184  			return errors.Reason("nil pointer encountered").Err()
   185  		}
   186  		if err := proto.Unmarshal(b.ByteSlice, goPtr); err != nil {
   187  			// If it was written to Spanner, it should have been validated.
   188  			panic(err)
   189  		}
   190  
   191  	default:
   192  		panic(fmt.Sprintf("impossible %q", goPtr))
   193  	}
   194  	return nil
   195  }
   196  
   197  // ToSpanner converts values from Go types to Spanner types. In addition to
   198  // supported types in FromSpanner, also supports []any and
   199  // map[string]any.
   200  func ToSpanner(v any) any {
   201  	switch v := v.(type) {
   202  	case Value:
   203  		return v.ToSpanner()
   204  
   205  	case *timestamppb.Timestamp:
   206  		if v == nil {
   207  			return spanner.NullTime{}
   208  		}
   209  		ret := spanner.NullTime{Valid: true}
   210  		ret.Time = v.AsTime()
   211  		// ptypes.Timestamp always returns a timestamp, even
   212  		// if the returned err is non-nil, see its documentation.
   213  		// The error is returned only if the timestamp violates its
   214  		// own invariants, which will be caught on the attempt to
   215  		// insert this value into Spanner.
   216  		// This is consistent with the behavior of spanner.Insert() and
   217  		// other mutating functions that ignore invalid time.Time
   218  		// until it is time to apply the mutation.
   219  		// Not returning an error here significantly simplifies usage
   220  		// of this function and functions based on this one.
   221  		return ret
   222  
   223  	case pb.BuildStatus:
   224  		return int64(v)
   225  
   226  	case []pb.ExonerationReason:
   227  		var spanPtr []int64
   228  		// Be careful to preserve NULLs.
   229  		if v != nil {
   230  			spanPtr = make([]int64, len(v))
   231  			for i, s := range v {
   232  				spanPtr[i] = int64(s)
   233  			}
   234  		}
   235  		return spanPtr
   236  
   237  	case pb.TestResultStatus:
   238  		return int64(v)
   239  
   240  	case pb.ChangelistOwnerKind:
   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 proto.Message:
   250  		if reflect.ValueOf(v).IsNil() {
   251  			// Do not store empty messages.
   252  			return []byte(nil)
   253  		}
   254  
   255  		ret, err := proto.Marshal(v)
   256  		if err != nil {
   257  			panic(err)
   258  		}
   259  		return ret
   260  
   261  	case []any:
   262  		ret := make([]any, len(v))
   263  		for i, el := range v {
   264  			ret[i] = ToSpanner(el)
   265  		}
   266  		return ret
   267  
   268  	case map[string]any:
   269  		ret := make(map[string]any, len(v))
   270  		for key, el := range v {
   271  			ret[key] = ToSpanner(el)
   272  		}
   273  		return ret
   274  
   275  	default:
   276  		return v
   277  	}
   278  }
   279  
   280  // ToSpannerMap converts a map of Go values to a map of Spanner values.
   281  // See also ToSpanner.
   282  func ToSpannerMap(values map[string]any) map[string]any {
   283  	return ToSpanner(values).(map[string]any)
   284  }
   285  
   286  // UpdateMap is a shortcut for spanner.UpdateMap with ToSpannerMap applied to
   287  // in.
   288  func UpdateMap(table string, in map[string]any) *spanner.Mutation {
   289  	return spanner.UpdateMap(table, ToSpannerMap(in))
   290  }
   291  
   292  // InsertMap is a shortcut for spanner.InsertMap with ToSpannerMap applied to
   293  // in.
   294  func InsertMap(table string, in map[string]any) *spanner.Mutation {
   295  	return spanner.InsertMap(table, ToSpannerMap(in))
   296  }
   297  
   298  // InsertOrUpdateMap is a shortcut for spanner.InsertOrUpdateMap with ToSpannerMap applied to
   299  // in.
   300  func InsertOrUpdateMap(table string, in map[string]any) *spanner.Mutation {
   301  	return spanner.InsertOrUpdateMap(table, ToSpannerMap(in))
   302  }