go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/resultdb/internal/exonerations/query.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 exonerations
    16  
    17  import (
    18  	"context"
    19  
    20  	"cloud.google.com/go/spanner"
    21  
    22  	"go.chromium.org/luci/common/proto/mask"
    23  
    24  	"go.chromium.org/luci/resultdb/internal/invocations"
    25  	"go.chromium.org/luci/resultdb/internal/pagination"
    26  	"go.chromium.org/luci/resultdb/internal/spanutil"
    27  	"go.chromium.org/luci/resultdb/pbutil"
    28  	pb "go.chromium.org/luci/resultdb/proto/v1"
    29  )
    30  
    31  // LimitedFields is a field mask for TestExoneration to use when the caller
    32  // only has the listLimited permission for test exonerations.
    33  var limitedFields = mask.MustFromReadMask(&pb.TestExoneration{},
    34  	"name",
    35  	"test_id",
    36  	"exoneration_id",
    37  	"variant_hash",
    38  	"explanation_html",
    39  	"reason",
    40  )
    41  
    42  // Query specifies test exonerations to fetch.
    43  type Query struct {
    44  	InvocationIDs invocations.IDSet
    45  	Predicate     *pb.TestExonerationPredicate
    46  	PageSize      int // must be positive
    47  	PageToken     string
    48  }
    49  
    50  // Fetch returns a page test of exonerations matching the query.
    51  // Returned test exonerations are ordered by invocation ID, test ID and
    52  // exoneration ID.
    53  func (q *Query) Fetch(ctx context.Context) (tes []*pb.TestExoneration, nextPageToken string, err error) {
    54  	if q.PageSize <= 0 {
    55  		panic("PageSize <= 0")
    56  	}
    57  
    58  	st := spanner.NewStatement(`
    59  		SELECT InvocationId, TestId, ExonerationId, Variant, VariantHash, ExplanationHtml, Reason
    60  		FROM TestExonerations
    61  		WHERE InvocationId IN UNNEST(@invIDs)
    62  			# Skip test exonerations after the one specified in the page token.
    63  			AND (
    64  				(InvocationId > @afterInvocationId) OR
    65  				(InvocationId = @afterInvocationId AND TestId > @afterTestId) OR
    66  				(InvocationId = @afterInvocationId AND TestId = @afterTestId AND ExonerationID > @afterExonerationID)
    67  		  )
    68  		ORDER BY InvocationId, TestId, ExonerationId
    69  		LIMIT @limit
    70  	`)
    71  	st.Params["invIDs"] = q.InvocationIDs
    72  	st.Params["limit"] = q.PageSize
    73  	err = invocations.TokenToMap(q.PageToken, st.Params, "afterInvocationId", "afterTestId", "afterExonerationID")
    74  	if err != nil {
    75  		return
    76  	}
    77  
    78  	// TODO(nodir): add support for q.Predicate.TestId.
    79  	// TODO(nodir): add support for q.Predicate.Variant.
    80  
    81  	tes = make([]*pb.TestExoneration, 0, q.PageSize)
    82  	var b spanutil.Buffer
    83  	var explanationHTML spanutil.Compressed
    84  	err = spanutil.Query(ctx, st, func(row *spanner.Row) error {
    85  		var invID invocations.ID
    86  		ex := &pb.TestExoneration{}
    87  		err := b.FromSpanner(row, &invID, &ex.TestId, &ex.ExonerationId, &ex.Variant, &ex.VariantHash, &explanationHTML, &ex.Reason)
    88  		if err != nil {
    89  			return err
    90  		}
    91  		ex.Name = pbutil.TestExonerationName(string(invID), ex.TestId, ex.ExonerationId)
    92  		ex.ExplanationHtml = string(explanationHTML)
    93  		tes = append(tes, ex)
    94  		return nil
    95  	})
    96  	if err != nil {
    97  		tes = nil
    98  		return
    99  	}
   100  
   101  	// If we got pageSize results, then we haven't exhausted the collection and
   102  	// need to return the next page token.
   103  	if len(tes) == q.PageSize {
   104  		last := tes[q.PageSize-1]
   105  		invID, testID, exID := MustParseName(last.Name)
   106  		nextPageToken = pagination.Token(string(invID), testID, exID)
   107  	}
   108  	return
   109  }
   110  
   111  // ToLimitedData limits the given TestExoneration to the fields allowed when
   112  // the caller only has the listLimited permission for test exonerations.
   113  func ToLimitedData(ctx context.Context, te *pb.TestExoneration) error {
   114  	if err := limitedFields.Trim(te); err != nil {
   115  		return err
   116  	}
   117  
   118  	te.IsMasked = true
   119  
   120  	return nil
   121  }