go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/gae/impl/memory/datastore_query_test.go (about) 1 // Copyright 2015 The LUCI 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 // http://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 memory 16 17 import ( 18 "bytes" 19 "testing" 20 21 "go.chromium.org/luci/common/data/cmpbin" 22 "go.chromium.org/luci/common/data/stringset" 23 24 dstore "go.chromium.org/luci/gae/service/datastore" 25 26 . "github.com/smartystreets/goconvey/convey" 27 . "go.chromium.org/luci/common/testing/assertions" 28 ) 29 30 type sillyCursor string 31 32 func (s sillyCursor) String() string { return string(s) } 33 34 func curs(pairs ...any) queryCursor { 35 if len(pairs)%2 != 0 { 36 panic("curs() takes only even pairs") 37 } 38 pre := &bytes.Buffer{} 39 if _, err := cmpbin.WriteUint(pre, uint64(len(pairs)/2)); err != nil { 40 panic(err) 41 } 42 post := cmpbin.Invertible(&bytes.Buffer{}) 43 for i := 0; i < len(pairs); i += 2 { 44 k, v := pairs[i].(string), pairs[i+1] 45 46 col, err := dstore.ParseIndexColumn(k) 47 if err != nil { 48 panic(err) 49 } 50 51 post.SetInvert(col.Descending) 52 if err := dstore.Serialize.IndexColumn(pre, col); err != nil { 53 panic(err) 54 } 55 if err := dstore.Serialize.Property(post, prop(v)); err != nil { 56 panic(err) 57 } 58 } 59 return queryCursor(cmpbin.ConcatBytes(pre.Bytes(), post.Bytes())) 60 } 61 62 type queryTest struct { 63 // name is the name of the test case 64 name string 65 66 // q is the input query 67 q *dstore.Query 68 69 // err is the error to expect after prepping the query (error, string or nil) 70 err any 71 72 // equivalentQuery is another query which ShouldResemble q. This is useful to 73 // see the effects of redundancy pruning on e.g. filters. 74 equivalentQuery *reducedQuery 75 } 76 77 var queryTests = []queryTest{ 78 {"bad cursors (empty)", 79 nq().Start(queryCursor("")), 80 "invalid cursor", nil}, 81 82 {"bad cursors (nil)", 83 nq().Start(queryCursor("")), 84 "invalid cursor", nil}, 85 86 {"bad cursors (no key)", 87 nq().End(curs("Foo", 100)), 88 "invalid cursor", nil}, 89 90 // TODO(riannucci): exclude cursors which are out-of-bounds with inequality? 91 // I think right now you could have a query for > 10 with a start cursor of 1. 92 {"bad cursors (doesn't include ineq)", 93 nq().Gt("Bob", 10).Start( 94 curs("Foo", 100, "__key__", key("something", 1)), 95 ), 96 "start cursor is invalid", nil}, 97 98 {"bad cursors (doesn't include all orders)", 99 nq().Order("Luci").Order("Charliene").Start( 100 curs("Luci", 100, "__key__", key("something", 1)), 101 ), 102 "start cursor is invalid", nil}, 103 104 {"cursor bad type", 105 nq().Order("Luci").End(sillyCursor("I am a banana")), 106 "bad cursor type", nil}, 107 108 {"overconstrained inequality (>= v <)", 109 nq().Gte("bob", 10).Lt("bob", 10), 110 dstore.ErrNullQuery, nil}, 111 112 {"overconstrained inequality (> v <)", 113 nq().Gt("bob", 10).Lt("bob", 10), 114 dstore.ErrNullQuery, nil}, 115 116 {"overconstrained inequality (> v <=)", 117 nq().Gt("bob", 10).Lte("bob", 10), 118 dstore.ErrNullQuery, nil}, 119 120 {"silly inequality (=> v <=)", 121 nq().Gte("bob", 10).Lte("bob", 10), 122 nil, nil}, 123 124 {"cursors get smooshed into the inquality range", 125 (nq().Gt("Foo", 3).Lt("Foo", 10). 126 Start(curs("Foo", 2, "__key__", key("Something", 1))). 127 End(curs("Foo", 20, "__key__", key("Something", 20)))), 128 nil, 129 &reducedQuery{ 130 dstore.MkKeyContext("dev~app", "ns"), 131 "Foo", map[string]stringset.Set{}, []dstore.IndexColumn{ 132 {Property: "Foo"}, 133 {Property: "__key__"}, 134 }, 135 increment(dstore.Serialize.ToBytes(dstore.MkProperty(3))), 136 dstore.Serialize.ToBytes(dstore.MkProperty(10)), 137 2, 138 }}, 139 140 {"cursors could cause the whole query to be useless", 141 (nq().Gt("Foo", 3).Lt("Foo", 10). 142 Start(curs("Foo", 200, "__key__", key("Something", 1))). 143 End(curs("Foo", 1, "__key__", key("Something", 20)))), 144 dstore.ErrNullQuery, 145 nil}, 146 } 147 148 func TestQueries(t *testing.T) { 149 t.Parallel() 150 151 Convey("queries have tons of condition checking", t, func() { 152 kc := dstore.MkKeyContext("dev~app", "ns") 153 154 Convey("non-ancestor queries in a transaction", func() { 155 fq, err := nq().Finalize() 156 So(err, ShouldErrLike, nil) 157 _, err = reduce(fq, kc, true) 158 So(err, ShouldErrLike, "must include an Ancestor filter") 159 }) 160 161 Convey("non-ancestor queries work in firestore mode in a transaction", func() { 162 fq, err := nq().FirestoreMode(true).EventualConsistency(true).Finalize() 163 So(err, ShouldErrLike, nil) 164 _, err = reduce(fq, kc, true) 165 So(err, ShouldErrLike, nil) 166 }) 167 168 Convey("absurd numbers of filters are prohibited", func() { 169 q := nq().Ancestor(key("thing", "wat")) 170 for i := 0; i < 100; i++ { 171 q = q.Eq("something", i) 172 } 173 fq, err := q.Finalize() 174 So(err, ShouldErrLike, nil) 175 _, err = reduce(fq, kc, false) 176 So(err, ShouldErrLike, "query is too large") 177 }) 178 179 Convey("bulk check", func() { 180 for _, tc := range queryTests { 181 Convey(tc.name, func() { 182 rq := (*reducedQuery)(nil) 183 fq, err := tc.q.Finalize() 184 if err == nil { 185 err = fq.Valid(kc) 186 if err == nil { 187 rq, err = reduce(fq, kc, false) 188 } 189 } 190 So(err, ShouldErrLike, tc.err) 191 192 if tc.equivalentQuery != nil { 193 So(rq, ShouldResemble, tc.equivalentQuery) 194 } 195 }) 196 } 197 }) 198 }) 199 }