Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(394)

Unified Diff: service/datastore/query.go

Issue 1355783002: Refactor keys and queries in datastore service and implementation. (Closed) Base URL: https://github.com/luci/gae.git@master
Patch Set: Created 5 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: service/datastore/query.go
diff --git a/service/datastore/query.go b/service/datastore/query.go
new file mode 100644
index 0000000000000000000000000000000000000000..83396fe8ec63bb863e696b31ac17d857239759fe
--- /dev/null
+++ b/service/datastore/query.go
@@ -0,0 +1,603 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package datastore
+
+import (
+ "bytes"
+ "fmt"
+ "sort"
+ "strings"
+
+ "github.com/luci/luci-go/common/errors"
+ "github.com/luci/luci-go/common/stringset"
+)
+
+var ErrMultipleInequalityFilter = errors.New(
+ "inequality filters on multiple properties in the same Query is not allowed")
+
+var ErrNullQuery = errors.New(
+ "the query is overconstraind and can never have results")
+
+type Query struct {
+ kind string
+
+ eventualConsistency bool
+ keysOnly bool
+ distinct bool
+
+ limit *int32
+ offset *int32
+
+ order []IndexColumn
+ project stringset.Set
+
+ eqFilts map[string]PropertySlice
+
+ ineqFiltProp string
+ ineqFiltLow Property
+ ineqFiltLowIncl bool
+ ineqFiltLowSet bool
+ ineqFiltHigh Property
+ ineqFiltHighIncl bool
+ ineqFiltHighSet bool
+
+ start Cursor
+ end Cursor
+
+ // These are set by Finalize as a way to cache the 1-1 correspondence of
+ // a Query to its FinalizedQuery form. err may also be set by intermediate
+ // Query functions if there's a problem before finalization.
+ finalized *FinalizedQuery
+ err error
+}
+
+func NewQuery(kind string) *Query {
+ return &Query{kind: kind}
+}
+
+func (q *Query) mod(cb func(*Query)) *Query {
+ if q.err != nil {
+ return q
+ }
+
+ ret := *q
+ ret.finalized = nil
+ if len(q.order) > 0 {
+ ret.order = make([]IndexColumn, len(q.order))
+ copy(ret.order, q.order)
+ }
+ if q.project != nil {
+ ret.project = q.project.Dup()
+ }
+ if q.eqFilts != nil {
dnj 2015/09/18 16:47:58 len(q.eqFilts) > 0
iannucci 2015/09/18 22:25:49 done
+ ret.eqFilts = make(map[string]PropertySlice, len(q.eqFilts))
+ for k, v := range q.eqFilts {
+ newV := make(PropertySlice, len(v))
+ copy(newV, v)
+ ret.eqFilts[k] = newV
+ }
+ }
+ cb(&ret)
+ return &ret
+}
+
+func (q *Query) Kind(kind string) *Query {
dnj 2015/09/18 16:47:58 Worth doing pre-mod identity checks for these? e.g
iannucci 2015/09/18 22:25:48 Not really. If you have allocation bottlenecks in
+ return q.mod(func(q *Query) {
+ q.kind = kind
+ })
+}
+
+func (q *Query) Ancestor(ancestor *Key) *Query {
+ return q.mod(func(q *Query) {
+ if q.eqFilts == nil {
+ q.eqFilts = map[string]PropertySlice{}
+ }
+ if ancestor == nil {
+ delete(q.eqFilts, "__ancestor__")
+ if len(q.eqFilts) == 0 {
+ q.eqFilts = nil
+ }
+ } else {
+ q.eqFilts["__ancestor__"] = PropertySlice{MkProperty(ancestor)}
+ }
+ })
+}
+
+func (q *Query) EventualConsistency(on bool) *Query {
+ return q.mod(func(q *Query) {
+ q.eventualConsistency = on
+ })
+}
+
+func (q *Query) Limit(limit int32) *Query {
+ return q.mod(func(q *Query) {
+ if limit < 0 {
+ q.limit = nil
+ } else {
+ q.limit = &limit
+ }
+ })
+}
+
+func (q *Query) Offset(offset int32) *Query {
+ return q.mod(func(q *Query) {
+ if offset < 0 {
+ q.offset = nil
+ } else {
+ q.offset = &offset
+ }
+ })
+}
+
+func (q *Query) KeysOnly(on bool) *Query {
+ return q.mod(func(q *Query) {
+ q.keysOnly = on
+ })
+}
+
+func (q *Query) Order(fieldNames ...string) *Query {
+ return q.mod(func(q *Query) {
+ for _, fn := range fieldNames {
+ ic, err := ParseIndexColumn(fn)
+ if err != nil {
+ q.err = err
+ return
+ }
+ if q.reserved(ic.Property) {
+ return
+ }
+ q.order = append(q.order, ic)
+ }
+ })
+}
+
+func (q *Query) RevertOrder() *Query {
dnj 2015/09/18 16:47:59 ClearOrder()
+ return q.mod(func(q *Query) {
+ q.order = nil
+ })
+}
+
+func (q *Query) Project(fieldNames ...string) *Query {
+ return q.mod(func(q *Query) {
+ for _, f := range fieldNames {
+ if q.reserved(f) {
+ return
+ }
+ if f == "__key__" {
+ q.err = fmt.Errorf("cannot project on %q", f)
+ return
+ }
+ if q.project == nil {
+ q.project = stringset.New(1)
+ }
+ q.project.Add(f)
+ }
+ })
+}
+
+func (q *Query) Distinct(on bool) *Query {
+ return q.mod(func(q *Query) {
+ q.distinct = on
+ })
+}
+
+func (q *Query) RevertProject() *Query {
dnj 2015/09/18 16:47:59 ClearProject
+ return q.mod(func(q *Query) {
+ q.project = nil
+ })
+}
+
+func (q *Query) Start(c Cursor) *Query {
+ return q.mod(func(q *Query) {
+ q.start = c
+ })
+}
+
+func (q *Query) End(c Cursor) *Query {
+ return q.mod(func(q *Query) {
+ q.end = c
+ })
+}
+
+func (q *Query) Eq(field string, values ...interface{}) *Query {
+ return q.mod(func(q *Query) {
+ if !q.reserved(field) {
+ if q.eqFilts == nil {
+ q.eqFilts = map[string]PropertySlice{}
dnj 2015/09/18 16:47:58 make(..., 1)
iannucci 2015/09/18 22:25:48 has anyone told you that you're the king of warran
+ }
+ s := q.eqFilts[field]
+ for _, value := range values {
+ p := Property{}
+ if q.err = p.SetValue(value, ShouldIndex); q.err != nil {
+ return
+ }
+ idx := sort.Search(len(s), func(i int) bool {
+ return s[i].Equal(&p)
+ })
+ if idx == len(s) {
+ s = append(s, p)
+ sort.Sort(s)
+ }
+ }
+ q.eqFilts[field] = s
+ }
+ })
+}
+
+func (q *Query) reserved(field string) bool {
+ if field == "__key__" {
+ return false
+ }
+ if field == "" {
+ q.err = fmt.Errorf(
+ "cannot filter/project on: %q", field)
+ return true
+ }
+ if strings.HasPrefix(field, "__") && strings.HasSuffix(field, "__") {
dnj 2015/09/18 16:47:58 Is "_ _ _" (without spaces) reserved? Granted it's
iannucci 2015/09/18 22:25:49 They're not actually reserved in the underlying th
+ q.err = fmt.Errorf(
+ "cannot filter/project on reserved property: %q", field)
+ return true
+ }
+ return false
+}
+
+func (q *Query) ineqOK(field string, value Property) bool {
+ if q.reserved(field) {
+ return false
+ }
+ if field == "__key__" && value.Type() != PTKey {
+ q.err = fmt.Errorf(
+ "filters on %q must have type *Key (got %s)", field, value.Type())
+ return false
+ }
+ if q.ineqFiltProp != "" && q.ineqFiltProp != field {
+ q.err = ErrMultipleInequalityFilter
+ return false
+ }
+ return true
+}
+
+func (q *Query) Lt(field string, value interface{}) *Query {
+ p := Property{}
+ err := p.SetValue(value, ShouldIndex)
+
+ if err == nil && q.ineqFiltHighSet {
dnj 2015/09/18 16:47:59 IMO the auto-adjustment is convenient, but it shou
iannucci 2015/09/18 22:25:49 yep, it does in q.ineqOK (which modifies q.err as
+ if q.ineqFiltHigh.Less(&p) {
+ return q
+ } else if q.ineqFiltHigh.Equal(&p) && !q.ineqFiltHighIncl {
+ return q
+ }
+ }
+
+ return q.mod(func(q *Query) {
+ if q.err = err; err != nil {
+ return
+ }
+ if q.ineqOK(field, p) {
+ q.ineqFiltProp = field
+ q.ineqFiltHighSet = true
+ q.ineqFiltHigh = p
+ q.ineqFiltHighIncl = false
+ }
+ })
+}
+
+func (q *Query) Lte(field string, value interface{}) *Query {
+ p := Property{}
+ err := p.SetValue(value, ShouldIndex)
+
+ if err == nil && q.ineqFiltHighSet {
+ if q.ineqFiltHigh.Less(&p) {
+ return q
+ } else if q.ineqFiltHigh.Equal(&p) {
+ return q
+ }
+ }
+
+ return q.mod(func(q *Query) {
+ if q.err = err; err != nil {
+ return
+ }
+ if q.ineqOK(field, p) {
+ q.ineqFiltProp = field
+ q.ineqFiltHighSet = true
+ q.ineqFiltHigh = p
+ q.ineqFiltHighIncl = true
+ }
+ })
+}
+
+func (q *Query) Gt(field string, value interface{}) *Query {
+ p := Property{}
+ err := p.SetValue(value, ShouldIndex)
+
+ if err == nil && q.ineqFiltLowSet {
+ if p.Less(&q.ineqFiltLow) {
+ return q
+ } else if p.Equal(&q.ineqFiltLow) && !q.ineqFiltLowIncl {
+ return q
+ }
+ }
+
+ return q.mod(func(q *Query) {
+ if q.err = err; err != nil {
+ return
+ }
+ if q.ineqOK(field, p) {
+ q.ineqFiltProp = field
+ q.ineqFiltLowSet = true
+ q.ineqFiltLow = p
+ q.ineqFiltLowIncl = false
+ }
+ })
+}
+
+func (q *Query) Gte(field string, value interface{}) *Query {
+ p := Property{}
+ err := p.SetValue(value, ShouldIndex)
+
+ if err == nil && q.ineqFiltLowSet {
+ if p.Less(&q.ineqFiltLow) {
+ return q
+ } else if p.Equal(&q.ineqFiltLow) {
+ return q
+ }
+ }
+
+ return q.mod(func(q *Query) {
+ if q.err = err; err != nil {
+ return
+ }
+ if q.ineqOK(field, p) {
+ q.ineqFiltProp = field
+ q.ineqFiltLowSet = true
+ q.ineqFiltLow = p
+ q.ineqFiltLowIncl = true
+ }
+ })
+}
+
+func (q *Query) RevertFilters() *Query {
dnj 2015/09/18 16:47:58 ClearFilters
iannucci 2015/09/18 22:25:48 done for all
+ return q.mod(func(q *Query) {
+ anc := q.eqFilts["__ancestor__"]
+ if anc != nil {
+ q.eqFilts = map[string]PropertySlice{"__ancestor__": anc}
+ } else {
+ q.eqFilts = nil
+ }
+ q.ineqFiltLowSet = false
+ q.ineqFiltHighSet = false
+ })
+}
+
+func (q *Query) Finalize() (*FinalizedQuery, error) {
+ if q.err != nil || q.finalized != nil {
+ return q.finalized, q.err
+ }
+
+ err := func() error {
+ if q.kind == "" { // kindless query checks
+ if q.ineqFiltProp != "" && q.ineqFiltProp != "__key__" {
+ return fmt.Errorf(
+ "kindless queries can only filter on __key__, got %q", q.ineqFiltProp)
+ }
+ if len(q.eqFilts) > 0 {
+ return fmt.Errorf("kindless queries not have any equality filters")
+ }
+ for _, o := range q.order {
+ if o.Property != "__key__" || o.Direction != ASCENDING {
+ return fmt.Errorf("invalid order for kindless query: %#v", o)
+ }
+ }
+ }
+
+ if q.keysOnly && q.project != nil && q.project.Len() > 0 {
+ return errors.New("cannot project a keysOnly query")
+ }
+
+ if q.ineqFiltProp != "" {
+ if len(q.order) > 0 && q.order[0].Property != q.ineqFiltProp {
+ return fmt.Errorf(
+ "first sort order must match inequality filter: %q v %q",
+ q.order[0].Property, q.ineqFiltProp)
+ }
+ if q.ineqFiltLowSet && q.ineqFiltHighSet {
+ if q.ineqFiltHigh.Less(&q.ineqFiltLow) &&
+ (q.ineqFiltHigh.Equal(&q.ineqFiltLow) &&
+ (!q.ineqFiltLowIncl || !q.ineqFiltHighIncl)) {
+ return ErrNullQuery
+ }
+ }
+ }
+
+ err := error(nil)
+ if q.project != nil {
+ q.project.Iter(func(p string) bool {
+ if _, iseq := q.eqFilts[p]; iseq {
+ err = fmt.Errorf("cannot project on equality filter field: %s", p)
+ return false
+ }
+ return true
+ })
+ }
+ return err
+ }()
+ if err != nil {
+ q.err = err
+ return nil, err
+ }
+
+ ret := &FinalizedQuery{
+ original: q,
+ kind: q.kind,
+
+ keysOnly: q.keysOnly,
+ eventuallyConsistent: q.eventualConsistency || q.eqFilts["__ancestor__"] == nil,
+ limit: q.limit,
+ offset: q.offset,
+ start: q.start,
+ end: q.end,
+
+ eqFilts: q.eqFilts,
+
+ ineqFiltProp: q.ineqFiltProp,
+ ineqFiltLow: q.ineqFiltLow,
+ ineqFiltLowIncl: q.ineqFiltLowIncl,
+ ineqFiltLowSet: q.ineqFiltLowSet,
+ ineqFiltHigh: q.ineqFiltHigh,
+ ineqFiltHighIncl: q.ineqFiltHighIncl,
+ ineqFiltHighSet: q.ineqFiltHighSet,
+ }
+
+ if q.project != nil {
+ ret.project = q.project.ToSlice()
+ ret.distinct = q.distinct && q.project.Len() > 0
+ }
+
+ // if len(q.order) > 0, we already enforce that the first order
+ // is the same as the inequality above. Otherwise we need to add it.
+ if len(q.order) == 0 && q.ineqFiltProp != "" {
+ ret.orders = []IndexColumn{{q.ineqFiltProp, ASCENDING}}
+ }
+
+ seenOrders := stringset.New(len(q.order))
+
+ // drop orders where there's an equality filter
+ // https://cloud.google.com/appengine/docs/go/datastore/queries#sort_orders_are_ignored_on_properties_with_equality_filters
+ // Deduplicate orders
+ for _, o := range q.order {
+ if _, iseq := q.eqFilts[o.Property]; !iseq {
+ if seenOrders.Add(o.Property) {
+ ret.orders = append(ret.orders, o)
+ }
+ }
+ }
+
+ // Add any projection columns not mentioned in the user-defined order as
+ // ASCENDING orders. Technically we could be smart and automatically use
+ // a DESCENDING ordered index, if it fit, but the logic gets insane, since all
+ // suffixes of all used indexes need to be PRECISELY equal (and so you'd have
+ // to hunt/invalidate/something to find the combination of indexes that are
+ // compatible with each other as well as the query). If you want to use
+ // a DESCENDING column, just add it to the user sort order, and this loop will
+ // not synthesize a new suffix entry for it.
+ //
+ // NOTE: if you want to use an index that sorts by -__key__, you MUST
+ // include all of the projected fields for that index in the order explicitly.
+ // Otherwise the generated orders will be wacky. So:
+ // Query("Foo").Project("A", "B").Order("A").Order("-__key__")
+ //
+ // will turn into a orders of:
+ // A, ASCENDING
+ // __key__, DESCENDING
+ // B, ASCENDING
+ // __key__, ASCENDING
+ //
+ // To prevent this, your query should have another Order("B") clause before
+ // the -__key__ clause.
+ if len(ret.project) > 0 {
+ sort.Strings(ret.project)
+ for _, p := range ret.project {
+ if !seenOrders.Has(p) {
+ ret.orders = append(ret.orders, IndexColumn{p, ASCENDING})
+ }
+ }
+ }
+
+ // If the suffix format ends with __key__ already (e.g. .Order("__key__")),
+ // then we're good to go. Otherwise we need to add it as the last bit of the
+ // suffix, since all indexes implicitly have it as the last column.
+ if len(ret.orders) == 0 || ret.orders[len(ret.orders)-1].Property != "__key__" {
+ ret.orders = append(ret.orders, IndexColumn{"__key__", ASCENDING})
+ }
+
+ q.finalized = ret
+ return ret, nil
+}
+
+func (q *Query) String() string {
+ ret := &bytes.Buffer{}
+ needComma := false
+ p := func(fmtStr string, stuff ...interface{}) {
+ if needComma {
+ ret.WriteString(", ")
+ }
+ needComma = true
+ fmt.Fprintf(ret, fmtStr, stuff...)
+ }
+ ret.WriteString("Query(")
+ if q.err != nil {
+ p("ERROR=%q", q.err.Error())
+ }
+
+ // Filters
+ if q.kind != "" {
+ p("Kind=%q", q.kind)
+ }
+ if q.eqFilts["__ancestor__"] != nil {
+ p("Ancestor=%s", q.eqFilts["__ancestor__"][0].Value().(*Key).String())
+ }
+ for prop, vals := range q.eqFilts {
+ if prop == "__ancestor__" {
+ continue
+ }
+ for _, v := range vals {
+ p("Filter(%q == %s)", prop, v.GQL())
+ }
+ }
+ if q.ineqFiltProp != "" {
+ if q.ineqFiltLowSet {
+ op := ">"
+ if q.ineqFiltLowIncl {
+ op = ">="
+ }
+ p("Filter(%q %s %s)", q.ineqFiltProp, op, q.ineqFiltLow.GQL())
+ }
dnj 2015/09/18 16:47:58 ineqFiltHighSet?
iannucci 2015/09/18 22:25:48 oops
+ }
+
+ // Order
+ if len(q.order) > 0 {
+ orders := make([]string, len(q.order))
+ for i, o := range q.order {
+ orders[i] = o.String()
+ }
+ p("Order(%s)", strings.Join(orders, ", "))
+ }
+
+ // Projection
+ if q.project != nil && q.project.Len() > 0 {
+ f := "Project(%s)"
+ if q.distinct {
+ f = "Project[DISTINCT](%s)"
+ }
+ p(f, strings.Join(q.project.ToSlice(), ", "))
+ }
+
+ // Cursors
+ if q.start != nil {
+ p("Start(%q)", q.start.String())
+ }
+ if q.end != nil {
+ p("End(%q)", q.end.String())
+ }
+
+ // Modifiiers
+ if q.limit != nil {
+ p("Limit=%d", *q.limit)
+ }
+ if q.offset != nil {
+ p("Offset=%d", *q.offset)
+ }
+ if q.eventualConsistency {
+ p("EventualConsistency")
+ }
+ if q.keysOnly {
+ p("KeysOnly")
+ }
+
+ ret.WriteRune(')')
+
+ return ret.String()
+}

Powered by Google App Engine
This is Rietveld 408576698