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 }