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 }