github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/internal/datastore/common/changes_test.go (about) 1 package common 2 3 import ( 4 "context" 5 "slices" 6 "sort" 7 "strings" 8 "testing" 9 10 "github.com/stretchr/testify/require" 11 12 "github.com/authzed/spicedb/internal/datastore/revisions" 13 "github.com/authzed/spicedb/pkg/datastore" 14 core "github.com/authzed/spicedb/pkg/proto/core/v1" 15 "github.com/authzed/spicedb/pkg/tuple" 16 ) 17 18 const ( 19 tuple1 = "docs:1#reader@user:1" 20 tuple2 = "docs:2#editor@user:2" 21 ) 22 23 var ( 24 rev1 = revisions.NewForTransactionID(1) 25 rev2 = revisions.NewForTransactionID(2) 26 rev3 = revisions.NewForTransactionID(3) 27 revOneMillion = revisions.NewForTransactionID(1_000_000) 28 revOneMillionOne = revisions.NewForTransactionID(1_000_001) 29 ) 30 31 func TestChanges(t *testing.T) { 32 type changeEntry struct { 33 revision uint64 34 relationship string 35 op core.RelationTupleUpdate_Operation 36 deletedNamespaces []string 37 deletedCaveats []string 38 changedDefinitions []datastore.SchemaDefinition 39 } 40 41 testCases := []struct { 42 name string 43 script []changeEntry 44 expected []datastore.RevisionChanges 45 }{ 46 { 47 "empty", 48 []changeEntry{}, 49 []datastore.RevisionChanges{}, 50 }, 51 { 52 "deleted namespace", 53 []changeEntry{ 54 {1, "", 0, []string{"somenamespace"}, nil, nil}, 55 }, 56 []datastore.RevisionChanges{ 57 {Revision: rev1, RelationshipChanges: nil, DeletedNamespaces: []string{"somenamespace"}}, 58 }, 59 }, 60 { 61 "deleted caveat", 62 []changeEntry{ 63 {1, "", 0, nil, []string{"somecaveat"}, nil}, 64 }, 65 []datastore.RevisionChanges{ 66 {Revision: rev1, RelationshipChanges: nil, DeletedCaveats: []string{"somecaveat"}}, 67 }, 68 }, 69 { 70 "changed namespace", 71 []changeEntry{ 72 {1, "", 0, nil, nil, []datastore.SchemaDefinition{&core.NamespaceDefinition{ 73 Name: "somenamespace", 74 }}}, 75 }, 76 []datastore.RevisionChanges{ 77 {Revision: rev1, RelationshipChanges: nil, ChangedDefinitions: []datastore.SchemaDefinition{&core.NamespaceDefinition{ 78 Name: "somenamespace", 79 }}}, 80 }, 81 }, 82 { 83 "create", 84 []changeEntry{ 85 {1, tuple1, core.RelationTupleUpdate_TOUCH, nil, nil, nil}, 86 }, 87 []datastore.RevisionChanges{ 88 {Revision: rev1, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple1)}}, 89 }, 90 }, 91 { 92 "delete", 93 []changeEntry{ 94 {1, tuple1, core.RelationTupleUpdate_DELETE, nil, nil, nil}, 95 }, 96 []datastore.RevisionChanges{ 97 {Revision: rev1, RelationshipChanges: []*core.RelationTupleUpdate{del(tuple1)}}, 98 }, 99 }, 100 { 101 "in-order touch", 102 []changeEntry{ 103 {1, tuple1, core.RelationTupleUpdate_DELETE, nil, nil, nil}, 104 {1, tuple1, core.RelationTupleUpdate_TOUCH, nil, nil, nil}, 105 }, 106 []datastore.RevisionChanges{ 107 {Revision: rev1, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple1)}}, 108 }, 109 }, 110 { 111 "reverse-order touch", 112 []changeEntry{ 113 {1, tuple1, core.RelationTupleUpdate_TOUCH, nil, nil, nil}, 114 {1, tuple1, core.RelationTupleUpdate_DELETE, nil, nil, nil}, 115 }, 116 []datastore.RevisionChanges{ 117 {Revision: rev1, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple1)}}, 118 }, 119 }, 120 { 121 "create and delete", 122 []changeEntry{ 123 {1, tuple1, core.RelationTupleUpdate_TOUCH, nil, nil, nil}, 124 {1, tuple2, core.RelationTupleUpdate_DELETE, nil, nil, nil}, 125 }, 126 []datastore.RevisionChanges{ 127 {Revision: rev1, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple1), del(tuple2)}}, 128 }, 129 }, 130 { 131 "multiple creates", 132 []changeEntry{ 133 {1, tuple1, core.RelationTupleUpdate_TOUCH, nil, nil, nil}, 134 {1, tuple2, core.RelationTupleUpdate_TOUCH, nil, nil, nil}, 135 }, 136 []datastore.RevisionChanges{ 137 {Revision: rev1, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple1), touch(tuple2)}}, 138 }, 139 }, 140 { 141 "duplicates", 142 []changeEntry{ 143 {1, tuple1, core.RelationTupleUpdate_TOUCH, nil, nil, nil}, 144 {1, tuple1, core.RelationTupleUpdate_TOUCH, nil, nil, nil}, 145 {1, tuple1, core.RelationTupleUpdate_TOUCH, nil, nil, nil}, 146 {1, tuple1, core.RelationTupleUpdate_TOUCH, nil, nil, nil}, 147 {1, tuple1, core.RelationTupleUpdate_TOUCH, nil, nil, nil}, 148 }, 149 []datastore.RevisionChanges{ 150 {Revision: rev1, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple1)}}, 151 }, 152 }, 153 { 154 "create then touch", 155 []changeEntry{ 156 {1, tuple1, core.RelationTupleUpdate_TOUCH, nil, nil, nil}, 157 {2, tuple1, core.RelationTupleUpdate_DELETE, nil, nil, nil}, 158 {2, tuple1, core.RelationTupleUpdate_TOUCH, nil, nil, nil}, 159 }, 160 []datastore.RevisionChanges{ 161 {Revision: rev1, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple1)}}, 162 {Revision: rev2, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple1)}}, 163 }, 164 }, 165 { 166 "big revision gap", 167 []changeEntry{ 168 {1, tuple1, core.RelationTupleUpdate_TOUCH, nil, nil, nil}, 169 {1_000_000, tuple1, core.RelationTupleUpdate_DELETE, nil, nil, nil}, 170 {1_000_000, tuple1, core.RelationTupleUpdate_TOUCH, nil, nil, nil}, 171 }, 172 []datastore.RevisionChanges{ 173 {Revision: rev1, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple1)}}, 174 {Revision: revOneMillion, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple1)}}, 175 }, 176 }, 177 { 178 "out of order", 179 []changeEntry{ 180 {1_000_000, tuple1, core.RelationTupleUpdate_TOUCH, nil, nil, nil}, 181 {1, tuple1, core.RelationTupleUpdate_TOUCH, nil, nil, nil}, 182 {1_000_000, tuple1, core.RelationTupleUpdate_DELETE, nil, nil, nil}, 183 }, 184 []datastore.RevisionChanges{ 185 {Revision: rev1, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple1)}}, 186 {Revision: revOneMillion, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple1)}}, 187 }, 188 }, 189 { 190 "changed then deleted namespace", 191 []changeEntry{ 192 {1, "", 0, nil, nil, []datastore.SchemaDefinition{&core.NamespaceDefinition{ 193 Name: "somenamespace", 194 }}}, 195 {1, "", 0, []string{"somenamespace"}, nil, nil}, 196 }, 197 []datastore.RevisionChanges{ 198 {Revision: rev1, DeletedNamespaces: []string{"somenamespace"}}, 199 }, 200 }, 201 { 202 "changed then deleted caveat", 203 []changeEntry{ 204 {1, "", 0, nil, nil, []datastore.SchemaDefinition{&core.CaveatDefinition{ 205 Name: "somecaveat", 206 }}}, 207 {1, "", 0, nil, []string{"somecaveat"}, nil}, 208 }, 209 []datastore.RevisionChanges{ 210 {Revision: rev1, DeletedCaveats: []string{"somecaveat"}}, 211 }, 212 }, 213 { 214 "deleted then changed namespace", 215 []changeEntry{ 216 {1, "", 0, []string{"somenamespace"}, nil, nil}, 217 {1, "", 0, nil, nil, []datastore.SchemaDefinition{&core.NamespaceDefinition{ 218 Name: "somenamespace", 219 }}}, 220 }, 221 []datastore.RevisionChanges{ 222 {Revision: rev1, ChangedDefinitions: []datastore.SchemaDefinition{&core.NamespaceDefinition{ 223 Name: "somenamespace", 224 }}}, 225 }, 226 }, 227 { 228 "deleted then changed caveat", 229 []changeEntry{ 230 {1, "", 0, nil, []string{"somecaveat"}, nil}, 231 {1, "", 0, nil, nil, []datastore.SchemaDefinition{&core.CaveatDefinition{ 232 Name: "somecaveat", 233 }}}, 234 }, 235 []datastore.RevisionChanges{ 236 {Revision: rev1, ChangedDefinitions: []datastore.SchemaDefinition{&core.CaveatDefinition{ 237 Name: "somecaveat", 238 }}}, 239 }, 240 }, 241 { 242 "changed namespace then deleted caveat", 243 []changeEntry{ 244 {1, "", 0, nil, nil, []datastore.SchemaDefinition{&core.NamespaceDefinition{ 245 Name: "somenamespaceorcaveat", 246 }}}, 247 {1, "", 0, nil, []string{"somenamespaceorcaveat"}, nil}, 248 }, 249 []datastore.RevisionChanges{ 250 {Revision: rev1, DeletedCaveats: []string{"somenamespaceorcaveat"}, ChangedDefinitions: []datastore.SchemaDefinition{&core.NamespaceDefinition{ 251 Name: "somenamespaceorcaveat", 252 }}}, 253 }, 254 }, 255 { 256 "kitchen sink relationships", 257 []changeEntry{ 258 {1, tuple1, core.RelationTupleUpdate_TOUCH, nil, nil, nil}, 259 {2, tuple1, core.RelationTupleUpdate_DELETE, nil, nil, nil}, 260 {1_000_000, tuple1, core.RelationTupleUpdate_TOUCH, nil, nil, nil}, 261 262 {1, tuple2, core.RelationTupleUpdate_DELETE, nil, nil, nil}, 263 {2, tuple2, core.RelationTupleUpdate_TOUCH, nil, nil, nil}, 264 {1_000_000, tuple2, core.RelationTupleUpdate_DELETE, nil, nil, nil}, 265 {1_000_000, tuple2, core.RelationTupleUpdate_TOUCH, nil, nil, nil}, 266 }, 267 []datastore.RevisionChanges{ 268 {Revision: rev1, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple1), del(tuple2)}}, 269 {Revision: rev2, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple2), del(tuple1)}}, 270 {Revision: revOneMillion, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple1), touch(tuple2)}}, 271 }, 272 }, 273 { 274 "kitchen sink", 275 []changeEntry{ 276 {1, tuple1, core.RelationTupleUpdate_TOUCH, nil, nil, nil}, 277 {2, tuple1, core.RelationTupleUpdate_DELETE, nil, nil, nil}, 278 {1_000_000, tuple1, core.RelationTupleUpdate_TOUCH, nil, nil, nil}, 279 {1_000_001, "", 0, []string{"deletednamespace"}, nil, nil}, 280 281 {3, "", 0, nil, nil, []datastore.SchemaDefinition{ 282 &core.NamespaceDefinition{Name: "midns"}, 283 }}, 284 285 {1, tuple2, core.RelationTupleUpdate_DELETE, nil, nil, nil}, 286 {2, tuple2, core.RelationTupleUpdate_TOUCH, nil, nil, nil}, 287 {1_000_000, tuple2, core.RelationTupleUpdate_DELETE, nil, nil, nil}, 288 {1_000_000, tuple2, core.RelationTupleUpdate_TOUCH, nil, nil, nil}, 289 {1_000_001, "", 0, nil, []string{"deletedcaveat"}, nil}, 290 }, 291 []datastore.RevisionChanges{ 292 {Revision: rev1, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple1), del(tuple2)}}, 293 {Revision: rev2, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple2), del(tuple1)}}, 294 {Revision: rev3, ChangedDefinitions: []datastore.SchemaDefinition{ 295 &core.NamespaceDefinition{Name: "midns"}, 296 }}, 297 {Revision: revOneMillion, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple1), touch(tuple2)}}, 298 {Revision: revOneMillionOne, DeletedNamespaces: []string{"deletednamespace"}, DeletedCaveats: []string{"deletedcaveat"}}, 299 }, 300 }, 301 } 302 303 for _, tc := range testCases { 304 tc := tc 305 t.Run(tc.name, func(t *testing.T) { 306 require := require.New(t) 307 308 ctx := context.Background() 309 ch := NewChanges(revisions.TransactionIDKeyFunc, datastore.WatchRelationships|datastore.WatchSchema) 310 for _, step := range tc.script { 311 if step.relationship != "" { 312 rel := tuple.MustParse(step.relationship) 313 err := ch.AddRelationshipChange(ctx, revisions.NewForTransactionID(step.revision), rel, step.op) 314 require.NoError(err) 315 } 316 317 for _, changed := range step.changedDefinitions { 318 ch.AddChangedDefinition(ctx, revisions.NewForTransactionID(step.revision), changed) 319 } 320 321 for _, ns := range step.deletedNamespaces { 322 ch.AddDeletedNamespace(ctx, revisions.NewForTransactionID(step.revision), ns) 323 } 324 325 for _, c := range step.deletedCaveats { 326 ch.AddDeletedCaveat(ctx, revisions.NewForTransactionID(step.revision), c) 327 } 328 } 329 330 require.Equal( 331 canonicalize(tc.expected), 332 canonicalize(ch.AsRevisionChanges(revisions.TransactionIDKeyLessThanFunc)), 333 ) 334 }) 335 } 336 } 337 338 func TestFilteredSchemaChanges(t *testing.T) { 339 ctx := context.Background() 340 ch := NewChanges(revisions.TransactionIDKeyFunc, datastore.WatchSchema) 341 require.True(t, ch.IsEmpty()) 342 343 require.NoError(t, ch.AddRelationshipChange(ctx, rev1, tuple.MustParse("document:firstdoc#viewer@user:tom"), core.RelationTupleUpdate_TOUCH)) 344 require.True(t, ch.IsEmpty()) 345 } 346 347 func TestFilteredRelationshipChanges(t *testing.T) { 348 ctx := context.Background() 349 ch := NewChanges(revisions.TransactionIDKeyFunc, datastore.WatchRelationships) 350 require.True(t, ch.IsEmpty()) 351 352 ch.AddDeletedNamespace(ctx, rev3, "deletedns3") 353 require.True(t, ch.IsEmpty()) 354 } 355 356 func TestFilterAndRemoveRevisionChanges(t *testing.T) { 357 ctx := context.Background() 358 ch := NewChanges(revisions.TransactionIDKeyFunc, datastore.WatchRelationships|datastore.WatchSchema) 359 360 require.True(t, ch.IsEmpty()) 361 362 ch.AddDeletedNamespace(ctx, rev1, "deletedns1") 363 ch.AddDeletedNamespace(ctx, rev2, "deletedns2") 364 ch.AddDeletedNamespace(ctx, rev3, "deletedns3") 365 366 require.False(t, ch.IsEmpty()) 367 368 results := ch.FilterAndRemoveRevisionChanges(revisions.TransactionIDKeyLessThanFunc, rev3) 369 require.Equal(t, 2, len(results)) 370 require.False(t, ch.IsEmpty()) 371 372 require.Equal(t, []datastore.RevisionChanges{ 373 { 374 Revision: rev1, 375 DeletedNamespaces: []string{"deletedns1"}, 376 DeletedCaveats: []string{}, 377 ChangedDefinitions: []datastore.SchemaDefinition{}, 378 }, 379 { 380 Revision: rev2, 381 DeletedNamespaces: []string{"deletedns2"}, 382 DeletedCaveats: []string{}, 383 ChangedDefinitions: []datastore.SchemaDefinition{}, 384 }, 385 }, results) 386 387 remaining := ch.AsRevisionChanges(revisions.TransactionIDKeyLessThanFunc) 388 require.Equal(t, 1, len(remaining)) 389 390 require.Equal(t, []datastore.RevisionChanges{ 391 { 392 Revision: rev3, 393 DeletedNamespaces: []string{"deletedns3"}, 394 DeletedCaveats: []string{}, 395 ChangedDefinitions: []datastore.SchemaDefinition{}, 396 }, 397 }, remaining) 398 399 results = ch.FilterAndRemoveRevisionChanges(revisions.TransactionIDKeyLessThanFunc, revOneMillion) 400 require.Equal(t, 1, len(results)) 401 require.True(t, ch.IsEmpty()) 402 403 results = ch.FilterAndRemoveRevisionChanges(revisions.TransactionIDKeyLessThanFunc, revOneMillionOne) 404 require.Equal(t, 0, len(results)) 405 require.True(t, ch.IsEmpty()) 406 } 407 408 func TestHLCOrdering(t *testing.T) { 409 ctx := context.Background() 410 411 ch := NewChanges(revisions.HLCKeyFunc, datastore.WatchRelationships|datastore.WatchSchema) 412 require.True(t, ch.IsEmpty()) 413 414 rev1, err := revisions.HLCRevisionFromString("1.0000000001") 415 require.NoError(t, err) 416 417 rev0, err := revisions.HLCRevisionFromString("1") 418 require.NoError(t, err) 419 420 err = ch.AddRelationshipChange(ctx, rev1, tuple.MustParse("document:foo#viewer@user:tom"), core.RelationTupleUpdate_DELETE) 421 require.NoError(t, err) 422 423 err = ch.AddRelationshipChange(ctx, rev0, tuple.MustParse("document:foo#viewer@user:tom"), core.RelationTupleUpdate_TOUCH) 424 require.NoError(t, err) 425 426 remaining := ch.AsRevisionChanges(revisions.HLCKeyLessThanFunc) 427 require.Equal(t, 2, len(remaining)) 428 429 require.Equal(t, []datastore.RevisionChanges{ 430 { 431 Revision: rev0, 432 RelationshipChanges: []*core.RelationTupleUpdate{ 433 tuple.Touch(tuple.MustParse("document:foo#viewer@user:tom")), 434 }, 435 DeletedNamespaces: []string{}, 436 DeletedCaveats: []string{}, 437 ChangedDefinitions: []datastore.SchemaDefinition{}, 438 }, 439 { 440 Revision: rev1, 441 RelationshipChanges: []*core.RelationTupleUpdate{ 442 tuple.Delete(tuple.MustParse("document:foo#viewer@user:tom")), 443 }, 444 DeletedNamespaces: []string{}, 445 DeletedCaveats: []string{}, 446 ChangedDefinitions: []datastore.SchemaDefinition{}, 447 }, 448 }, remaining) 449 } 450 451 func TestHLCSameRevision(t *testing.T) { 452 ctx := context.Background() 453 454 ch := NewChanges(revisions.HLCKeyFunc, datastore.WatchRelationships|datastore.WatchSchema) 455 require.True(t, ch.IsEmpty()) 456 457 rev0, err := revisions.HLCRevisionFromString("1") 458 require.NoError(t, err) 459 460 rev0again, err := revisions.HLCRevisionFromString("1") 461 require.NoError(t, err) 462 463 err = ch.AddRelationshipChange(ctx, rev0, tuple.MustParse("document:foo#viewer@user:tom"), core.RelationTupleUpdate_TOUCH) 464 require.NoError(t, err) 465 466 err = ch.AddRelationshipChange(ctx, rev0again, tuple.MustParse("document:foo#viewer@user:sarah"), core.RelationTupleUpdate_TOUCH) 467 require.NoError(t, err) 468 469 remaining := ch.AsRevisionChanges(revisions.HLCKeyLessThanFunc) 470 require.Equal(t, 1, len(remaining)) 471 472 expected := []*core.RelationTupleUpdate{ 473 tuple.Touch(tuple.MustParse("document:foo#viewer@user:tom")), 474 tuple.Touch(tuple.MustParse("document:foo#viewer@user:sarah")), 475 } 476 slices.SortFunc(expected, func(i, j *core.RelationTupleUpdate) int { 477 iStr := tuple.StringWithoutCaveat(i.Tuple) 478 jStr := tuple.StringWithoutCaveat(j.Tuple) 479 return strings.Compare(iStr, jStr) 480 }) 481 482 slices.SortFunc(remaining[0].RelationshipChanges, func(i, j *core.RelationTupleUpdate) int { 483 iStr := tuple.StringWithoutCaveat(i.Tuple) 484 jStr := tuple.StringWithoutCaveat(j.Tuple) 485 return strings.Compare(iStr, jStr) 486 }) 487 488 require.Equal(t, []datastore.RevisionChanges{ 489 { 490 Revision: rev0, 491 RelationshipChanges: expected, 492 DeletedNamespaces: []string{}, 493 DeletedCaveats: []string{}, 494 ChangedDefinitions: []datastore.SchemaDefinition{}, 495 }, 496 }, remaining) 497 } 498 499 func TestCanonicalize(t *testing.T) { 500 testCases := []struct { 501 name string 502 input, expected []datastore.RevisionChanges 503 }{ 504 { 505 "empty", 506 []datastore.RevisionChanges{}, 507 []datastore.RevisionChanges{}, 508 }, 509 { 510 "single entries", 511 []datastore.RevisionChanges{ 512 {Revision: rev1, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple1)}}, 513 }, 514 []datastore.RevisionChanges{ 515 {Revision: rev1, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple1)}}, 516 }, 517 }, 518 { 519 "tuples out of order", 520 []datastore.RevisionChanges{ 521 {Revision: rev1, RelationshipChanges: []*core.RelationTupleUpdate{del(tuple2), touch(tuple1)}}, 522 }, 523 []datastore.RevisionChanges{ 524 {Revision: rev1, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple1), del(tuple2)}}, 525 }, 526 }, 527 { 528 "operations out of order", 529 []datastore.RevisionChanges{ 530 {Revision: rev1, RelationshipChanges: []*core.RelationTupleUpdate{del(tuple1), touch(tuple1)}}, 531 }, 532 []datastore.RevisionChanges{ 533 {Revision: rev1, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple1), del(tuple1)}}, 534 }, 535 }, 536 { 537 "equal entries", 538 []datastore.RevisionChanges{ 539 {Revision: rev1, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple1), touch(tuple1)}}, 540 }, 541 []datastore.RevisionChanges{ 542 {Revision: rev1, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple1), touch(tuple1)}}, 543 }, 544 }, 545 { 546 "already canonical", 547 []datastore.RevisionChanges{ 548 {Revision: rev1, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple1), del(tuple2)}}, 549 {Revision: rev2, RelationshipChanges: []*core.RelationTupleUpdate{del(tuple1), touch(tuple2)}}, 550 {Revision: revOneMillion, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple1), touch(tuple2)}}, 551 }, 552 []datastore.RevisionChanges{ 553 {Revision: rev1, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple1), del(tuple2)}}, 554 {Revision: rev2, RelationshipChanges: []*core.RelationTupleUpdate{del(tuple1), touch(tuple2)}}, 555 {Revision: revOneMillion, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple1), touch(tuple2)}}, 556 }, 557 }, 558 { 559 "revisions allowed out of order", 560 []datastore.RevisionChanges{ 561 {Revision: revOneMillion, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple1), touch(tuple2)}}, 562 {Revision: rev2, RelationshipChanges: []*core.RelationTupleUpdate{del(tuple1), touch(tuple2)}}, 563 {Revision: rev1, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple1), del(tuple2)}}, 564 }, 565 []datastore.RevisionChanges{ 566 {Revision: revOneMillion, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple1), touch(tuple2)}}, 567 {Revision: rev2, RelationshipChanges: []*core.RelationTupleUpdate{del(tuple1), touch(tuple2)}}, 568 {Revision: rev1, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple1), del(tuple2)}}, 569 }, 570 }, 571 } 572 573 for _, tc := range testCases { 574 tc := tc 575 t.Run(tc.name, func(t *testing.T) { 576 require := require.New(t) 577 require.Equal(tc.expected, canonicalize(tc.input)) 578 }) 579 } 580 } 581 582 func touch(relationship string) *core.RelationTupleUpdate { 583 return &core.RelationTupleUpdate{ 584 Operation: core.RelationTupleUpdate_TOUCH, 585 Tuple: tuple.MustParse(relationship), 586 } 587 } 588 589 func del(relationship string) *core.RelationTupleUpdate { 590 return &core.RelationTupleUpdate{ 591 Operation: core.RelationTupleUpdate_DELETE, 592 Tuple: tuple.MustParse(relationship), 593 } 594 } 595 596 func canonicalize(in []datastore.RevisionChanges) []datastore.RevisionChanges { 597 out := make([]datastore.RevisionChanges, 0, len(in)) 598 599 for _, rev := range in { 600 outChanges := make([]*core.RelationTupleUpdate, 0, len(rev.RelationshipChanges)) 601 602 outChanges = append(outChanges, rev.RelationshipChanges...) 603 sort.Slice(outChanges, func(i, j int) bool { 604 // Return if i < j 605 left, right := outChanges[i], outChanges[j] 606 tupleCompareResult := strings.Compare(tuple.StringWithoutCaveat(left.Tuple), tuple.StringWithoutCaveat(right.Tuple)) 607 if tupleCompareResult < 0 { 608 return true 609 } 610 if tupleCompareResult > 0 { 611 return false 612 } 613 614 // Tuples are equal, sort by op 615 return left.Operation < right.Operation 616 }) 617 618 deletedNamespaces := rev.DeletedNamespaces 619 if len(rev.DeletedNamespaces) == 0 { 620 deletedNamespaces = nil 621 } else { 622 sort.Strings(deletedNamespaces) 623 } 624 625 deletedCaveats := rev.DeletedCaveats 626 if len(rev.DeletedCaveats) == 0 { 627 deletedCaveats = nil 628 } else { 629 sort.Strings(deletedCaveats) 630 } 631 632 changedDefinitions := rev.ChangedDefinitions 633 if len(rev.ChangedDefinitions) == 0 { 634 changedDefinitions = nil 635 } 636 637 out = append(out, datastore.RevisionChanges{ 638 Revision: rev.Revision, 639 RelationshipChanges: outChanges, 640 DeletedNamespaces: deletedNamespaces, 641 DeletedCaveats: deletedCaveats, 642 ChangedDefinitions: changedDefinitions, 643 }) 644 } 645 646 return out 647 }