Chromium Code Reviews| 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() |
| +} |