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  }