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 }