go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/proto/paged/queries.go (about)

     1  // Copyright 2019 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 paged implements a helper for making paginated Datastore queries.
    16  package paged
    17  
    18  import (
    19  	"context"
    20  	"reflect"
    21  
    22  	"github.com/golang/protobuf/proto"
    23  
    24  	"google.golang.org/grpc/codes"
    25  	"google.golang.org/grpc/status"
    26  
    27  	"go.chromium.org/luci/common/errors"
    28  	"go.chromium.org/luci/gae/service/datastore"
    29  )
    30  
    31  // Response is an interface implemented by ListResponses which support page
    32  // tokens.
    33  type Response interface {
    34  	proto.Message
    35  	// GetNextPageToken returns a token to use to fetch the next page of results.
    36  	GetNextPageToken() string
    37  }
    38  
    39  // cursorCBType is the reflect.Type of a datastore.CursorCB.
    40  var cursorCBType = reflect.TypeOf((datastore.CursorCB)(nil))
    41  
    42  // returnedNil is a []reflect.Value{} containing one nil error.
    43  var returnedNil = reflect.ValueOf(func() error { return nil }).Call([]reflect.Value{})
    44  
    45  // returnedStop is a []reflect.Value{} containing one datastore.Stop error.
    46  var returnedStop = reflect.ValueOf(func() error { return datastore.Stop }).Call([]reflect.Value{})
    47  
    48  // Query executes a query to fetch the given page of results, invoking a
    49  // callback function for each key or entity returned by the query. If the page
    50  // isn't the last of the query, the given response will have its next page token
    51  // set appropriately.
    52  //
    53  // A non-positive limit means to fetch all results starting at the given page
    54  // token in a single page. An empty page token means to start at the first page.
    55  //
    56  // The callback must be a function of one argument, the type of which is either
    57  // *datastore.Key (implies keys-only query) or a pointer to a struct to decode
    58  // the returned entity into. The callback should return an error, which if not
    59  // nil halts the query, and if the error is not datastore.Stop, causes this
    60  // function to return an error as well. See datastore.Run for more information.
    61  // No maximum page size is imposed, use datastore.Stop to enforce one.
    62  func Query(ctx context.Context, lim int32, tok string, rsp Response, q *datastore.Query, cb any) error {
    63  	// Validate as much about the callback as this function relies on.
    64  	// The rest is validated by datastore.Run.
    65  	v := reflect.ValueOf(cb)
    66  	if v.Kind() != reflect.Func {
    67  		return errors.Reason("callback must be a function").Err()
    68  	}
    69  	t := v.Type()
    70  	switch {
    71  	case t.NumIn() != 1:
    72  		return errors.Reason("callback function must accept one argument").Err()
    73  	case t.NumOut() != 1:
    74  		return errors.Reason("callback function must return one value").Err()
    75  	}
    76  
    77  	// Modify the query with the request parameters.
    78  	if tok != "" {
    79  		cur, err := datastore.DecodeCursor(ctx, tok)
    80  		if err != nil {
    81  			return status.Errorf(codes.InvalidArgument, "invalid page token %q", tok)
    82  		}
    83  		q = q.Start(cur)
    84  	}
    85  	if lim > 0 {
    86  		// Peek ahead at the next result to determine if the cursor for the given page size
    87  		// is worth returning. The cursor should be omitted if there are no further results.
    88  		q = q.Limit(lim + 1)
    89  	}
    90  
    91  	// Wrap the callback with a custom function that grabs the cursor (if necessary) before
    92  	// invoking the callback for each result up to the page size specified in the request.
    93  	// This is the type of function datastore.Run will receive as an argument.
    94  	// TODO(smut): Move this block to gae/datastore, since it doesn't depend on PagedRequest.
    95  	t = reflect.FuncOf([]reflect.Type{t.In(0), cursorCBType}, []reflect.Type{t.Out(0)}, false)
    96  	var cur datastore.Cursor
    97  
    98  	// If the query is not limited and the callback never returns datastore.Stop, the query runs
    99  	// until the end so it's not necessary to set the next page token. If the callback does
   100  	// return datastore.Stop, save the cursor but peek at the next result. Only set the next
   101  	// page token if there is a next result.
   102  	// If the query is limited, the limit is set to one more than the specified value in order
   103  	// to peek at the next result by default. Save the cursor at the limit but peek at the next
   104  	// result. Only set the next page token if there is a next result. The callback may return
   105  	// datastore.Stop ahead of the limit. If it does, save the cursor but peek at the next result
   106  	// Only set the next page token if there is a next result.
   107  	i := int32(0)
   108  	curCB := reflect.MakeFunc(t, func(args []reflect.Value) []reflect.Value {
   109  		i++
   110  		if cur != nil {
   111  			// Cursor is set below, when the result is at the limit or datastore.Stop
   112  			// is returned by the callback. Since the query is still running, there
   113  			// are more results. Set the page token and halt the query. Don't invoke
   114  			// the callback since it isn't expecting any more results.
   115  			f := reflect.ValueOf(rsp).Elem().FieldByName("NextPageToken")
   116  			f.SetString(cur.String())
   117  			return returnedStop
   118  		}
   119  		// Invoke the callback. Per t, it returns one argument (the error).
   120  		ret := v.Call([]reflect.Value{args[0]})
   121  		// Save the cursor if the callback wants to stop or the query is limited and
   122  		// this is the last requested result. In either case peek at the next result.
   123  		if ret[0].Interface() == datastore.Stop || (i == lim && ret[0].IsNil()) {
   124  			var err error
   125  			cur, err = args[1].Interface().(datastore.CursorCB)()
   126  			if err != nil {
   127  				return []reflect.Value{reflect.ValueOf(errors.Annotate(err, "failed to fetch cursor").Err())}
   128  			}
   129  			return returnedNil
   130  		}
   131  		return ret
   132  	}).Interface()
   133  
   134  	if err := datastore.Run(ctx, q, curCB); err != nil {
   135  		return errors.Annotate(err, "failed to fetch entities").Err()
   136  	}
   137  	return nil
   138  }