go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/resultdb/internal/testmetadata/query.go (about) 1 // Copyright 2023 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 testmetadata implements methods to query from TestMetadata spanner table. 16 package testmetadata 17 18 import ( 19 "context" 20 "encoding/hex" 21 "text/template" 22 23 "google.golang.org/protobuf/proto" 24 25 "cloud.google.com/go/spanner" 26 "go.chromium.org/luci/common/errors" 27 "go.chromium.org/luci/resultdb/internal/pagination" 28 "go.chromium.org/luci/resultdb/internal/spanutil" 29 "go.chromium.org/luci/resultdb/pbutil" 30 pb "go.chromium.org/luci/resultdb/proto/v1" 31 ) 32 33 // Query a set of TestMetadataRow for a list of tests 34 // which have the same same subRealm and sourceRef in a LUCI project. 35 type Query struct { 36 Project string 37 SubRealms []string 38 Predicate *pb.TestMetadataPredicate 39 40 PageSize int 41 PageToken string 42 } 43 44 // Fetch distinct matching TestMetadataDetails from on the TestMetadata table. 45 // The returned TestMetadataDetails are ordered by testID, refHash ascendingly. 46 // If the test exists in multiples subRealms, lexicographically minimum subRealm is returned. 47 func (q *Query) Fetch(ctx context.Context) (tmr []*pb.TestMetadataDetail, nextPageToken string, err error) { 48 if q.PageSize <= 0 { 49 panic("can't use fetch with non-positive page size") 50 } 51 52 err = q.run(ctx, func(row *pb.TestMetadataDetail) error { 53 tmr = append(tmr, row) 54 return nil 55 }) 56 if err != nil { 57 return tmr, nextPageToken, err 58 } 59 if len(tmr) == q.PageSize { 60 lastRow := tmr[q.PageSize-1] 61 nextPageToken = pagination.Token(lastRow.TestId, lastRow.RefHash) 62 } 63 return tmr, nextPageToken, err 64 } 65 66 func parseQueryPageToken(pageToken string) (afterTestID, afterRefHash string, err error) { 67 tokens, err := pagination.ParseToken(pageToken) 68 if err != nil { 69 return "", "", err 70 } 71 if len(tokens) != 2 { 72 return "", "", pagination.InvalidToken(errors.Reason("expected 2 components, got %d", len(tokens)).Err()) 73 } 74 return tokens[0], tokens[1], nil 75 } 76 77 // Run the query and call f for each test metadata detail returned from the query. 78 func (q *Query) run(ctx context.Context, f func(tmd *pb.TestMetadataDetail) error) error { 79 st, err := spanutil.GenerateStatement(queryTmpl, map[string]any{ 80 "pagination": len(q.PageToken) > 0, 81 "hasLimit": q.PageSize > 0, 82 "filterTestID": len(q.Predicate.GetTestIds()) != 0, 83 }) 84 if err != nil { 85 return err 86 } 87 params := map[string]any{ 88 "project": q.Project, 89 "testIDs": q.Predicate.GetTestIds(), 90 "subRealms": q.SubRealms, 91 "limit": q.PageSize, 92 } 93 94 if q.PageToken != "" { 95 afterTestID, afterRefHash, err := parseQueryPageToken(q.PageToken) 96 if err != nil { 97 return err 98 } 99 params["afterTestID"] = afterTestID 100 params["afterRefHash"], err = hex.DecodeString(afterRefHash) 101 if err != nil { 102 return pagination.InvalidToken(err) 103 } 104 } 105 st.Params = spanutil.ToSpannerMap(params) 106 107 var b spanutil.Buffer 108 return spanutil.Query(ctx, st, func(row *spanner.Row) error { 109 tmd := &pb.TestMetadataDetail{} 110 var compressedTestMetadata spanutil.Compressed 111 var compressedSourceRef spanutil.Compressed 112 var refHash []byte 113 if err := b.FromSpanner(row, 114 &tmd.Project, 115 &tmd.TestId, 116 &refHash, 117 &compressedSourceRef, 118 &compressedTestMetadata); err != nil { 119 return err 120 } 121 tmd.Name = pbutil.TestMetadataName(tmd.Project, tmd.TestId, refHash) 122 tmd.RefHash = hex.EncodeToString(refHash) 123 tmd.TestMetadata = &pb.TestMetadata{} 124 if err := proto.Unmarshal(compressedTestMetadata, tmd.TestMetadata); err != nil { 125 return err 126 } 127 tmd.SourceRef = &pb.SourceRef{} 128 if err := proto.Unmarshal(compressedSourceRef, tmd.SourceRef); err != nil { 129 return err 130 } 131 return f(tmd) 132 }) 133 } 134 135 var queryTmpl = template.Must(template.New("").Parse(` 136 SELECT 137 tm.Project, 138 tm.TestId, 139 tm.RefHash, 140 ANY_VALUE(tm.SourceRef) AS SourceRef, 141 ANY_VALUE(tm.TestMetadata HAVING MIN tm.SubRealm) AS TestMetadata 142 FROM TestMetadata tm 143 WHERE tm.Project = @project 144 {{if .filterTestID}} 145 AND tm.TestId IN UNNEST(@testIDs) 146 {{end}} 147 AND tm.SubRealm IN UNNEST(@subRealms) 148 {{if .pagination}} 149 AND ((tm.TestId > @afterTestID) OR 150 (tm.TestId = @afterTestID AND tm.RefHash > @afterRefHash)) 151 {{end}} 152 GROUP BY tm.Project, tm.TestId, tm.RefHash 153 ORDER BY tm.TestId, tm.RefHash 154 {{if .hasLimit}}LIMIT @limit{{end}} 155 `))