github.com/cornelk/go-cloud@v0.17.1/docstore/query.go (about)

     1  // Copyright 2019 The Go Cloud Development Kit 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  //     https://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 docstore
    16  
    17  import (
    18  	"context"
    19  	"io"
    20  	"reflect"
    21  	"time"
    22  
    23  	"github.com/cornelk/go-cloud/docstore/driver"
    24  	"github.com/cornelk/go-cloud/internal/gcerr"
    25  )
    26  
    27  // Query represents a query over a collection.
    28  type Query struct {
    29  	coll *Collection
    30  	dq   *driver.Query
    31  	err  error
    32  }
    33  
    34  // Query creates a new Query over the collection.
    35  func (c *Collection) Query() *Query {
    36  	return &Query{coll: c, dq: &driver.Query{}}
    37  }
    38  
    39  // Where expresses a condition on the query.
    40  // Valid ops are: "=", ">", "<", ">=", "<=".
    41  // Valid values are strings, integers, floating-point numbers, and time.Time values.
    42  func (q *Query) Where(fp FieldPath, op string, value interface{}) *Query {
    43  	if q.err != nil {
    44  		return q
    45  	}
    46  	pfp, err := parseFieldPath(fp)
    47  	if err != nil {
    48  		q.err = err
    49  		return q
    50  	}
    51  	if !validOp[op] {
    52  		return q.invalidf("invalid filter operator: %q. Use one of: =, >, <, >=, <=", op)
    53  	}
    54  	if !validFilterValue(value) {
    55  		return q.invalidf("invalid filter value: %v", value)
    56  	}
    57  	q.dq.Filters = append(q.dq.Filters, driver.Filter{
    58  		FieldPath: pfp,
    59  		Op:        op,
    60  		Value:     value,
    61  	})
    62  	return q
    63  }
    64  
    65  var validOp = map[string]bool{
    66  	"=":  true,
    67  	">":  true,
    68  	"<":  true,
    69  	">=": true,
    70  	"<=": true,
    71  }
    72  
    73  func validFilterValue(v interface{}) bool {
    74  	if v == nil {
    75  		return false
    76  	}
    77  	if _, ok := v.(time.Time); ok {
    78  		return true
    79  	}
    80  	switch reflect.TypeOf(v).Kind() {
    81  	case reflect.String:
    82  		return true
    83  	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
    84  		return true
    85  	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
    86  		return true
    87  	case reflect.Float32, reflect.Float64:
    88  		return true
    89  	default:
    90  		return false
    91  	}
    92  }
    93  
    94  // Limit will limit the results to at most n documents.
    95  // n must be positive.
    96  // It is an error to specify Limit more than once in a Get query, or
    97  // at all in a Delete or Update query.
    98  func (q *Query) Limit(n int) *Query {
    99  	if q.err != nil {
   100  		return q
   101  	}
   102  	if n <= 0 {
   103  		return q.invalidf("limit value of %d must be greater than zero", n)
   104  	}
   105  	if q.dq.Limit > 0 {
   106  		return q.invalidf("query can have at most one limit clause")
   107  	}
   108  	q.dq.Limit = n
   109  	return q
   110  }
   111  
   112  // Ascending and Descending are constants for use in the OrderBy method.
   113  const (
   114  	Ascending  = "asc"
   115  	Descending = "desc"
   116  )
   117  
   118  // OrderBy specifies that the returned documents appear sorted by the given field in
   119  // the given direction.
   120  // A query can have at most one OrderBy clause. If it has none, the order of returned
   121  // documents is unspecified.
   122  // If a query has a Where clause and an OrderBy clause, the OrderBy clause's field
   123  // must appear in a Where clause.
   124  // It is an error to specify OrderBy in a Delete or Update query.
   125  func (q *Query) OrderBy(field, direction string) *Query {
   126  	if q.err != nil {
   127  		return q
   128  	}
   129  	if field == "" {
   130  		return q.invalidf("OrderBy: empty field")
   131  	}
   132  	if direction != Ascending && direction != Descending {
   133  		return q.invalidf("OrderBy: direction must be one of %q or %q", Ascending, Descending)
   134  	}
   135  	if q.dq.OrderByField != "" {
   136  		return q.invalidf("a query can have at most one OrderBy")
   137  	}
   138  	q.dq.OrderByField = field
   139  	q.dq.OrderAscending = (direction == Ascending)
   140  	return q
   141  }
   142  
   143  // BeforeQuery takes a callback function that will be called before the Query is
   144  // executed to the underlying service's query functionality. The callback takes
   145  // a parameter, asFunc, that converts its argument to driver-specific types.
   146  // See https://github.com/cornelk/go-cloud/concepts/as/ for background information.
   147  func (q *Query) BeforeQuery(f func(asFunc func(interface{}) bool) error) *Query {
   148  	q.dq.BeforeQuery = f
   149  	return q
   150  }
   151  
   152  // Get returns an iterator for retrieving the documents specified by the query. If
   153  // field paths are provided, only those paths are set in the resulting documents.
   154  //
   155  // Call Stop on the iterator when finished.
   156  func (q *Query) Get(ctx context.Context, fps ...FieldPath) *DocumentIterator {
   157  	return q.get(ctx, true, fps...)
   158  }
   159  
   160  // get implements Get, with optional OpenCensus tracing so it can be used internally.
   161  func (q *Query) get(ctx context.Context, oc bool, fps ...FieldPath) *DocumentIterator {
   162  	dcoll := q.coll.driver
   163  	if err := q.initGet(fps); err != nil {
   164  		return &DocumentIterator{err: wrapError(dcoll, err)}
   165  	}
   166  
   167  	var err error
   168  	if oc {
   169  		ctx = q.coll.tracer.Start(ctx, "Query.Get")
   170  		defer func() { q.coll.tracer.End(ctx, err) }()
   171  	}
   172  	it, err := dcoll.RunGetQuery(ctx, q.dq)
   173  	return &DocumentIterator{iter: it, coll: q.coll, err: wrapError(dcoll, err)}
   174  }
   175  
   176  func (q *Query) initGet(fps []FieldPath) error {
   177  	if q.err != nil {
   178  		return q.err
   179  	}
   180  	if err := q.coll.checkClosed(); err != nil {
   181  		return errClosed
   182  	}
   183  	pfps, err := parseFieldPaths(fps)
   184  	if err != nil {
   185  		return err
   186  	}
   187  	q.dq.FieldPaths = pfps
   188  	if q.dq.OrderByField != "" && len(q.dq.Filters) > 0 {
   189  		found := false
   190  		for _, f := range q.dq.Filters {
   191  			if len(f.FieldPath) == 1 && f.FieldPath[0] == q.dq.OrderByField {
   192  				found = true
   193  				break
   194  			}
   195  		}
   196  		if !found {
   197  			return gcerr.Newf(gcerr.InvalidArgument, nil, "OrderBy field %s must appear in a Where clause",
   198  				q.dq.OrderByField)
   199  		}
   200  	}
   201  	return nil
   202  }
   203  
   204  func (q *Query) invalidf(format string, args ...interface{}) *Query {
   205  	q.err = gcerr.Newf(gcerr.InvalidArgument, nil, format, args...)
   206  	return q
   207  }
   208  
   209  // DocumentIterator iterates over documents.
   210  //
   211  // Always call Stop on the iterator.
   212  type DocumentIterator struct {
   213  	iter driver.DocumentIterator
   214  	coll *Collection
   215  	err  error // already wrapped
   216  }
   217  
   218  // Next stores the next document in dst. It returns io.EOF if there are no more
   219  // documents.
   220  // Once Next returns an error, it will always return the same error.
   221  func (it *DocumentIterator) Next(ctx context.Context, dst Document) error {
   222  	if it.err != nil {
   223  		return it.err
   224  	}
   225  	if err := it.coll.checkClosed(); err != nil {
   226  		it.err = err
   227  		return it.err
   228  	}
   229  	ddoc, err := driver.NewDocument(dst)
   230  	if err != nil {
   231  		it.err = wrapError(it.coll.driver, err)
   232  		return it.err
   233  	}
   234  	it.err = wrapError(it.coll.driver, it.iter.Next(ctx, ddoc))
   235  	return it.err
   236  }
   237  
   238  // Stop stops the iterator. Calling Next on a stopped iterator will return io.EOF, or
   239  // the error that Next previously returned.
   240  func (it *DocumentIterator) Stop() {
   241  	if it.err != nil {
   242  		return
   243  	}
   244  	it.err = io.EOF
   245  	it.iter.Stop()
   246  }
   247  
   248  // As converts i to driver-specific types.
   249  // See https://github.com/cornelk/go-cloud/concepts/as/ for background information, the "As"
   250  // examples in this package for examples, and the driver package
   251  // documentation for the specific types supported for that driver.
   252  func (it *DocumentIterator) As(i interface{}) bool {
   253  	if i == nil || it.iter == nil {
   254  		return false
   255  	}
   256  	return it.iter.As(i)
   257  }
   258  
   259  // Plan describes how the query would be executed if its Get method were called with
   260  // the given field paths. Plan uses only information available to the client, so it
   261  // cannot know whether a service uses indexes or scans internally.
   262  func (q *Query) Plan(fps ...FieldPath) (string, error) {
   263  	if err := q.initGet(fps); err != nil {
   264  		return "", err
   265  	}
   266  	return q.coll.driver.QueryPlan(q.dq)
   267  }