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  }