go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/gae/impl/memory/datastore_index_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  	"sort"
    19  	"testing"
    20  	"time"
    21  
    22  	ds "go.chromium.org/luci/gae/service/datastore"
    23  
    24  	. "github.com/smartystreets/goconvey/convey"
    25  )
    26  
    27  var fakeKey = key("parentKind", "sid", "knd", 10)
    28  
    29  var rgenComplexTime = time.Date(
    30  	1986, time.October, 26, 1, 20, 00, 00, time.UTC)
    31  var rgenComplexKey = key("kind", "id")
    32  
    33  var _, rgenComplexTimeInt = prop(rgenComplexTime).IndexTypeAndValue()
    34  var rgenComplexTimeIdx = prop(rgenComplexTimeInt)
    35  
    36  var rowGenTestCases = []struct {
    37  	name        string
    38  	pmap        ds.PropertyMap
    39  	withBuiltin bool
    40  	idxs        []*ds.IndexDefinition
    41  
    42  	// These are checked in TestIndexRowGen. nil to skip test case.
    43  	expected []ds.IndexedPropertySlice
    44  
    45  	// just the collections you want to assert. These are checked in
    46  	// TestIndexEntries. nil to skip test case.
    47  	collections map[string][][]byte
    48  }{
    49  
    50  	{
    51  		name: "simple including builtins",
    52  		pmap: ds.PropertyMap{
    53  			"wat":  ds.PropertySlice{propNI("thing"), prop("hat"), prop(100)},
    54  			"nerd": prop(103.7),
    55  			"spaz": propNI(false),
    56  		},
    57  		withBuiltin: true,
    58  		idxs: []*ds.IndexDefinition{
    59  			indx("knd", "-wat", "nerd"),
    60  		},
    61  		expected: []ds.IndexedPropertySlice{
    62  			{cat(prop(fakeKey))},              // B:knd
    63  			{cat(prop(103.7), prop(fakeKey))}, // B:knd/nerd
    64  			{ // B:knd/wat
    65  				cat(prop(100), prop(fakeKey)),
    66  				cat(prop("hat"), prop(fakeKey)),
    67  			},
    68  			{ // B:knd/-nerd
    69  				cat(icat(prop(103.7)), prop(fakeKey)),
    70  			},
    71  			{ // B:knd/-wat
    72  				cat(icat(prop("hat")), prop(fakeKey)),
    73  				cat(icat(prop(100)), prop(fakeKey)),
    74  			},
    75  			{ // C:knd/-wat/nerd
    76  				cat(icat(prop("hat")), prop(103.7), prop(fakeKey)),
    77  				cat(icat(prop(100)), prop(103.7), prop(fakeKey)),
    78  			},
    79  		},
    80  
    81  		collections: map[string][][]byte{
    82  			"idx": {
    83  				cat("knd", byte(0), byte(1), byte(0), "__key__", byte(0)),
    84  				cat("knd", byte(0), byte(1), byte(0), "__key__", byte(1), byte(0), "nerd", byte(0)),
    85  				cat("knd", byte(0), byte(1), byte(0), "__key__", byte(1), byte(0), "nerd", byte(1), byte(1), "wat", byte(0)),
    86  				cat("knd", byte(0), byte(1), byte(0), "__key__", byte(1), byte(0), "wat", byte(0)),
    87  				cat("knd", byte(0), byte(1), byte(0), "__key__", byte(1), byte(1), "nerd", byte(0)),
    88  				cat("knd", byte(0), byte(1), byte(0), "__key__", byte(1), byte(1), "wat", byte(0)),
    89  			},
    90  			"idx:ns:" + sat(indx("knd").PrepForIdxTable()): {
    91  				cat(prop(fakeKey)),
    92  			},
    93  			"idx:ns:" + sat(indx("knd", "wat").PrepForIdxTable()): {
    94  				cat(prop(100), prop(fakeKey)),
    95  				cat(prop("hat"), prop(fakeKey)),
    96  			},
    97  			"idx:ns:" + sat(indx("knd", "-wat").PrepForIdxTable()): {
    98  				cat(icat(prop("hat")), prop(fakeKey)),
    99  				cat(icat(prop(100)), prop(fakeKey)),
   100  			},
   101  		},
   102  	},
   103  
   104  	{
   105  		name: "complex",
   106  		pmap: ds.PropertyMap{
   107  			"yerp": ds.PropertySlice{prop("hat"), prop(73.9)},
   108  			"wat": ds.PropertySlice{
   109  				prop(rgenComplexTime),
   110  				prop([]byte("value")),
   111  				prop(rgenComplexKey)},
   112  			"spaz": ds.PropertySlice{prop(nil), prop(false), prop(true)},
   113  		},
   114  		idxs: []*ds.IndexDefinition{
   115  			indx("knd", "-wat", "nerd", "spaz"), // doesn't match, so empty
   116  			indx("knd", "yerp", "-wat", "spaz"),
   117  		},
   118  		expected: []ds.IndexedPropertySlice{
   119  			{}, // C:knd/-wat/nerd/spaz, no match
   120  			{ // C:knd/yerp/-wat/spaz
   121  				// thank goodness the binary serialization only happens 1/val in the
   122  				// real code :).
   123  				cat(prop("hat"), icat(prop(rgenComplexKey)), prop(nil), prop(fakeKey)),
   124  				cat(prop("hat"), icat(prop(rgenComplexKey)), prop(false), prop(fakeKey)),
   125  				cat(prop("hat"), icat(prop(rgenComplexKey)), prop(true), prop(fakeKey)),
   126  				cat(prop("hat"), icat(prop("value")), prop(nil), prop(fakeKey)),
   127  				cat(prop("hat"), icat(prop("value")), prop(false), prop(fakeKey)),
   128  				cat(prop("hat"), icat(prop("value")), prop(true), prop(fakeKey)),
   129  				cat(prop("hat"), icat(rgenComplexTimeIdx), prop(nil), prop(fakeKey)),
   130  				cat(prop("hat"), icat(rgenComplexTimeIdx), prop(false), prop(fakeKey)),
   131  				cat(prop("hat"), icat(rgenComplexTimeIdx), prop(true), prop(fakeKey)),
   132  
   133  				cat(prop(73.9), icat(prop(rgenComplexKey)), prop(nil), prop(fakeKey)),
   134  				cat(prop(73.9), icat(prop(rgenComplexKey)), prop(false), prop(fakeKey)),
   135  				cat(prop(73.9), icat(prop(rgenComplexKey)), prop(true), prop(fakeKey)),
   136  				cat(prop(73.9), icat(prop("value")), prop(nil), prop(fakeKey)),
   137  				cat(prop(73.9), icat(prop("value")), prop(false), prop(fakeKey)),
   138  				cat(prop(73.9), icat(prop("value")), prop(true), prop(fakeKey)),
   139  				cat(prop(73.9), icat(rgenComplexTimeIdx), prop(nil), prop(fakeKey)),
   140  				cat(prop(73.9), icat(rgenComplexTimeIdx), prop(false), prop(fakeKey)),
   141  				cat(prop(73.9), icat(rgenComplexTimeIdx), prop(true), prop(fakeKey)),
   142  			},
   143  		},
   144  	},
   145  
   146  	{
   147  		name: "ancestor",
   148  		pmap: ds.PropertyMap{
   149  			"wat": prop("sup"),
   150  		},
   151  		idxs: []*ds.IndexDefinition{
   152  			indx("knd!", "wat"),
   153  		},
   154  		collections: map[string][][]byte{
   155  			"idx:ns:" + sat(indx("knd!", "wat").PrepForIdxTable()): {
   156  				cat(prop(fakeKey.Parent()), prop("sup"), prop(fakeKey)),
   157  				cat(prop(fakeKey), prop("sup"), prop(fakeKey)),
   158  			},
   159  		},
   160  	},
   161  }
   162  
   163  func TestIndexRowGen(t *testing.T) {
   164  	t.Parallel()
   165  
   166  	Convey("Test Index Row Generation", t, func() {
   167  		for _, tc := range rowGenTestCases {
   168  			if tc.expected == nil {
   169  				Convey(tc.name, nil) // shows up as 'skipped'
   170  				continue
   171  			}
   172  
   173  			Convey(tc.name, func() {
   174  				mvals := ds.Serialize.IndexedProperties(fakeKey, tc.pmap)
   175  				idxs := []*ds.IndexDefinition(nil)
   176  				if tc.withBuiltin {
   177  					idxs = append(defaultIndexes("coolKind", mvals), tc.idxs...)
   178  				} else {
   179  					idxs = tc.idxs
   180  				}
   181  
   182  				m := matcher{}
   183  				for i, idx := range idxs {
   184  					Convey(idx.String(), func() {
   185  						iGen, ok := m.match(idx.GetFullSortOrder(), mvals)
   186  						if len(tc.expected[i]) > 0 {
   187  							So(ok, ShouldBeTrue)
   188  							actual := make(ds.IndexedPropertySlice, 0, len(tc.expected[i]))
   189  							iGen.permute(func(row, _ []byte) {
   190  								actual = append(actual, row)
   191  							})
   192  							So(len(actual), ShouldEqual, len(tc.expected[i]))
   193  							sort.Sort(actual)
   194  							for j, act := range actual {
   195  								So(act, ShouldResemble, tc.expected[i][j])
   196  							}
   197  						} else {
   198  							So(ok, ShouldBeFalse)
   199  						}
   200  					})
   201  				}
   202  			})
   203  		}
   204  	})
   205  
   206  	Convey("default indexes", t, func() {
   207  		Convey("nil collated", func() {
   208  			Convey("defaultIndexes (nil)", func() {
   209  				idxs := defaultIndexes("knd", nil)
   210  				So(len(idxs), ShouldEqual, 1)
   211  				So(idxs[0].String(), ShouldEqual, "B:knd")
   212  			})
   213  
   214  			Convey("indexEntries", func() {
   215  				sip := ds.Serialize.IndexedProperties(fakeKey, nil)
   216  				s := indexEntries(fakeKey, sip, defaultIndexes("knd", nil))
   217  				So(countItems(s.Snapshot().GetCollection("idx")), ShouldEqual, 1)
   218  				itm := s.GetCollection("idx").MinItem()
   219  				So(itm.key, ShouldResemble, cat(indx("knd").PrepForIdxTable()))
   220  				So(countItems(s.Snapshot().GetCollection("idx:ns:"+string(itm.key))), ShouldEqual, 1)
   221  			})
   222  
   223  			Convey("defaultIndexes", func() {
   224  				pm := ds.PropertyMap{
   225  					"wat":  ds.PropertySlice{propNI("thing"), prop("hat"), prop(100)},
   226  					"nerd": prop(103.7),
   227  					"spaz": propNI(false),
   228  				}
   229  				idxs := defaultIndexes("knd", ds.Serialize.IndexedProperties(fakeKey, pm))
   230  				So(len(idxs), ShouldEqual, 5)
   231  				So(idxs[0].String(), ShouldEqual, "B:knd")
   232  				So(idxs[1].String(), ShouldEqual, "B:knd/nerd")
   233  				So(idxs[2].String(), ShouldEqual, "B:knd/wat")
   234  				So(idxs[3].String(), ShouldEqual, "B:knd/-nerd")
   235  				So(idxs[4].String(), ShouldEqual, "B:knd/-wat")
   236  			})
   237  
   238  		})
   239  	})
   240  }
   241  
   242  func TestIndexEntries(t *testing.T) {
   243  	t.Parallel()
   244  
   245  	Convey("Test indexEntriesWithBuiltins", t, func() {
   246  		for _, tc := range rowGenTestCases {
   247  			if tc.collections == nil {
   248  				Convey(tc.name, nil) // shows up as 'skipped'
   249  				continue
   250  			}
   251  
   252  			Convey(tc.name, func() {
   253  				store := (memStore)(nil)
   254  				if tc.withBuiltin {
   255  					store = indexEntriesWithBuiltins(fakeKey, tc.pmap, tc.idxs)
   256  				} else {
   257  					sip := ds.Serialize.IndexedProperties(fakeKey, tc.pmap)
   258  					store = indexEntries(fakeKey, sip, tc.idxs)
   259  				}
   260  				for colName, vals := range tc.collections {
   261  					i := 0
   262  					coll := store.Snapshot().GetCollection(colName)
   263  					So(countItems(coll), ShouldEqual, len(tc.collections[colName]))
   264  
   265  					coll.ForEachItem(func(k, _ []byte) bool {
   266  						So(k, ShouldResemble, vals[i])
   267  						i++
   268  						return true
   269  					})
   270  					So(i, ShouldEqual, len(vals))
   271  				}
   272  			})
   273  		}
   274  	})
   275  }
   276  
   277  type dumbItem struct {
   278  	key   *ds.Key
   279  	props ds.PropertyMap
   280  }
   281  
   282  var updateIndexesTests = []struct {
   283  	name     string
   284  	idxs     []*ds.IndexDefinition
   285  	data     []dumbItem
   286  	expected map[string][][]byte
   287  }{
   288  
   289  	{
   290  		name: "basic",
   291  		data: []dumbItem{
   292  			{key("knd", 1), ds.PropertyMap{
   293  				"wat":  prop(10),
   294  				"yerp": prop(10)},
   295  			},
   296  			{key("knd", 10), ds.PropertyMap{
   297  				"wat":  prop(1),
   298  				"yerp": prop(200)},
   299  			},
   300  			{key("knd", 1), ds.PropertyMap{
   301  				"wat":  prop(10),
   302  				"yerp": prop(202)},
   303  			},
   304  		},
   305  		expected: map[string][][]byte{
   306  			"idx:ns:" + sat(indx("knd", "wat").PrepForIdxTable()): {
   307  				cat(prop(1), prop(key("knd", 10))),
   308  				cat(prop(10), prop(key("knd", 1))),
   309  			},
   310  			"idx:ns:" + sat(indx("knd", "-wat").PrepForIdxTable()): {
   311  				cat(icat(prop(10)), prop(key("knd", 1))),
   312  				cat(icat(prop(1)), prop(key("knd", 10))),
   313  			},
   314  			"idx:ns:" + sat(indx("knd", "yerp").PrepForIdxTable()): {
   315  				cat(prop(200), prop(key("knd", 10))),
   316  				cat(prop(202), prop(key("knd", 1))),
   317  			},
   318  		},
   319  	},
   320  
   321  	{
   322  		name: "compound",
   323  		idxs: []*ds.IndexDefinition{indx("knd", "yerp", "-wat")},
   324  		data: []dumbItem{
   325  			{key("knd", 1), ds.PropertyMap{
   326  				"wat":  prop(10),
   327  				"yerp": prop(100)},
   328  			},
   329  			{key("knd", 10), ds.PropertyMap{
   330  				"wat":  prop(1),
   331  				"yerp": prop(200)},
   332  			},
   333  			{key("knd", 11), ds.PropertyMap{
   334  				"wat":  prop(20),
   335  				"yerp": prop(200)},
   336  			},
   337  			{key("knd", 14), ds.PropertyMap{
   338  				"wat":  prop(20),
   339  				"yerp": prop(200)},
   340  			},
   341  			{key("knd", 1), ds.PropertyMap{
   342  				"wat":  prop(10),
   343  				"yerp": prop(202)},
   344  			},
   345  		},
   346  		expected: map[string][][]byte{
   347  			"idx:ns:" + sat(indx("knd", "yerp", "-wat").PrepForIdxTable()): {
   348  				cat(prop(200), icat(prop(20)), prop(key("knd", 11))),
   349  				cat(prop(200), icat(prop(20)), prop(key("knd", 14))),
   350  				cat(prop(200), icat(prop(1)), prop(key("knd", 10))),
   351  				cat(prop(202), icat(prop(10)), prop(key("knd", 1))),
   352  			},
   353  		},
   354  	},
   355  }
   356  
   357  func TestUpdateIndexes(t *testing.T) {
   358  	t.Parallel()
   359  
   360  	Convey("Test updateIndexes", t, func() {
   361  		for _, tc := range updateIndexesTests {
   362  			Convey(tc.name, func() {
   363  				store := newMemStore()
   364  				idxColl := store.GetOrCreateCollection("idx")
   365  				for _, i := range tc.idxs {
   366  					idxColl.Set(cat(i.PrepForIdxTable()), []byte{})
   367  				}
   368  
   369  				tmpLoader := map[string]ds.PropertyMap{}
   370  				for _, itm := range tc.data {
   371  					ks := itm.key.String()
   372  					prev := tmpLoader[ks]
   373  					updateIndexes(store, itm.key, prev, itm.props)
   374  					tmpLoader[ks] = itm.props
   375  				}
   376  				tmpLoader = nil
   377  
   378  				for colName, data := range tc.expected {
   379  					coll := store.Snapshot().GetCollection(colName)
   380  					So(coll, ShouldNotBeNil)
   381  					i := 0
   382  					coll.ForEachItem(func(k, _ []byte) bool {
   383  						So(data[i], ShouldResemble, k)
   384  						i++
   385  						return true
   386  					})
   387  					So(i, ShouldEqual, len(data))
   388  				}
   389  			})
   390  		}
   391  	})
   392  }