| Index: service/datastore/finalized_query.go
|
| diff --git a/service/datastore/finalized_query.go b/service/datastore/finalized_query.go
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..c57eaa97b4a7b10154c5840ebaae2b8a8b8cd5f3
|
| --- /dev/null
|
| +++ b/service/datastore/finalized_query.go
|
| @@ -0,0 +1,337 @@
|
| +// 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"
|
| +)
|
| +
|
| +// FinalizedQuery is the representation of a Query which has been normalized.
|
| +//
|
| +// It contains only fully-specified, non-redundant, non-conflicting information
|
| +// pertaining to the Query to run. It can only represent a valid query.
|
| +type FinalizedQuery struct {
|
| + original *Query
|
| +
|
| + kind string
|
| + eventuallyConsistent bool
|
| + distinct bool
|
| + keysOnly bool
|
| +
|
| + limit *int32
|
| + offset *int32
|
| +
|
| + start Cursor
|
| + end Cursor
|
| +
|
| + project []string
|
| + orders []IndexColumn
|
| +
|
| + eqFilts map[string]PropertySlice
|
| +
|
| + ineqFiltProp string
|
| + ineqFiltLow Property
|
| + ineqFiltLowIncl bool
|
| + ineqFiltLowSet bool
|
| + ineqFiltHigh Property
|
| + ineqFiltHighIncl bool
|
| + ineqFiltHighSet bool
|
| +}
|
| +
|
| +// Original returns the original Query object from which this FinalizedQuery was
|
| +// derived.
|
| +func (q *FinalizedQuery) Original() *Query {
|
| + return q.original
|
| +}
|
| +
|
| +// Kind returns the datastore 'Kind' over which this query operates. It may be
|
| +// empty for a kindless query.
|
| +func (q *FinalizedQuery) Kind() string {
|
| + return q.kind
|
| +}
|
| +
|
| +// EventuallyConsistent returns true iff this query will be eventually
|
| +// consistent. This is true when the query is a non-ancestor query, or when it's
|
| +// an ancestory query with the 'EventualConsistency(true)' option set.
|
| +func (q *FinalizedQuery) EventuallyConsistent() bool {
|
| + return q.eventuallyConsistent
|
| +}
|
| +
|
| +// Project is the list of fields that this query projects on, or empty if this
|
| +// is not a projection query.
|
| +func (q *FinalizedQuery) Project() []string {
|
| + if len(q.project) == 0 {
|
| + return nil
|
| + }
|
| + ret := make([]string, len(q.project))
|
| + copy(ret, q.project)
|
| + return ret
|
| +}
|
| +
|
| +// Distinct returnst true iff this is a distinct projection query. It will never
|
| +// be true for non-projection queries.
|
| +func (q *FinalizedQuery) Distinct() bool {
|
| + return q.distinct
|
| +}
|
| +
|
| +// KeysOnly returns true iff this query will only return keys (as opposed to a
|
| +// normal or projection query).
|
| +func (q *FinalizedQuery) KeysOnly() bool {
|
| + return q.keysOnly
|
| +}
|
| +
|
| +// Limit returns the maximum number of responses this query will retrieve, and a
|
| +// boolean indicating if the limit is set.
|
| +func (q *FinalizedQuery) Limit() (int32, bool) {
|
| + if q.limit != nil {
|
| + return *q.limit, true
|
| + }
|
| + return 0, false
|
| +}
|
| +
|
| +// Offset returns the number of responses this query will skip before returning
|
| +// data, and a boolean indicating if the offset is set.
|
| +func (q *FinalizedQuery) Offset() (int32, bool) {
|
| + if q.offset != nil {
|
| + return *q.offset, true
|
| + }
|
| + return 0, false
|
| +}
|
| +
|
| +// Orders returns the sort orders that this query will use, including all orders
|
| +// implied by the projections, and the implicit __key__ order at the end.
|
| +func (q *FinalizedQuery) Orders() []IndexColumn {
|
| + ret := make([]IndexColumn, len(q.orders))
|
| + copy(ret, q.orders)
|
| + return ret
|
| +}
|
| +
|
| +// Bounds returns the start and end Cursors. One or both may be nil. The Cursors
|
| +// returned are implementation-specific depending on the actual RawInterface
|
| +// implementation and the filters installed (if the filters interfere with
|
| +// Cursor production).
|
| +func (q *FinalizedQuery) Bounds() (start, end Cursor) {
|
| + return q.start, q.end
|
| +}
|
| +
|
| +// Ancestor returns the ancestor filter key, if any. This is a convenience
|
| +// function for getting the value from EqFilters()["__ancestor__"].
|
| +func (q *FinalizedQuery) Ancestor() *Key {
|
| + if anc, ok := q.eqFilts["__ancestor__"]; ok {
|
| + return anc[0].Value().(*Key)
|
| + }
|
| + return nil
|
| +}
|
| +
|
| +// EqFilters returns all the equality filters. The map key is the field name
|
| +// and the PropertySlice is the values that field should equal.
|
| +//
|
| +// This includes a special equality filter on "__ancestor__". If "__ancestor__"
|
| +// is present in the result, it's guaranteed to have 1 value in the
|
| +// PropertySlice which is of type *Key.
|
| +func (q *FinalizedQuery) EqFilters() map[string]PropertySlice {
|
| + ret := make(map[string]PropertySlice, len(q.eqFilts))
|
| + for k, v := range q.eqFilts {
|
| + newV := make(PropertySlice, len(v))
|
| + copy(newV, v)
|
| + ret[k] = newV
|
| + }
|
| + return ret
|
| +}
|
| +
|
| +// IneqFilterProp returns the inequality filter property name, if one is used
|
| +// for this filter. An empty return value means that this query does not
|
| +// contain any inequality filters.
|
| +func (q *FinalizedQuery) IneqFilterProp() string {
|
| + return q.ineqFiltProp
|
| +}
|
| +
|
| +// IneqFilterLow returns the field name, operator and value for the low-side
|
| +// inequality filter. If the returned field name is "", it means that there's
|
| +// now lower inequality bound on this query.
|
| +//
|
| +// If field is non-empty, op may have the values ">" or ">=".
|
| +func (q *FinalizedQuery) IneqFilterLow() (field, op string, val Property) {
|
| + if q.ineqFiltLowSet {
|
| + field = q.ineqFiltProp
|
| + val = q.ineqFiltLow
|
| + op = ">"
|
| + if q.ineqFiltLowIncl {
|
| + op = ">="
|
| + }
|
| + }
|
| + return
|
| +}
|
| +
|
| +// IneqFilterHigh returns the field name, operator and value for the high-side
|
| +// inequality filter. If the returned field name is "", it means that there's
|
| +// now upper inequality bound on this query.
|
| +//
|
| +// If field is non-empty, op may have the values "<" or "<=".
|
| +func (q *FinalizedQuery) IneqFilterHigh() (field, op string, val Property) {
|
| + if q.ineqFiltHighSet {
|
| + field = q.ineqFiltProp
|
| + val = q.ineqFiltHigh
|
| + op = "<"
|
| + if q.ineqFiltHighIncl {
|
| + op = "<="
|
| + }
|
| + }
|
| + return
|
| +}
|
| +
|
| +var escaper = strings.NewReplacer(
|
| + "\\%", `\%`,
|
| + "\\_", `\_`,
|
| + "\\", `\\`,
|
| + "\x00", `\0`,
|
| + "\b", `\b`,
|
| + "\n", `\n`,
|
| + "\r", `\r`,
|
| + "\t", `\t`,
|
| + "\x1A", `\Z`,
|
| + "'", `\'`,
|
| + "\"", `\"`,
|
| + "`", "\\`",
|
| +)
|
| +
|
| +func gqlQuoteName(s string) string {
|
| + return fmt.Sprintf("`%s`", escaper.Replace(s))
|
| +}
|
| +
|
| +func gqlQuoteString(s string) string {
|
| + return fmt.Sprintf(`"%s"`, escaper.Replace(s))
|
| +}
|
| +
|
| +// GQL returns a correctly formatted Cloud Datastore GQL expression which
|
| +// is equivalent to this query.
|
| +//
|
| +// The flavor of GQL that this emits is defined here:
|
| +// https://cloud.google.com/datastore/docs/apis/gql/gql_reference
|
| +//
|
| +// NOTE: Cursors are omitted because currently there's currently no syntax for
|
| +// literal cursors.
|
| +//
|
| +// NOTE: GeoPoint values are emitted with speculated future syntax. There is
|
| +// currently no syntax for literal GeoPoint values.
|
| +func (q *FinalizedQuery) GQL() string {
|
| + ret := bytes.Buffer{}
|
| +
|
| + ws := func(s string) {
|
| + _, err := ret.WriteString(s)
|
| + if err != nil {
|
| + panic(err)
|
| + }
|
| + }
|
| +
|
| + ws("SELECT")
|
| + if len(q.project) != 0 {
|
| + if q.distinct {
|
| + ws(" DISTINCT")
|
| + }
|
| + proj := make([]string, len(q.project))
|
| + for i, p := range q.project {
|
| + proj[i] = gqlQuoteName(p)
|
| + }
|
| + ws(" ")
|
| + ws(strings.Join(proj, ", "))
|
| + } else {
|
| + ws(" *")
|
| + }
|
| +
|
| + if q.kind != "" {
|
| + fmt.Fprintf(&ret, " FROM %s", gqlQuoteName(q.kind))
|
| + }
|
| +
|
| + filts := []string(nil)
|
| + anc := Property{}
|
| + if len(q.eqFilts) > 0 {
|
| + eqProps := make([]string, 0, len(q.eqFilts))
|
| + for k, v := range q.eqFilts {
|
| + if k == "__ancestor__" {
|
| + anc = v[0]
|
| + continue
|
| + }
|
| + eqProps = append(eqProps, k)
|
| + }
|
| + sort.Strings(eqProps)
|
| + for _, k := range eqProps {
|
| + vals := q.eqFilts[k]
|
| + k = gqlQuoteName(k)
|
| + for _, v := range vals {
|
| + if v.Type() == PTNull {
|
| + filts = append(filts, fmt.Sprintf("%s IS NULL", k))
|
| + } else {
|
| + filts = append(filts, fmt.Sprintf("%s = %s", k, v.GQL()))
|
| + }
|
| + }
|
| + }
|
| + }
|
| + if q.ineqFiltProp != "" {
|
| + for _, f := range [](func() (p, op string, v Property)){q.IneqFilterLow, q.IneqFilterHigh} {
|
| + prop, op, v := f()
|
| + if prop != "" {
|
| + filts = append(filts, fmt.Sprintf("%s %s %s", gqlQuoteName(prop), op, v.GQL()))
|
| + }
|
| + }
|
| + }
|
| + if anc.propType != PTNull {
|
| + filts = append(filts, fmt.Sprintf("__key__ HAS ANCESTOR %s", anc.GQL()))
|
| + }
|
| + if len(filts) > 0 {
|
| + fmt.Fprintf(&ret, " WHERE %s", strings.Join(filts, " AND "))
|
| + }
|
| +
|
| + if len(q.orders) > 0 {
|
| + orders := make([]string, len(q.orders))
|
| + for i, col := range q.orders {
|
| + orders[i] = col.GQL()
|
| + }
|
| + fmt.Fprintf(&ret, " ORDER BY %s", strings.Join(orders, ", "))
|
| + }
|
| +
|
| + if q.limit != nil {
|
| + fmt.Fprintf(&ret, " LIMIT %d", *q.limit)
|
| + }
|
| + if q.offset != nil {
|
| + fmt.Fprintf(&ret, " OFFSET %d", *q.offset)
|
| + }
|
| +
|
| + return ret.String()
|
| +}
|
| +
|
| +func (q *FinalizedQuery) String() string {
|
| + // TODO(riannucci): make a more compact go-like representation here.
|
| + return q.GQL()
|
| +}
|
| +
|
| +// Valid returns true iff this FinalizedQuery is valid in the provided appID and
|
| +// namespace.
|
| +//
|
| +// This checks the ancestor filter (if any), as well as the inequality filters
|
| +// if they filter on '__key__'.
|
| +//
|
| +// In particular, it does NOT validate equality filters which happen to have
|
| +// values of type PTKey, nor does it validate inequality filters that happen to
|
| +// have values of type PTKey (but don't filter on the magic '__key__' field).
|
| +func (q *FinalizedQuery) Valid(aid, ns string) error {
|
| + anc := q.Ancestor()
|
| + if anc != nil && (!anc.Valid(false, aid, ns) || anc.Incomplete()) {
|
| + return ErrInvalidKey
|
| + }
|
| +
|
| + if q.ineqFiltProp == "__key__" {
|
| + if q.ineqFiltLowSet && !q.ineqFiltLow.Value().(*Key).Valid(false, aid, ns) {
|
| + return ErrInvalidKey
|
| + }
|
| + if q.ineqFiltHighSet && !q.ineqFiltHigh.Value().(*Key).Valid(false, aid, ns) {
|
| + return ErrInvalidKey
|
| + }
|
| + }
|
| + return nil
|
| +}
|
|
|