github.com/willyham/dosa@v2.3.1-0.20171024181418-1e446d37ee71+incompatible/connectors/memory/memory_test.go (about) 1 // Copyright (c) 2017 Uber Technologies, Inc. 2 // 3 // Permission is hereby granted, free of charge, to any person obtaining a copy 4 // of this software and associated documentation files (the "Software"), to deal 5 // in the Software without restriction, including without limitation the rights 6 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 // copies of the Software, and to permit persons to whom the Software is 8 // furnished to do so, subject to the following conditions: 9 // 10 // The above copyright notice and this permission notice shall be included in 11 // all copies or substantial portions of the Software. 12 // 13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 // THE SOFTWARE. 20 21 package memory 22 23 import ( 24 "context" 25 "testing" 26 "time" 27 28 "sort" 29 30 "github.com/satori/go.uuid" 31 "github.com/stretchr/testify/assert" 32 "github.com/uber-go/dosa" 33 ) 34 35 var testSchemaRef = dosa.SchemaRef{ 36 Scope: "scope1", 37 NamePrefix: "namePrefix", 38 EntityName: "eName", 39 Version: 12345, 40 } 41 42 var testEi = &dosa.EntityInfo{ 43 Ref: &testSchemaRef, 44 Def: &dosa.EntityDefinition{ 45 Columns: []*dosa.ColumnDefinition{ 46 {Name: "p1", Type: dosa.String}, 47 {Name: "c1", Type: dosa.Int64}, 48 {Name: "c2", Type: dosa.Double}, 49 {Name: "c3", Type: dosa.String}, 50 {Name: "c4", Type: dosa.Blob}, 51 {Name: "c5", Type: dosa.Bool}, 52 {Name: "c6", Type: dosa.Int32}, 53 {Name: "c7", Type: dosa.TUUID}, 54 }, 55 Key: &dosa.PrimaryKey{ 56 PartitionKeys: []string{"p1"}, 57 }, 58 Name: "t1", 59 Indexes: map[string]*dosa.IndexDefinition{ 60 "i1": {Key: &dosa.PrimaryKey{PartitionKeys: []string{"c1"}}}}, 61 }, 62 } 63 var clusteredEi = &dosa.EntityInfo{ 64 Ref: &testSchemaRef, 65 Def: &dosa.EntityDefinition{ 66 Columns: []*dosa.ColumnDefinition{ 67 {Name: "f1", Type: dosa.String}, 68 {Name: "c1", Type: dosa.Int64}, 69 {Name: "c2", Type: dosa.Double}, 70 {Name: "c3", Type: dosa.String}, 71 {Name: "c4", Type: dosa.Blob}, 72 {Name: "c5", Type: dosa.Bool}, 73 {Name: "c6", Type: dosa.Int32}, 74 {Name: "c7", Type: dosa.TUUID}, 75 }, 76 Key: &dosa.PrimaryKey{ 77 PartitionKeys: []string{"f1"}, 78 ClusteringKeys: []*dosa.ClusteringKey{ 79 {Name: "c1", Descending: false}, 80 {Name: "c7", Descending: true}, 81 }, 82 }, 83 Name: "t2", 84 Indexes: map[string]*dosa.IndexDefinition{ 85 "i2": {Key: &dosa.PrimaryKey{PartitionKeys: []string{"c1"}}}}, 86 }, 87 } 88 89 func TestConnector_CreateIfNotExists(t *testing.T) { 90 sut := NewConnector() 91 92 err := sut.CreateIfNotExists(context.TODO(), testEi, map[string]dosa.FieldValue{ 93 "p1": dosa.FieldValue("data"), 94 }) 95 assert.NoError(t, err) 96 97 err = sut.CreateIfNotExists(context.TODO(), testEi, map[string]dosa.FieldValue{ 98 "p1": dosa.FieldValue("data"), 99 }) 100 101 assert.True(t, dosa.ErrorIsAlreadyExists(err)) 102 } 103 func TestConnector_Upsert(t *testing.T) { 104 sut := NewConnector() 105 106 // no key value specified 107 err := sut.Upsert(context.TODO(), testEi, map[string]dosa.FieldValue{}) 108 assert.Error(t, err) 109 assert.Contains(t, err.Error(), `partition key "p1"`) 110 111 // regular upsert 112 err = sut.Upsert(context.TODO(), testEi, map[string]dosa.FieldValue{ 113 "p1": dosa.FieldValue("data"), 114 }) 115 assert.NoError(t, err) 116 vals, err := sut.Read(context.TODO(), testEi, map[string]dosa.FieldValue{ 117 "p1": dosa.FieldValue("data")}, []string{"c1"}) 118 assert.NoError(t, err) 119 assert.Nil(t, vals["c1"]) 120 121 err = sut.Upsert(context.TODO(), testEi, map[string]dosa.FieldValue{ 122 "p1": dosa.FieldValue("data"), 123 "c1": dosa.FieldValue(int64(1)), 124 }) 125 assert.NoError(t, err) 126 127 vals, err = sut.Read(context.TODO(), testEi, map[string]dosa.FieldValue{ 128 "p1": dosa.FieldValue("data")}, []string{"c1"}) 129 assert.NoError(t, err) 130 assert.Equal(t, dosa.FieldValue(int64(1)), vals["c1"]) 131 } 132 133 func TestConnector_Read(t *testing.T) { 134 sut := NewConnector() 135 136 // read with no data 137 vals, err := sut.Read(context.TODO(), testEi, map[string]dosa.FieldValue{ 138 "p1": dosa.FieldValue("data")}, []string{"c1"}) 139 assert.True(t, dosa.ErrorIsNotFound(err)) 140 141 // read with no key field 142 vals, err = sut.Read(context.TODO(), testEi, map[string]dosa.FieldValue{}, dosa.All()) 143 assert.Error(t, err) 144 assert.Contains(t, err.Error(), `partition key "p1"`) 145 146 err = sut.CreateIfNotExists(context.TODO(), testEi, map[string]dosa.FieldValue{ 147 "p1": dosa.FieldValue("data"), 148 "c1": dosa.FieldValue(int64(1)), 149 "c2": dosa.FieldValue(float64(2)), 150 }) 151 assert.NoError(t, err) 152 153 vals, err = sut.Read(context.TODO(), testEi, map[string]dosa.FieldValue{ 154 "p1": dosa.FieldValue("data")}, []string{"c1"}) 155 assert.NoError(t, err) 156 assert.Equal(t, int64(1), vals["c1"]) 157 158 vals, err = sut.Read(context.TODO(), testEi, map[string]dosa.FieldValue{ 159 "p1": dosa.FieldValue("data")}, dosa.All()) 160 assert.NoError(t, err) 161 assert.Equal(t, int64(1), vals["c1"]) 162 assert.Equal(t, float64(2), vals["c2"]) 163 assert.Equal(t, "data", vals["p1"]) 164 165 // read a key that isn't there 166 vals, err = sut.Read(context.TODO(), testEi, map[string]dosa.FieldValue{ 167 "p1": dosa.FieldValue("not there")}, dosa.All()) 168 assert.True(t, dosa.ErrorIsNotFound(err)) 169 170 // now delete the one that is 171 err = sut.Remove(context.TODO(), testEi, map[string]dosa.FieldValue{ 172 "p1": dosa.FieldValue("data")}) 173 assert.NoError(t, err) 174 175 // read the deleted key 176 vals, err = sut.Read(context.TODO(), testEi, map[string]dosa.FieldValue{ 177 "p1": dosa.FieldValue("data")}, dosa.All()) 178 assert.True(t, dosa.ErrorIsNotFound(err)) 179 180 // insert into clustered entity 181 id := dosa.NewUUID() 182 err = sut.CreateIfNotExists(context.TODO(), clusteredEi, map[string]dosa.FieldValue{ 183 "f1": dosa.FieldValue("key"), 184 "c1": dosa.FieldValue(int64(1)), 185 "c2": dosa.FieldValue(float64(1.2)), 186 "c7": dosa.FieldValue(id)}) 187 assert.NoError(t, err) 188 189 // read that row 190 vals, err = sut.Read(context.TODO(), clusteredEi, map[string]dosa.FieldValue{ 191 "f1": dosa.FieldValue("key"), 192 "c1": dosa.FieldValue(int64(1)), 193 "c7": dosa.FieldValue(id)}, dosa.All()) 194 assert.NoError(t, err) 195 assert.Equal(t, dosa.FieldValue(float64(1.2)), vals["c2"]) 196 197 // and fail a read on a clustered key 198 _, err = sut.Read(context.TODO(), clusteredEi, map[string]dosa.FieldValue{ 199 "f1": dosa.FieldValue("key"), 200 "c1": dosa.FieldValue(int64(2)), 201 "c7": dosa.FieldValue(id)}, dosa.All()) 202 assert.True(t, dosa.ErrorIsNotFound(err)) 203 } 204 205 func TestConnector_Remove(t *testing.T) { 206 sut := NewConnector() 207 208 // remove with no data 209 err := sut.Remove(context.TODO(), testEi, map[string]dosa.FieldValue{ 210 "p1": dosa.FieldValue("data")}) 211 assert.NoError(t, err) 212 213 // create a single row 214 err = sut.CreateIfNotExists(context.TODO(), testEi, map[string]dosa.FieldValue{ 215 "p1": dosa.FieldValue("data"), 216 "c1": dosa.FieldValue(int64(1)), 217 "c2": dosa.FieldValue(float64(2)), 218 }) 219 assert.NoError(t, err) 220 221 // remove something not there 222 err = sut.Remove(context.TODO(), testEi, map[string]dosa.FieldValue{ 223 "p1": dosa.FieldValue("nothere")}) 224 assert.NoError(t, err) 225 226 // insert into clustered entity 227 id := dosa.NewUUID() 228 err = sut.CreateIfNotExists(context.TODO(), clusteredEi, map[string]dosa.FieldValue{ 229 "f1": dosa.FieldValue("key"), 230 "c1": dosa.FieldValue(int64(1)), 231 "c2": dosa.FieldValue(float64(1.2)), 232 "c7": dosa.FieldValue(id)}) 233 assert.NoError(t, err) 234 235 // remove something not there, but matches partition 236 err = sut.Remove(context.TODO(), clusteredEi, map[string]dosa.FieldValue{ 237 "f1": dosa.FieldValue("key"), 238 "c1": dosa.FieldValue(int64(1)), 239 "c7": dosa.FieldValue(dosa.NewUUID())}) 240 assert.NoError(t, err) 241 242 // and remove the partitioned value 243 err = sut.Remove(context.TODO(), clusteredEi, map[string]dosa.FieldValue{ 244 "f1": dosa.FieldValue("key"), 245 "c1": dosa.FieldValue(int64(1)), 246 "c7": dosa.FieldValue(id)}) 247 assert.NoError(t, err) 248 249 // remove it again, now that there's nothing at all there (corner case) 250 err = sut.Remove(context.TODO(), clusteredEi, map[string]dosa.FieldValue{ 251 "f1": dosa.FieldValue("key"), 252 "c1": dosa.FieldValue(int64(1)), 253 "c7": dosa.FieldValue(id)}) 254 assert.NoError(t, err) 255 } 256 257 func TestConnector_RemoveRange(t *testing.T) { 258 const idcount = 10 259 sut := NewConnector() 260 261 // test removing a range with no data in the range 262 err := sut.RemoveRange(context.TODO(), clusteredEi, map[string][]*dosa.Condition{ 263 "f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}}, 264 }) 265 assert.NoError(t, err) 266 267 // insert some data all into the data partition, spread out among the c1 clustering key 268 for x := 0; x < idcount; x++ { 269 err := sut.CreateIfNotExists(context.TODO(), clusteredEi, map[string]dosa.FieldValue{ 270 "f1": dosa.FieldValue("data"), 271 "c1": dosa.FieldValue(int64(x)), 272 "c7": dosa.FieldValue(dosa.NewUUID())}) 273 assert.NoError(t, err) 274 } 275 276 // remove with missing primary key values 277 err = sut.RemoveRange(context.TODO(), clusteredEi, map[string][]*dosa.Condition{ 278 "c1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}}, 279 "c7": {{Op: dosa.Gt, Value: dosa.FieldValue(int64(4))}}, 280 }) 281 assert.Error(t, err) 282 assert.Contains(t, err.Error(), "f1") 283 assert.Contains(t, err.Error(), "missing") 284 285 // delete all values greater than those with 4 for c1 286 err = sut.RemoveRange(context.TODO(), clusteredEi, map[string][]*dosa.Condition{ 287 "f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}}, 288 "c1": {{Op: dosa.Gt, Value: dosa.FieldValue(int64(4))}}, 289 }) 290 assert.NoError(t, err) 291 292 // ensure all the rows with c1 value less than or equal to 4 still exist 293 data, _, err := sut.Range(context.TODO(), clusteredEi, map[string][]*dosa.Condition{ 294 "f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}}, 295 "c1": {{Op: dosa.LtOrEq, Value: dosa.FieldValue(int64(4))}}, 296 }, dosa.All(), "", 200) 297 assert.NoError(t, err) 298 assert.Len(t, data, idcount/2) 299 for i, x := range data { 300 assert.Equal(t, x["c1"], int64(i)) 301 } 302 303 // ensure all the with a c1 value greater than 4 are deleted. 304 data, _, err = sut.Range(context.TODO(), clusteredEi, map[string][]*dosa.Condition{ 305 "f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}}, 306 "c1": {{Op: dosa.Gt, Value: dosa.FieldValue(int64(4))}}, 307 }, dosa.All(), "", 200) 308 assert.NoError(t, err) 309 assert.Empty(t, data) 310 311 // remove everything but the highest value 312 err = sut.RemoveRange(context.TODO(), clusteredEi, map[string][]*dosa.Condition{ 313 "f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}}, 314 "c1": {{Op: dosa.Gt, Value: dosa.FieldValue(int64(0))}}, 315 }) 316 assert.NoError(t, err) 317 318 // there should only be one value left now 319 data, _, err = sut.Range(context.TODO(), clusteredEi, map[string][]*dosa.Condition{ 320 "f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}}, 321 }, dosa.All(), "", 200) 322 assert.NoError(t, err) 323 assert.Len(t, data, 1) 324 325 // test completely deleting all the rows in a partition. 326 err = sut.RemoveRange(context.TODO(), clusteredEi, map[string][]*dosa.Condition{ 327 "f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}}, 328 }) 329 assert.NoError(t, err) 330 data, _, err = sut.Range(context.TODO(), clusteredEi, map[string][]*dosa.Condition{ 331 "f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}}, 332 }, dosa.All(), "", 200) 333 assert.NoError(t, err) 334 assert.Empty(t, data) 335 } 336 337 func TestConnector_Shutdown(t *testing.T) { 338 sut := NewConnector() 339 340 err := sut.Shutdown() 341 assert.NoError(t, err) 342 assert.Nil(t, sut.data) 343 } 344 345 // test CreateIfNotExists with partitioning 346 func TestConnector_CreateIfNotExists2(t *testing.T) { 347 sut := NewConnector() 348 349 testUUIDs := make([]dosa.UUID, 10) 350 for x := 0; x < 10; x++ { 351 testUUIDs[x] = dosa.NewUUID() 352 } 353 354 // first, insert 10 random UUID values into same partition key 355 for x := 0; x < 10; x++ { 356 err := sut.CreateIfNotExists(context.TODO(), clusteredEi, map[string]dosa.FieldValue{ 357 "f1": dosa.FieldValue("data"), 358 "c1": dosa.FieldValue(int64(1)), 359 "c7": dosa.FieldValue(testUUIDs[x])}) 360 assert.NoError(t, err) 361 } 362 // attempt to insert them all again 363 for x := 0; x < 10; x++ { 364 err := sut.CreateIfNotExists(context.TODO(), clusteredEi, map[string]dosa.FieldValue{ 365 "f1": dosa.FieldValue("data"), 366 "c1": dosa.FieldValue(int64(1)), 367 "c7": dosa.FieldValue(testUUIDs[x])}) 368 assert.Error(t, err, string(testUUIDs[x])) 369 assert.True(t, dosa.ErrorIsAlreadyExists(err)) 370 } 371 // now, insert them again, but this time with a different secondary key 372 for x := 0; x < 10; x++ { 373 err := sut.CreateIfNotExists(context.TODO(), clusteredEi, map[string]dosa.FieldValue{ 374 "f1": dosa.FieldValue("data"), 375 "c1": dosa.FieldValue(int64(2)), 376 "c7": dosa.FieldValue(testUUIDs[x])}) 377 assert.NoError(t, err) 378 } 379 // and with a different primary key 380 for x := 0; x < 10; x++ { 381 err := sut.CreateIfNotExists(context.TODO(), clusteredEi, map[string]dosa.FieldValue{ 382 "f1": dosa.FieldValue("different"), 383 "c1": dosa.FieldValue(int64(1)), 384 "c7": dosa.FieldValue(testUUIDs[x])}) 385 assert.NoError(t, err) 386 } 387 data, token, err := sut.Range(context.TODO(), clusteredEi, map[string][]*dosa.Condition{ 388 "f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}}, 389 }, dosa.All(), "", 200) 390 assert.NoError(t, err) 391 assert.Empty(t, token) 392 assert.Len(t, data, 20) 393 } 394 395 func TestConnector_Upsert2(t *testing.T) { 396 sut := NewConnector() 397 398 testUUIDs := make([]dosa.UUID, 10) 399 for x := 0; x < 10; x++ { 400 testUUIDs[x] = dosa.NewUUID() 401 } 402 403 // first, insert 10 random UUID values into same partition key 404 for x := 0; x < 10; x++ { 405 err := sut.Upsert(context.TODO(), clusteredEi, map[string]dosa.FieldValue{ 406 "f1": dosa.FieldValue("data"), 407 "c1": dosa.FieldValue(int64(1)), 408 "c7": dosa.FieldValue(testUUIDs[x])}) 409 assert.NoError(t, err) 410 } 411 // attempt to insert them all again 412 for x := 0; x < 10; x++ { 413 err := sut.Upsert(context.TODO(), clusteredEi, map[string]dosa.FieldValue{ 414 "f1": dosa.FieldValue("data"), 415 "c1": dosa.FieldValue(int64(1)), 416 "c6": dosa.FieldValue(int32(x)), 417 "c7": dosa.FieldValue(testUUIDs[x])}) 418 assert.NoError(t, err) 419 } 420 // now, insert them again, but this time with a different secondary key 421 for x := 0; x < 10; x++ { 422 err := sut.Upsert(context.TODO(), clusteredEi, map[string]dosa.FieldValue{ 423 "f1": dosa.FieldValue("data"), 424 "c1": dosa.FieldValue(int64(2)), 425 "c7": dosa.FieldValue(testUUIDs[x])}) 426 assert.NoError(t, err) 427 } 428 // and with a different primary key 429 for x := 0; x < 10; x++ { 430 err := sut.Upsert(context.TODO(), clusteredEi, map[string]dosa.FieldValue{ 431 "f1": dosa.FieldValue("different"), 432 "c1": dosa.FieldValue(int64(1)), 433 "c7": dosa.FieldValue(testUUIDs[x])}) 434 assert.NoError(t, err) 435 } 436 data, token, err := sut.Range(context.TODO(), clusteredEi, map[string][]*dosa.Condition{ 437 "f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}}, 438 }, dosa.All(), "", 200) 439 assert.NoError(t, err) 440 assert.Empty(t, token) 441 assert.Len(t, data, 20) 442 assert.NotNil(t, data[0]["c6"]) 443 } 444 445 func TestConnector_Range(t *testing.T) { 446 const idcount = 10 447 sut := NewConnector() 448 449 // no data at all (corner case) 450 data, token, err := sut.Range(context.TODO(), clusteredEi, map[string][]*dosa.Condition{ 451 "f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}}, 452 }, dosa.All(), "", 200) 453 assert.NoError(t, err) 454 assert.Empty(t, token) 455 assert.Empty(t, data) 456 457 // insert some data into data/1/uuid with a random set of uuids 458 // we insert them in a random order 459 testUUIDs := make([]dosa.UUID, idcount) 460 for x := 0; x < idcount; x++ { 461 testUUIDs[x] = dosa.NewUUID() 462 } 463 for x := 0; x < idcount; x++ { 464 err := sut.CreateIfNotExists(context.TODO(), clusteredEi, map[string]dosa.FieldValue{ 465 "f1": dosa.FieldValue("data"), 466 "c1": dosa.FieldValue(int64(1)), 467 "c7": dosa.FieldValue(testUUIDs[x])}) 468 assert.NoError(t, err) 469 } 470 471 // search using a different partition key 472 data, token, err = sut.Range(context.TODO(), clusteredEi, map[string][]*dosa.Condition{ 473 "f1": {{Op: dosa.Eq, Value: dosa.FieldValue("wrongdata")}}, 474 }, dosa.All(), "", 200) 475 assert.NoError(t, err) 476 assert.Empty(t, data) 477 478 sort.Sort(ByUUID(testUUIDs)) 479 // search using the right partition key, and check that the data was insertion-sorted 480 // correctly 481 data, _, _ = sut.Range(context.TODO(), clusteredEi, map[string][]*dosa.Condition{ 482 "f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}}, 483 "c1": {{Op: dosa.Eq, Value: dosa.FieldValue(int64(1))}}, 484 }, dosa.All(), "", 200) 485 for idx, row := range data { 486 assert.Equal(t, testUUIDs[idx], row["c7"]) 487 } 488 489 // find the midpoint and look for all values greater than that 490 data, token, err = sut.Range(context.TODO(), clusteredEi, map[string][]*dosa.Condition{ 491 "f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}}, 492 "c1": {{Op: dosa.Eq, Value: dosa.FieldValue(int64(1))}}, 493 "c7": {{Op: dosa.Gt, Value: dosa.FieldValue(testUUIDs[idcount/2-1])}}, 494 }, dosa.All(), "", 200) 495 assert.NoError(t, err) 496 assert.Len(t, data, idcount/2-1) 497 498 // there's one more for greater than or equal 499 data, token, err = sut.Range(context.TODO(), clusteredEi, map[string][]*dosa.Condition{ 500 "f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}}, 501 "c1": {{Op: dosa.Eq, Value: dosa.FieldValue(int64(1))}}, 502 "c7": {{Op: dosa.GtOrEq, Value: dosa.FieldValue(testUUIDs[idcount/2-1])}}, 503 }, dosa.All(), "", 200) 504 assert.NoError(t, err) 505 assert.Len(t, data, idcount/2) 506 507 // find the midpoint and look for all values less than that 508 data, token, err = sut.Range(context.TODO(), clusteredEi, map[string][]*dosa.Condition{ 509 "f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}}, 510 "c1": {{Op: dosa.Eq, Value: dosa.FieldValue(int64(1))}}, 511 "c7": {{Op: dosa.Lt, Value: dosa.FieldValue(testUUIDs[idcount/2])}}, 512 }, dosa.All(), "", 200) 513 assert.NoError(t, err) 514 assert.Len(t, data, idcount/2-1) 515 516 // and same for less than or equal 517 data, token, err = sut.Range(context.TODO(), clusteredEi, map[string][]*dosa.Condition{ 518 "f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}}, 519 "c1": {{Op: dosa.Eq, Value: dosa.FieldValue(int64(1))}}, 520 "c7": {{Op: dosa.LtOrEq, Value: dosa.FieldValue(testUUIDs[idcount/2])}}, 521 }, dosa.All(), "", 200) 522 assert.NoError(t, err) 523 assert.Len(t, data, idcount/2) 524 525 // look off the end of the left side, so greater than maximum (edge case) 526 // (uuids are ordered descending so this is non-intuitively backwards) 527 data, token, err = sut.Range(context.TODO(), clusteredEi, map[string][]*dosa.Condition{ 528 "f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}}, 529 "c1": {{Op: dosa.Eq, Value: dosa.FieldValue(int64(1))}}, 530 "c7": {{Op: dosa.Gt, Value: dosa.FieldValue(testUUIDs[0])}}, 531 }, dosa.All(), "", 200) 532 assert.NoError(t, err) 533 assert.Empty(t, data) 534 535 // look off the end of the left side, so greater than maximum 536 data, _, err = sut.Range(context.TODO(), clusteredEi, map[string][]*dosa.Condition{ 537 "f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}}, 538 "c1": {{Op: dosa.Eq, Value: dosa.FieldValue(int64(1))}}, 539 "c7": {{Op: dosa.Lt, Value: dosa.FieldValue(testUUIDs[idcount-1])}}, 540 }, dosa.All(), "", 200) 541 assert.NoError(t, err) 542 assert.Empty(t, data) 543 544 // Test Ranging on an Index 545 546 // Get "1" partition 547 data, token, err = sut.Range(context.TODO(), clusteredEi, map[string][]*dosa.Condition{ 548 "c1": {{Op: dosa.Eq, Value: dosa.FieldValue(int64(1))}}, 549 }, dosa.All(), "", 200) 550 assert.NoError(t, err) 551 assert.Len(t, data, 10) 552 assert.Empty(t, token) 553 554 // Get the "2" partition, should be empty 555 data, token, err = sut.Range(context.TODO(), clusteredEi, map[string][]*dosa.Condition{ 556 "c1": {{Op: dosa.Eq, Value: dosa.FieldValue(int64(2))}}, 557 }, dosa.All(), "", 200) 558 assert.NoError(t, err) 559 assert.Empty(t, data) 560 assert.Empty(t, token) 561 } 562 563 func TestConnector_TimeUUIDs(t *testing.T) { 564 sut := NewConnector() 565 const idcount = 10 566 567 // insert a bunch of values with V1 timestamps as clustering keys 568 for x := 0; x < idcount; x++ { 569 err := sut.Upsert(context.TODO(), clusteredEi, map[string]dosa.FieldValue{ 570 "f1": dosa.FieldValue("data"), 571 "c1": dosa.FieldValue(int64(1)), 572 "c6": dosa.FieldValue(int32(x)), 573 "c7": dosa.FieldValue(dosa.UUID(uuid.NewV1().String()))}) 574 assert.NoError(t, err) 575 } 576 577 // read them back, they should be in reverse order 578 data, _, _ := sut.Range(context.TODO(), clusteredEi, map[string][]*dosa.Condition{ 579 "f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}}, 580 "c1": {{Op: dosa.Eq, Value: dosa.FieldValue(int64(1))}}, 581 }, dosa.All(), "", 200) 582 583 // check that the order is backwards 584 for idx, row := range data { 585 assert.Equal(t, int32(idcount-idx-1), row["c6"]) 586 } 587 588 // now mix in a few V4 UUIDs 589 for x := 0; x < idcount; x++ { 590 err := sut.Upsert(context.TODO(), clusteredEi, map[string]dosa.FieldValue{ 591 "f1": dosa.FieldValue("data"), 592 "c1": dosa.FieldValue(int64(1)), 593 "c6": dosa.FieldValue(int32(idcount + x)), 594 "c7": dosa.FieldValue(dosa.NewUUID())}) 595 assert.NoError(t, err) 596 } 597 598 // the V4's should all be first, since V4 UUIDs sort > V1 UUIDs 599 data, _, _ = sut.Range(context.TODO(), clusteredEi, map[string][]*dosa.Condition{ 600 "f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}}, 601 "c1": {{Op: dosa.Eq, Value: dosa.FieldValue(int64(1))}}, 602 }, dosa.All(), "", 200) 603 for _, row := range data[0:idcount] { 604 assert.True(t, row["c6"].(int32) >= idcount, row["c6"]) 605 } 606 607 } 608 609 type ByUUID []dosa.UUID 610 611 func (u ByUUID) Len() int { return len(u) } 612 func (u ByUUID) Swap(i, j int) { u[i], u[j] = u[j], u[i] } 613 func (u ByUUID) Less(i, j int) bool { return string(u[i]) > string(u[j]) } 614 615 func BenchmarkConnector_CreateIfNotExists(b *testing.B) { 616 sut := NewConnector() 617 for x := 0; x < b.N; x++ { 618 id := dosa.NewUUID() 619 err := sut.CreateIfNotExists(context.TODO(), clusteredEi, map[string]dosa.FieldValue{ 620 "f1": dosa.FieldValue("key"), 621 "c1": dosa.FieldValue(int64(1)), 622 "c7": dosa.FieldValue(id)}) 623 assert.NoError(b, err) 624 if x%1000 == 0 { 625 sut.data = nil 626 } 627 } 628 } 629 630 func BenchmarkConnector_Read(b *testing.B) { 631 const idcount = 100 632 testUUIDs := make([]dosa.UUID, idcount) 633 for x := 0; x < idcount; x++ { 634 testUUIDs[x] = dosa.NewUUID() 635 } 636 sut := NewConnector() 637 for x := 0; x < idcount; x++ { 638 err := sut.CreateIfNotExists(context.TODO(), clusteredEi, map[string]dosa.FieldValue{ 639 "f1": dosa.FieldValue("data"), 640 "c1": dosa.FieldValue(int64(1)), 641 "c7": dosa.FieldValue(testUUIDs[x])}) 642 assert.NoError(b, err) 643 } 644 645 for x := 0; x < b.N; x++ { 646 _, err := sut.Read(context.TODO(), clusteredEi, map[string]dosa.FieldValue{ 647 "f1": dosa.FieldValue("data"), 648 "c1": dosa.FieldValue(int64(1)), 649 "c7": dosa.FieldValue(testUUIDs[x%idcount])}, dosa.All()) 650 assert.NoError(b, err) 651 } 652 } 653 654 func TestCompareType(t *testing.T) { 655 tuuid := dosa.NewUUID() 656 v1uuid := dosa.UUID(uuid.NewV1().String()) 657 v1newer := dosa.UUID(uuid.NewV1().String()) 658 tests := []struct { 659 t1, t2 dosa.FieldValue 660 result int8 661 }{ 662 {dosa.FieldValue(int32(1)), dosa.FieldValue(int32(1)), 0}, 663 {dosa.FieldValue(int64(1)), dosa.FieldValue(int64(1)), 0}, 664 {dosa.FieldValue("test"), dosa.FieldValue("test"), 0}, 665 {dosa.FieldValue(time.Time{}), dosa.FieldValue(time.Time{}), 0}, 666 {dosa.FieldValue(tuuid), dosa.FieldValue(tuuid), 0}, 667 {dosa.FieldValue(v1uuid), dosa.FieldValue(v1uuid), 0}, 668 {dosa.FieldValue(false), dosa.FieldValue(false), 0}, 669 {dosa.FieldValue([]byte{1}), dosa.FieldValue([]byte{1}), 0}, 670 {dosa.FieldValue(1.0), dosa.FieldValue(1.0), 0}, 671 672 {dosa.FieldValue(int32(1)), dosa.FieldValue(int32(2)), -1}, 673 {dosa.FieldValue(int64(1)), dosa.FieldValue(int64(2)), -1}, 674 {dosa.FieldValue("test"), dosa.FieldValue("test2"), -1}, 675 {dosa.FieldValue(time.Time{}), dosa.FieldValue(time.Time{}.Add(time.Duration(1))), -1}, 676 {dosa.FieldValue(v1uuid), dosa.FieldValue(tuuid), -1}, 677 {dosa.FieldValue(v1uuid), dosa.FieldValue(v1newer), -1}, 678 {dosa.FieldValue(false), dosa.FieldValue(true), -1}, 679 {dosa.FieldValue([]byte{1}), dosa.FieldValue([]byte{2}), -1}, 680 {dosa.FieldValue(0.9), dosa.FieldValue(1.0), -1}, 681 682 {dosa.FieldValue(int32(2)), dosa.FieldValue(int32(1)), 1}, 683 {dosa.FieldValue(int64(2)), dosa.FieldValue(int64(1)), 1}, 684 {dosa.FieldValue("test2"), dosa.FieldValue("test"), 1}, 685 {dosa.FieldValue(time.Time{}.Add(time.Duration(1))), dosa.FieldValue(time.Time{}), 1}, 686 {dosa.FieldValue(tuuid), dosa.FieldValue(v1uuid), 1}, 687 {dosa.FieldValue(v1newer), dosa.FieldValue(v1uuid), 1}, 688 {dosa.FieldValue(true), dosa.FieldValue(false), 1}, 689 {dosa.FieldValue([]byte{2}), dosa.FieldValue([]byte{1}), 1}, 690 {dosa.FieldValue(1.1), dosa.FieldValue(1.0), 1}, 691 } 692 for _, test := range tests { 693 assert.Equal(t, test.result, compareType(test.t1, test.t2)) 694 } 695 696 assert.Panics(t, func() { compareType(t, t) }) 697 } 698 699 func TestConnector_Scan(t *testing.T) { 700 sut := NewConnector() 701 const idcount = 10 702 703 testUUIDs := make([]dosa.UUID, idcount) 704 for x := 0; x < idcount; x++ { 705 testUUIDs[x] = dosa.NewUUID() 706 } 707 // scan with nothing there yet 708 _, token, err := sut.Scan(context.TODO(), clusteredEi, dosa.All(), "", 100) 709 assert.NoError(t, err) 710 assert.Empty(t, token) 711 712 // first, insert some random UUID values into two partition keys 713 for x := 0; x < idcount; x++ { 714 err := sut.Upsert(context.TODO(), clusteredEi, map[string]dosa.FieldValue{ 715 "f1": dosa.FieldValue("data" + string(x%2)), 716 "c1": dosa.FieldValue(int64(1)), 717 "c7": dosa.FieldValue(testUUIDs[x])}) 718 assert.NoError(t, err) 719 } 720 721 data, token, err := sut.Scan(context.TODO(), clusteredEi, dosa.All(), "", 100) 722 assert.NoError(t, err) 723 assert.Len(t, data, idcount) 724 assert.Empty(t, token) 725 726 // there's an odd edge case when you delete everything, so do that, then call scan 727 for x := 0; x < idcount; x++ { 728 err := sut.Remove(context.TODO(), clusteredEi, map[string]dosa.FieldValue{ 729 "f1": dosa.FieldValue("data" + string(x%2)), 730 "c1": dosa.FieldValue(int64(1)), 731 "c7": dosa.FieldValue(testUUIDs[x])}) 732 assert.NoError(t, err) 733 } 734 data, token, err = sut.Scan(context.TODO(), clusteredEi, dosa.All(), "", 100) 735 assert.NoError(t, err) 736 assert.Empty(t, data) 737 assert.Empty(t, token) 738 } 739 740 func TestConnector_ScanWithToken(t *testing.T) { 741 sut := NewConnector() 742 const idcount = 100 743 createTestData(t, sut, func(id int) string { 744 return "data" + string(id%3) 745 }, idcount) 746 var token string 747 var err error 748 var data []map[string]dosa.FieldValue 749 for x := 0; x < idcount; x++ { 750 data, token, err = sut.Scan(context.TODO(), clusteredEi, dosa.All(), token, 1) 751 assert.NoError(t, err) 752 assert.Len(t, data, 1) 753 if x < idcount-1 { 754 assert.NotEmpty(t, token) 755 } else { 756 assert.Empty(t, token) 757 } 758 } 759 // now walk through again, but delete the item returned 760 // (note: we don't have to reset token, because it should be empty now) 761 for x := 0; x < idcount; x++ { 762 data, token, err = sut.Scan(context.TODO(), clusteredEi, dosa.All(), token, 1) 763 assert.NoError(t, err) 764 assert.Len(t, data, 1) 765 if x < idcount-1 { 766 assert.NotEmpty(t, token) 767 } else { 768 assert.Empty(t, token) 769 } 770 err = sut.Remove(context.TODO(), clusteredEi, data[0]) 771 assert.NoError(t, err) 772 } 773 } 774 775 func TestConnector_ScanWithTokenFromWrongTable(t *testing.T) { 776 sut := NewConnector() 777 const idcount = 100 778 createTestData(t, sut, func(id int) string { 779 return "data" + string(id%3) 780 }, idcount) 781 err := sut.Upsert(context.TODO(), testEi, map[string]dosa.FieldValue{ 782 "p1": dosa.FieldValue("test"), 783 }) 784 assert.NoError(t, err) 785 786 // get a token from one table 787 _, token, err := sut.Scan(context.TODO(), clusteredEi, dosa.All(), "", 1) 788 assert.NoError(t, err) 789 assert.NotEmpty(t, token) 790 791 // now use it for another scan (oops) 792 _, _, err = sut.Scan(context.TODO(), testEi, dosa.All(), token, 1) 793 assert.Error(t, err) 794 assert.Contains(t, err.Error(), "Invalid token") 795 assert.Contains(t, err.Error(), "Missing value") 796 } 797 798 func TestConnector_ScanWithTokenNoClustering(t *testing.T) { 799 sut := NewConnector() 800 801 // add some data 802 const idcount = 100 803 for x := 0; x < idcount; x++ { 804 sut.Upsert(context.TODO(), testEi, map[string]dosa.FieldValue{ 805 "p1": dosa.FieldValue("data" + string(x)), 806 }) 807 } 808 var token string 809 var err error 810 var data []map[string]dosa.FieldValue 811 for x := 0; x < idcount; x++ { 812 data, token, err = sut.Scan(context.TODO(), testEi, dosa.All(), token, 1) 813 assert.NoError(t, err) 814 assert.Len(t, data, 1) 815 if x < idcount-1 { 816 assert.NotEmpty(t, token) 817 } else { 818 assert.Empty(t, token) 819 } 820 } 821 // and again, this time deleting as we go 822 for x := 0; x < idcount; x++ { 823 data, token, err = sut.Scan(context.TODO(), testEi, dosa.All(), token, 1) 824 assert.NoError(t, err) 825 assert.Len(t, data, 1) 826 if x < idcount-1 { 827 assert.NotEmpty(t, token) 828 } else { 829 assert.Empty(t, token) 830 } 831 err = sut.Remove(context.TODO(), testEi, data[0]) 832 assert.NoError(t, err) 833 } 834 835 } 836 837 func TestConstruction(t *testing.T) { 838 c, err := dosa.GetConnector("memory", nil) 839 assert.NoError(t, err) 840 assert.IsType(t, NewConnector(), c) 841 842 v, err := c.CheckSchema(context.TODO(), "dummy", "dummy", nil) 843 assert.Equal(t, int32(1), v) 844 assert.NoError(t, err) 845 } 846 847 func TestPanics(t *testing.T) { 848 assert.Panics(t, func() { 849 passCol(dosa.FieldValue(int64(1)), &dosa.Condition{Op: 0, Value: dosa.FieldValue(int64(1))}) 850 }) 851 } 852 853 func TestRangePager(t *testing.T) { 854 sut := NewConnector() 855 idcount := 5 856 // create test data in one partition "data" 857 createTestData(t, sut, func(_ int) string { return "data" }, idcount) 858 var token string 859 var err error 860 var data []map[string]dosa.FieldValue 861 for x := 0; x < idcount; x++ { 862 data, token, err = sut.Range(context.TODO(), clusteredEi, map[string][]*dosa.Condition{ 863 "f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}}, 864 "c1": {{Op: dosa.Eq, Value: dosa.FieldValue(int64(1))}}, 865 }, dosa.All(), token, 1) 866 assert.NoError(t, err) 867 assert.Len(t, data, 1) 868 if x < idcount-1 { 869 assert.NotEmpty(t, token) 870 } else { 871 assert.Empty(t, token) 872 } 873 } 874 // now walk through again, but delete the item returned 875 // (note: we don't have to reset token, because it should be empty now) 876 for x := 0; x < idcount; x++ { 877 data, token, err = sut.Range(context.TODO(), clusteredEi, map[string][]*dosa.Condition{ 878 "f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}}, 879 "c1": {{Op: dosa.Eq, Value: dosa.FieldValue(int64(1))}}, 880 }, dosa.All(), token, 1) 881 assert.NoError(t, err) 882 assert.Len(t, data, 1) 883 if x < idcount-1 { 884 assert.NotEmpty(t, token) 885 } else { 886 assert.Empty(t, token) 887 } 888 err = sut.Remove(context.TODO(), clusteredEi, data[0]) 889 assert.NoError(t, err) 890 } 891 } 892 func TestInvalidToken(t *testing.T) { 893 sut := NewConnector() 894 895 // we don't use the token if there's no data that matches, so lets 896 // create one row 897 createTestData(t, sut, func(id int) string { 898 return "data" 899 }, 1) 900 901 token := "this is not a token and not a hot dog" 902 t.Run("testInvalidTokenRange", func(t *testing.T) { 903 _, _, err := sut.Range(context.TODO(), clusteredEi, map[string][]*dosa.Condition{ 904 "f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}}, 905 "c1": {{Op: dosa.Eq, Value: dosa.FieldValue(int64(1))}}, 906 }, dosa.All(), token, 1) 907 assert.Error(t, err) 908 assert.Contains(t, err.Error(), "Invalid token") 909 }) 910 t.Run("testInvalidTokenScan", func(t *testing.T) { 911 _, _, err := sut.Scan(context.TODO(), clusteredEi, dosa.All(), token, 1) 912 assert.Error(t, err) 913 assert.Contains(t, err.Error(), "Invalid token") 914 }) 915 } 916 917 // TestEncoderPanic covers the one panic() in makeToken 918 // This panic should only happen if someone passes a dosa.FieldValue that points to 919 // something we don't know how to handle 920 func TestEncoderPanic(t *testing.T) { 921 assert.Panics(t, func() { 922 makeToken(map[string]dosa.FieldValue{ 923 "oops": func() {}, 924 }) 925 }) 926 } 927 928 func TestConnector_RangeWithBadCriteria(t *testing.T) { 929 sut := NewConnector() 930 // we don't look at the criteria unless there is at least one row 931 createTestData(t, sut, func(id int) string { 932 return "data" 933 }, 1) 934 935 _, _, err := sut.Range(context.TODO(), clusteredEi, map[string][]*dosa.Condition{ 936 "c2": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}}, 937 "c3": {{Op: dosa.Eq, Value: dosa.FieldValue(int64(1))}}, 938 }, dosa.All(), "", 1) 939 assert.Error(t, err) 940 941 } 942 943 // createTestData populates some test data. The keyGenFunc can either return a constant, 944 // which gives you a single partition of data, or some function of the current offset, which 945 // will scatter the data across different partition keys 946 func createTestData(t *testing.T, sut *Connector, keyGenFunc func(int) string, idcount int) { 947 // insert a bunch of values with V1 timestamps as clustering keys 948 for x := 0; x < idcount; x++ { 949 err := sut.Upsert(context.TODO(), clusteredEi, map[string]dosa.FieldValue{ 950 "f1": dosa.FieldValue(keyGenFunc(x)), 951 "c1": dosa.FieldValue(int64(1)), 952 "c6": dosa.FieldValue(int32(x)), 953 "c7": dosa.FieldValue(dosa.UUID(uuid.NewV1().String()))}) 954 assert.NoError(t, err) 955 } 956 }