| Index: impl/memory/datastore_query_execution_test.go
 | 
| diff --git a/impl/memory/datastore_query_execution_test.go b/impl/memory/datastore_query_execution_test.go
 | 
| new file mode 100644
 | 
| index 0000000000000000000000000000000000000000..01e779d2335ead31c50a4d907bb22234d311ae86
 | 
| --- /dev/null
 | 
| +++ b/impl/memory/datastore_query_execution_test.go
 | 
| @@ -0,0 +1,359 @@
 | 
| +// 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 memory
 | 
| +
 | 
| +import (
 | 
| +	"fmt"
 | 
| +	"testing"
 | 
| +
 | 
| +	ds "github.com/luci/gae/service/datastore"
 | 
| +	"github.com/luci/gae/service/info"
 | 
| +	. "github.com/luci/luci-go/common/testing/assertions"
 | 
| +	. "github.com/smartystreets/goconvey/convey"
 | 
| +	"golang.org/x/net/context"
 | 
| +)
 | 
| +
 | 
| +type qExpect struct {
 | 
| +	q     ds.Query
 | 
| +	inTxn bool
 | 
| +
 | 
| +	get  []ds.PropertyMap
 | 
| +	keys []ds.Key
 | 
| +}
 | 
| +
 | 
| +type qExStage struct {
 | 
| +	addIdxs []*ds.IndexDefinition
 | 
| +	putEnts []ds.PropertyMap
 | 
| +	delEnts []ds.Key
 | 
| +
 | 
| +	expect []qExpect
 | 
| +
 | 
| +	extraFns []func(context.Context)
 | 
| +}
 | 
| +
 | 
| +type qExTest struct {
 | 
| +	name string
 | 
| +	test []qExStage
 | 
| +}
 | 
| +
 | 
| +var stage1Data = []ds.PropertyMap{
 | 
| +	pmap("$key", key("Kind", 1), NEXT,
 | 
| +		"Val", 1, 2, 3, NEXT,
 | 
| +		"Extra", "hello",
 | 
| +	),
 | 
| +	pmap("$key", key("Kind", 2), NEXT,
 | 
| +		"Val", 6, 8, 7, NEXT,
 | 
| +		"Extra", "zebra",
 | 
| +	),
 | 
| +	pmap("$key", key("Kind", 3), NEXT,
 | 
| +		"Val", 1, 2, 2, 100, NEXT,
 | 
| +		"Extra", "waffle",
 | 
| +	),
 | 
| +	pmap("$key", key("Kind", 6), NEXT,
 | 
| +		"Val", 5, NEXT,
 | 
| +		"Extra", "waffle",
 | 
| +	),
 | 
| +	pmap("$key", key("Child", "seven", key("Kind", 3)), NEXT,
 | 
| +		"Interesting", 28, NEXT,
 | 
| +		"Extra", "hello",
 | 
| +	),
 | 
| +	pmap("$key", key("Unique", 1), NEXT,
 | 
| +		"Derp", 39,
 | 
| +	),
 | 
| +}
 | 
| +
 | 
| +var stage2Data = []ds.PropertyMap{
 | 
| +	pmap("$key", key("Kind", 1, key("Kind", 3)), NEXT,
 | 
| +		"Val", 2, 4, 28, NEXT,
 | 
| +		"Extra", "hello", "waffle",
 | 
| +	),
 | 
| +	pmap("$key", key("Kind", 2, key("Kind", 3)), NEXT,
 | 
| +		"Val", 3, 4, NEXT,
 | 
| +		"Extra", "hello", "waffle",
 | 
| +	),
 | 
| +	pmap("$key", key("Kind", 3, key("Kind", 3)), NEXT,
 | 
| +		"Val", 3, 4, 2, 1, NEXT,
 | 
| +		"Extra", "nuts",
 | 
| +	),
 | 
| +}
 | 
| +
 | 
| +var queryExecutionTests = []qExTest{
 | 
| +	{"basic", []qExStage{
 | 
| +		{
 | 
| +			addIdxs: []*ds.IndexDefinition{
 | 
| +				indx("Unrelated", "-thing", "bob", "-__key__"),
 | 
| +				indx("Wat", "deep", "opt", "other"),
 | 
| +				indx("Wat", "meep", "opt", "other"),
 | 
| +			},
 | 
| +		},
 | 
| +
 | 
| +		{
 | 
| +			expect: []qExpect{
 | 
| +				// tests the case where the query has indexes to fulfill it, but there
 | 
| +				// are no actual entities in the datastore.
 | 
| +				{q: nq("Wat").Filter("meep =", 1).Filter("deep =", 2).Order("opt").Order("other"),
 | 
| +					get: []ds.PropertyMap{}},
 | 
| +			},
 | 
| +		},
 | 
| +
 | 
| +		{
 | 
| +			putEnts: stage1Data,
 | 
| +			expect: []qExpect{
 | 
| +				{q: nq("Kind"), get: []ds.PropertyMap{}},
 | 
| +				{q: nq("Child").Ancestor(key("Kind", 3)), keys: []ds.Key{
 | 
| +					key("Child", "seven", key("Kind", 3)),
 | 
| +				}},
 | 
| +			},
 | 
| +		},
 | 
| +
 | 
| +		{
 | 
| +			putEnts: stage2Data,
 | 
| +			delEnts: []ds.Key{key("Unique", 1)},
 | 
| +			addIdxs: []*ds.IndexDefinition{
 | 
| +				indx("Kind!", "-Extra", "-Val"),
 | 
| +				indx("Kind!", "-Extra", "-Val", "-__key__"),
 | 
| +				indx("Kind!", "Bogus", "Extra", "-Val"),
 | 
| +			},
 | 
| +			expect: []qExpect{
 | 
| +				{q: nq("Kind"), get: stage1Data[:4]},
 | 
| +
 | 
| +				{q: nq("Kind").Offset(2).Limit(1), get: []ds.PropertyMap{
 | 
| +					stage1Data[2],
 | 
| +				}},
 | 
| +
 | 
| +				{q: nq("Missing"), get: []ds.PropertyMap{}},
 | 
| +
 | 
| +				{q: nq("Missing").Filter("Id <", 2).Filter("Id >", 2), get: []ds.PropertyMap{}},
 | 
| +
 | 
| +				{q: nq("Missing").Filter("Bogus =", 3), get: []ds.PropertyMap{}},
 | 
| +
 | 
| +				{q: nq("Kind").Filter("Extra =", "waffle"), get: []ds.PropertyMap{
 | 
| +					stage1Data[2], stage1Data[3],
 | 
| +				}},
 | 
| +
 | 
| +				// get ziggy with it
 | 
| +				{q: nq("Kind").Filter("Extra =", "waffle").Filter("Val =", 100), get: []ds.PropertyMap{
 | 
| +					stage1Data[2],
 | 
| +				}},
 | 
| +				{q: nq("Child").Filter("Interesting =", 28).Filter("Extra =", "hello"), get: []ds.PropertyMap{
 | 
| +					stage1Data[4],
 | 
| +				}},
 | 
| +
 | 
| +				{q: (nq("Kind").Ancestor(key("Kind", 3)).Order("Val").
 | 
| +					Start(curs("Val", 1, "__key__", key("Kind", 3))).
 | 
| +					End(curs("Val", 90, "__key__", key("Zeta", "woot", key("Kind", 3))))), keys: []ds.Key{},
 | 
| +				},
 | 
| +
 | 
| +				{q: nq("Kind").Filter("Val >", 2).Filter("Val <=", 5), get: []ds.PropertyMap{
 | 
| +					stage1Data[0], stage1Data[3],
 | 
| +				}},
 | 
| +
 | 
| +				{q: nq("Kind").Filter("Val >", 2).Filter("Val <=", 5).Order("-Val"), get: []ds.PropertyMap{
 | 
| +					stage1Data[3], stage1Data[0],
 | 
| +				}},
 | 
| +
 | 
| +				{q: nq("").Filter("__key__ >", key("Kind", 2)), get: []ds.PropertyMap{
 | 
| +					// TODO(riannucci): determine if the real datastore shows metadata
 | 
| +					// during kindless queries. The documentation seems to imply so, but
 | 
| +					// I'd like to be sure.
 | 
| +					pmap("$key", key("__entity_group__", 1, key("Kind", 2)), NEXT,
 | 
| +						"__version__", 1),
 | 
| +					stage1Data[2],
 | 
| +					stage1Data[4],
 | 
| +					// this is 5 because the value is retrieved from HEAD and not from
 | 
| +					// the index snapshot!
 | 
| +					pmap("$key", key("__entity_group__", 1, key("Kind", 3)), NEXT,
 | 
| +						"__version__", 5),
 | 
| +					stage1Data[3],
 | 
| +					pmap("$key", key("__entity_group__", 1, key("Kind", 6)), NEXT,
 | 
| +						"__version__", 1),
 | 
| +					pmap("$key", key("__entity_group__", 1, key("Unique", 1)), NEXT,
 | 
| +						"__version__", 2),
 | 
| +				}},
 | 
| +
 | 
| +				{q: (nq("Kind").
 | 
| +					Filter("Val >", 2).Filter("Extra =", "waffle").
 | 
| +					Order("-Val").
 | 
| +					Ancestor(key("Kind", 3))),
 | 
| +					get: []ds.PropertyMap{
 | 
| +						stage1Data[2],
 | 
| +						stage2Data[0],
 | 
| +						stage2Data[1],
 | 
| +					}},
 | 
| +
 | 
| +				{q: (nq("Kind").
 | 
| +					Filter("Val >", 2).Filter("Extra =", "waffle").
 | 
| +					Order("-Val").Order("-__key__").
 | 
| +					Ancestor(key("Kind", 3))),
 | 
| +					get: []ds.PropertyMap{
 | 
| +						stage1Data[2],
 | 
| +						stage2Data[0],
 | 
| +						stage2Data[1],
 | 
| +					}},
 | 
| +
 | 
| +				{q: (nq("Kind").
 | 
| +					Filter("Val >", 2).Filter("Extra =", "waffle").
 | 
| +					Order("-Val").
 | 
| +					Ancestor(key("Kind", 3)).Project("Val")),
 | 
| +					get: []ds.PropertyMap{
 | 
| +						pmap("$key", key("Kind", 3), NEXT,
 | 
| +							"Val", 100),
 | 
| +						pmap("$key", key("Kind", 1, key("Kind", 3)), NEXT,
 | 
| +							"Val", 28),
 | 
| +						pmap("$key", key("Kind", 1, key("Kind", 3)), NEXT,
 | 
| +							"Val", 4),
 | 
| +						pmap("$key", key("Kind", 2, key("Kind", 3)), NEXT,
 | 
| +							"Val", 4),
 | 
| +						pmap("$key", key("Kind", 2, key("Kind", 3)), NEXT,
 | 
| +							"Val", 3),
 | 
| +					}},
 | 
| +
 | 
| +				{q: (nq("Kind").
 | 
| +					Filter("Val >", 2).Filter("Extra =", "waffle").
 | 
| +					Order("-Val").
 | 
| +					Ancestor(key("Kind", 3)).Project("Val").Distinct()),
 | 
| +					get: []ds.PropertyMap{
 | 
| +						pmap("$key", key("Kind", 3), NEXT,
 | 
| +							"Val", 100),
 | 
| +						pmap("$key", key("Kind", 1, key("Kind", 3)), NEXT,
 | 
| +							"Val", 28),
 | 
| +						pmap("$key", key("Kind", 1, key("Kind", 3)), NEXT,
 | 
| +							"Val", 4),
 | 
| +						pmap("$key", key("Kind", 2, key("Kind", 3)), NEXT,
 | 
| +							"Val", 3),
 | 
| +					}},
 | 
| +			},
 | 
| +
 | 
| +			extraFns: []func(context.Context){
 | 
| +				func(c context.Context) {
 | 
| +					data := ds.Get(c)
 | 
| +					curs := ds.Cursor(nil)
 | 
| +
 | 
| +					q := nq("").Filter("__key__ >", key("Kind", 2))
 | 
| +
 | 
| +					err := data.Run(q, func(pm ds.PropertyMap, gc ds.CursorCB) bool {
 | 
| +						So(pm, ShouldResemble, pmap(
 | 
| +							"$key", key("__entity_group__", 1, key("Kind", 2)), NEXT,
 | 
| +							"__version__", 1))
 | 
| +
 | 
| +						err := error(nil)
 | 
| +						curs, err = gc()
 | 
| +						So(err, ShouldBeNil)
 | 
| +						return false
 | 
| +					})
 | 
| +					So(err, ShouldBeNil)
 | 
| +
 | 
| +					err = data.Run(q.Start(curs), func(pm ds.PropertyMap, gc ds.CursorCB) bool {
 | 
| +						So(pm, ShouldResemble, stage1Data[2])
 | 
| +						return false
 | 
| +					})
 | 
| +					So(err, ShouldBeNil)
 | 
| +				},
 | 
| +
 | 
| +				func(c context.Context) {
 | 
| +					data := ds.Get(c)
 | 
| +					q := nq("Something").Filter("Does =", 2).Order("Not").Order("Work")
 | 
| +					So(data.Run(q, func(ds.Key, ds.CursorCB) bool {
 | 
| +						return true
 | 
| +					}), ShouldErrLike, "Try adding:\n  C:Something/Does/Not/Work")
 | 
| +				},
 | 
| +			},
 | 
| +		},
 | 
| +
 | 
| +		{
 | 
| +			expect: []qExpect{
 | 
| +				// eventual consistency; Unique/1 is deleted at HEAD. Keysonly finds it,
 | 
| +				// but 'normal' doesn't.
 | 
| +				{q: nq("Unique").Filter("__key__ >", key("AKind", 5)).Filter("__key__ <=", key("Zeta", "prime")),
 | 
| +					keys: []ds.Key{key("Unique", 1)},
 | 
| +					get:  []ds.PropertyMap{}},
 | 
| +
 | 
| +				{q: nq("Kind").Filter("Val =", 1).Filter("Val =", 3), get: []ds.PropertyMap{
 | 
| +					stage1Data[0], stage2Data[2],
 | 
| +				}},
 | 
| +			},
 | 
| +		},
 | 
| +	}},
 | 
| +}
 | 
| +
 | 
| +func TestQueryExecution(t *testing.T) {
 | 
| +	t.Parallel()
 | 
| +
 | 
| +	Convey("Test query execution", t, func() {
 | 
| +		c, err := info.Get(Use(context.Background())).Namespace("ns")
 | 
| +		if err != nil {
 | 
| +			panic(err)
 | 
| +		}
 | 
| +
 | 
| +		data := ds.Get(c)
 | 
| +		testing := data.Raw().Testable()
 | 
| +
 | 
| +		for _, tc := range queryExecutionTests {
 | 
| +			Convey(tc.name, func() {
 | 
| +				for i, stage := range tc.test {
 | 
| +					// outside of Convey, since these must always happen
 | 
| +					testing.CatchupIndexes()
 | 
| +
 | 
| +					testing.AddIndexes(stage.addIdxs...)
 | 
| +					if err := data.PutMulti(stage.putEnts); err != nil {
 | 
| +						// prevent Convey from thinking this assertion should show up in
 | 
| +						// every test loop.
 | 
| +						panic(err)
 | 
| +					}
 | 
| +
 | 
| +					if err := data.DeleteMulti(stage.delEnts); err != nil {
 | 
| +						panic(err)
 | 
| +					}
 | 
| +
 | 
| +					Convey(fmt.Sprintf("stage %d", i), func() {
 | 
| +						for j, expect := range stage.expect {
 | 
| +							runner := func(f func(ic context.Context) error, _ *ds.TransactionOptions) error {
 | 
| +								return f(c)
 | 
| +							}
 | 
| +							if expect.inTxn {
 | 
| +								runner = data.RunInTransaction
 | 
| +							}
 | 
| +
 | 
| +							if expect.keys != nil {
 | 
| +								runner(func(c context.Context) error {
 | 
| +									data := ds.Get(c)
 | 
| +									Convey(fmt.Sprintf("expect %d (keys)", j), func() {
 | 
| +										rslt := []ds.Key(nil)
 | 
| +										So(data.GetAll(expect.q, &rslt), ShouldBeNil)
 | 
| +										So(len(rslt), ShouldEqual, len(expect.keys))
 | 
| +										for i, r := range rslt {
 | 
| +											So(r, ShouldResemble, expect.keys[i])
 | 
| +										}
 | 
| +									})
 | 
| +									return nil
 | 
| +								}, &ds.TransactionOptions{XG: true})
 | 
| +							}
 | 
| +
 | 
| +							if expect.get != nil {
 | 
| +								Convey(fmt.Sprintf("expect %d (data)", j), func() {
 | 
| +									runner(func(c context.Context) error {
 | 
| +										rslt := []ds.PropertyMap(nil)
 | 
| +										So(data.GetAll(expect.q, &rslt), ShouldBeNil)
 | 
| +										So(len(rslt), ShouldEqual, len(expect.get))
 | 
| +										for i, r := range rslt {
 | 
| +											So(r, ShouldResemble, expect.get[i])
 | 
| +										}
 | 
| +										return nil
 | 
| +									}, &ds.TransactionOptions{XG: true})
 | 
| +								})
 | 
| +							}
 | 
| +						}
 | 
| +
 | 
| +						for j, fn := range stage.extraFns {
 | 
| +							Convey(fmt.Sprintf("extraFn %d", j), func() {
 | 
| +								fn(c)
 | 
| +							})
 | 
| +						}
 | 
| +					})
 | 
| +				}
 | 
| +			})
 | 
| +		}
 | 
| +	})
 | 
| +}
 | 
| 
 |