github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/pkg/datastore/test/pagination.go (about) 1 package test 2 3 import ( 4 "context" 5 "fmt" 6 "sort" 7 "testing" 8 9 "github.com/samber/lo" 10 "github.com/stretchr/testify/require" 11 12 "github.com/authzed/spicedb/internal/testfixtures" 13 "github.com/authzed/spicedb/pkg/datastore" 14 "github.com/authzed/spicedb/pkg/datastore/options" 15 core "github.com/authzed/spicedb/pkg/proto/core/v1" 16 "github.com/authzed/spicedb/pkg/tuple" 17 ) 18 19 func OrderingTest(t *testing.T, tester DatastoreTester) { 20 testCases := []struct { 21 resourceType string 22 ordering options.SortOrder 23 }{ 24 {testfixtures.DocumentNS.Name, options.ByResource}, 25 {testfixtures.FolderNS.Name, options.ByResource}, 26 {testfixtures.UserNS.Name, options.ByResource}, 27 28 {testfixtures.DocumentNS.Name, options.BySubject}, 29 {testfixtures.FolderNS.Name, options.BySubject}, 30 {testfixtures.UserNS.Name, options.BySubject}, 31 } 32 33 rawDS, err := tester.New(0, veryLargeGCInterval, veryLargeGCWindow, 1) 34 require.NoError(t, err) 35 36 ds, rev := testfixtures.StandardDatastoreWithData(rawDS, require.New(t)) 37 tRequire := testfixtures.TupleChecker{Require: require.New(t), DS: ds} 38 39 for _, tc := range testCases { 40 tc := tc 41 42 t.Run(fmt.Sprintf("%s-%d", tc.resourceType, tc.ordering), func(t *testing.T) { 43 require := require.New(t) 44 ctx := context.Background() 45 46 expected := sortedStandardData(tc.resourceType, tc.ordering) 47 48 // Check the snapshot reader order 49 iter, err := ds.SnapshotReader(rev).QueryRelationships(ctx, datastore.RelationshipsFilter{ 50 OptionalResourceType: tc.resourceType, 51 }, options.WithSort(tc.ordering)) 52 53 require.NoError(err) 54 defer iter.Close() 55 56 cursor, err := iter.Cursor() 57 require.Empty(cursor) 58 require.ErrorIs(err, datastore.ErrCursorEmpty) 59 60 tRequire.VerifyOrderedIteratorResults(iter, expected...) 61 62 cursor, err = iter.Cursor() 63 if len(expected) > 0 { 64 require.NotEmpty(cursor) 65 require.NoError(err) 66 } else { 67 require.Empty(cursor) 68 require.ErrorIs(err, datastore.ErrCursorEmpty) 69 } 70 71 // Check a reader from with a transaction 72 _, err = ds.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error { 73 iter, err := rwt.QueryRelationships(ctx, datastore.RelationshipsFilter{ 74 OptionalResourceType: tc.resourceType, 75 }, options.WithSort(tc.ordering)) 76 require.NoError(err) 77 defer iter.Close() 78 79 cursor, err := iter.Cursor() 80 require.Empty(cursor) 81 require.ErrorIs(err, datastore.ErrCursorEmpty) 82 83 tRequire.VerifyOrderedIteratorResults(iter, expected...) 84 85 cursor, err = iter.Cursor() 86 if len(expected) > 0 { 87 require.NotEmpty(cursor) 88 require.NoError(err) 89 } else { 90 require.Empty(cursor) 91 require.ErrorIs(err, datastore.ErrCursorEmpty) 92 } 93 94 return nil 95 }) 96 require.NoError(err) 97 }) 98 } 99 } 100 101 func LimitTest(t *testing.T, tester DatastoreTester) { 102 testCases := []string{ 103 testfixtures.DocumentNS.Name, 104 testfixtures.UserNS.Name, 105 testfixtures.FolderNS.Name, 106 } 107 108 rawDS, err := tester.New(0, veryLargeGCInterval, veryLargeGCWindow, 1) 109 require.NoError(t, err) 110 111 ds, rev := testfixtures.StandardDatastoreWithData(rawDS, require.New(t)) 112 tRequire := testfixtures.TupleChecker{Require: require.New(t), DS: ds} 113 114 for _, objectType := range testCases { 115 expected := sortedStandardData(objectType, options.ByResource) 116 for limit := 1; limit <= len(expected)+1; limit++ { 117 testLimit := uint64(limit) 118 t.Run(fmt.Sprintf("%s-%d", objectType, limit), func(t *testing.T) { 119 require := require.New(t) 120 ctx := context.Background() 121 122 foreachTxType(ctx, ds, rev, func(reader datastore.Reader) { 123 iter, err := reader.QueryRelationships(ctx, datastore.RelationshipsFilter{ 124 OptionalResourceType: objectType, 125 }, options.WithLimit(&testLimit)) 126 127 require.NoError(err) 128 defer iter.Close() 129 130 expectedCount := limit 131 if expectedCount > len(expected) { 132 expectedCount = len(expected) 133 } 134 135 cursor, err := iter.Cursor() 136 require.Empty(cursor) 137 require.ErrorIs(err, datastore.ErrCursorsWithoutSorting) 138 139 tRequire.VerifyIteratorCount(iter, expectedCount) 140 141 cursor, err = iter.Cursor() 142 require.Empty(cursor) 143 require.ErrorIs(err, datastore.ErrCursorsWithoutSorting) 144 }) 145 }) 146 } 147 } 148 } 149 150 type ( 151 iterator func(ctx context.Context, reader datastore.Reader, limit uint64, cursor options.Cursor) (datastore.RelationshipIterator, error) 152 ) 153 154 func forwardIterator(resourceType string, _ options.SortOrder) iterator { 155 return func(ctx context.Context, reader datastore.Reader, limit uint64, cursor options.Cursor) (datastore.RelationshipIterator, error) { 156 return reader.QueryRelationships(ctx, datastore.RelationshipsFilter{ 157 OptionalResourceType: resourceType, 158 }, options.WithSort(options.ByResource), options.WithLimit(&limit), options.WithAfter(cursor)) 159 } 160 } 161 162 func reverseIterator(subjectType string, _ options.SortOrder) iterator { 163 return func(ctx context.Context, reader datastore.Reader, limit uint64, cursor options.Cursor) (datastore.RelationshipIterator, error) { 164 return reader.ReverseQueryRelationships(ctx, datastore.SubjectsFilter{ 165 SubjectType: subjectType, 166 }, options.WithSortForReverse(options.ByResource), options.WithLimitForReverse(&limit), options.WithAfterForReverse(cursor)) 167 } 168 } 169 170 var orderedTestCases = []struct { 171 name string 172 objectType string 173 sortOrder options.SortOrder 174 isForwardQuery bool 175 }{ 176 { 177 "document resources by resource", 178 testfixtures.DocumentNS.Name, 179 options.ByResource, 180 true, 181 }, 182 { 183 "folder resources by resource", 184 testfixtures.FolderNS.Name, 185 options.ByResource, 186 true, 187 }, 188 { 189 "user resources by resource", 190 testfixtures.UserNS.Name, 191 options.ByResource, 192 true, 193 }, 194 { 195 "resources with user subject", 196 testfixtures.UserNS.Name, 197 options.ByResource, 198 false, 199 }, 200 } 201 202 func OrderedLimitTest(t *testing.T, tester DatastoreTester) { 203 rawDS, err := tester.New(0, veryLargeGCInterval, veryLargeGCWindow, 1) 204 require.NoError(t, err) 205 206 ds, rev := testfixtures.StandardDatastoreWithData(rawDS, require.New(t)) 207 tRequire := testfixtures.TupleChecker{Require: require.New(t), DS: ds} 208 209 for _, tc := range orderedTestCases { 210 expected := sortedStandardData(tc.objectType, tc.sortOrder) 211 if !tc.isForwardQuery { 212 expected = sortedStandardDataBySubject(tc.objectType, tc.sortOrder) 213 } 214 215 for limit := 1; limit <= len(expected); limit++ { 216 testLimit := uint64(limit) 217 218 t.Run(tc.name, func(t *testing.T) { 219 require := require.New(t) 220 ctx := context.Background() 221 222 foreachTxType(ctx, ds, rev, func(reader datastore.Reader) { 223 var iter datastore.RelationshipIterator 224 var err error 225 226 if tc.isForwardQuery { 227 iter, err = forwardIterator(tc.objectType, tc.sortOrder)(ctx, reader, testLimit, nil) 228 } else { 229 iter, err = reverseIterator(tc.objectType, tc.sortOrder)(ctx, reader, testLimit, nil) 230 } 231 232 require.NoError(err) 233 defer iter.Close() 234 235 cursor, err := iter.Cursor() 236 require.Empty(cursor) 237 require.ErrorIs(err, datastore.ErrCursorEmpty) 238 239 tRequire.VerifyOrderedIteratorResults(iter, expected[0:limit]...) 240 241 cursor, err = iter.Cursor() 242 if len(expected) > 0 { 243 require.NotEmpty(cursor) 244 require.NoError(err) 245 } else { 246 require.Empty(cursor) 247 require.ErrorIs(err, datastore.ErrCursorEmpty) 248 } 249 }) 250 }) 251 } 252 } 253 } 254 255 func ResumeTest(t *testing.T, tester DatastoreTester) { 256 rawDS, err := tester.New(0, veryLargeGCInterval, veryLargeGCWindow, 1) 257 require.NoError(t, err) 258 259 ds, rev := testfixtures.StandardDatastoreWithData(rawDS, require.New(t)) 260 tRequire := testfixtures.TupleChecker{Require: require.New(t), DS: ds} 261 262 for _, tc := range orderedTestCases { 263 expected := sortedStandardData(tc.objectType, tc.sortOrder) 264 if !tc.isForwardQuery { 265 expected = sortedStandardDataBySubject(tc.objectType, tc.sortOrder) 266 } 267 268 for batchSize := 1; batchSize <= len(expected); batchSize++ { 269 testLimit := uint64(batchSize) 270 expected := expected 271 272 t.Run(fmt.Sprintf("%s-batches-%d", tc.name, batchSize), func(t *testing.T) { 273 require := require.New(t) 274 ctx := context.Background() 275 276 foreachTxType(ctx, ds, rev, func(reader datastore.Reader) { 277 cursor := options.Cursor(nil) 278 for offset := 0; offset <= len(expected); offset += batchSize { 279 var iter datastore.RelationshipIterator 280 var err error 281 282 if tc.isForwardQuery { 283 iter, err = forwardIterator(tc.objectType, tc.sortOrder)(ctx, reader, testLimit, cursor) 284 } else { 285 iter, err = reverseIterator(tc.objectType, tc.sortOrder)(ctx, reader, testLimit, cursor) 286 } 287 288 require.NoError(err) 289 defer iter.Close() 290 291 emptyCursor, err := iter.Cursor() 292 require.Empty(emptyCursor) 293 require.ErrorIs(err, datastore.ErrCursorEmpty) 294 295 upperBound := offset + batchSize 296 if upperBound > len(expected) { 297 upperBound = len(expected) 298 } 299 300 tRequire.VerifyOrderedIteratorResults(iter, expected[offset:upperBound]...) 301 302 cursor, err = iter.Cursor() 303 if upperBound-offset > 0 { 304 require.NotEmpty(cursor) 305 require.NoError(err) 306 } else { 307 require.Empty(cursor) 308 require.ErrorIs(err, datastore.ErrCursorEmpty) 309 } 310 } 311 }) 312 }) 313 } 314 } 315 } 316 317 func CursorErrorsTest(t *testing.T, tester DatastoreTester) { 318 testCases := []struct { 319 order options.SortOrder 320 defaultCursorError error 321 }{ 322 {options.Unsorted, datastore.ErrCursorsWithoutSorting}, 323 {options.ByResource, nil}, 324 } 325 326 rawDS, err := tester.New(0, veryLargeGCInterval, veryLargeGCWindow, 1) 327 require.NoError(t, err) 328 329 ds, rev := testfixtures.StandardDatastoreWithData(rawDS, require.New(t)) 330 ctx := context.Background() 331 332 for _, tc := range testCases { 333 t.Run(fmt.Sprintf("Order-%d", tc.order), func(t *testing.T) { 334 require := require.New(t) 335 336 foreachTxType(ctx, ds, rev, func(reader datastore.Reader) { 337 iter, err := reader.QueryRelationships(ctx, datastore.RelationshipsFilter{ 338 OptionalResourceType: testfixtures.DocumentNS.Name, 339 }, options.WithSort(tc.order)) 340 require.NoError(err) 341 require.NotNil(iter) 342 defer iter.Close() 343 344 cursor, err := iter.Cursor() 345 require.Nil(cursor) 346 if tc.defaultCursorError != nil { 347 require.ErrorIs(err, tc.defaultCursorError) 348 } else { 349 require.ErrorIs(err, datastore.ErrCursorEmpty) 350 } 351 352 val := iter.Next() 353 require.NotNil(val) 354 355 cursor, err = iter.Cursor() 356 if tc.defaultCursorError != nil { 357 require.Nil(cursor) 358 require.ErrorIs(err, tc.defaultCursorError) 359 } else { 360 require.NotNil(cursor) 361 require.Nil(err) 362 } 363 364 iter.Close() 365 valAfterClose := iter.Next() 366 require.Nil(valAfterClose) 367 368 err = iter.Err() 369 require.ErrorIs(err, datastore.ErrClosedIterator) 370 cursorAfterClose, err := iter.Cursor() 371 require.Nil(cursorAfterClose) 372 require.ErrorIs(err, datastore.ErrClosedIterator) 373 }) 374 }) 375 } 376 } 377 378 func foreachTxType( 379 ctx context.Context, 380 ds datastore.Datastore, 381 snapshotRev datastore.Revision, 382 fn func(reader datastore.Reader), 383 ) { 384 reader := ds.SnapshotReader(snapshotRev) 385 fn(reader) 386 387 _, _ = ds.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error { 388 fn(rwt) 389 return nil 390 }) 391 } 392 393 func sortedStandardData(resourceType string, order options.SortOrder) []*core.RelationTuple { 394 asTuples := lo.Map(testfixtures.StandardTuples, func(item string, _ int) *core.RelationTuple { 395 return tuple.Parse(item) 396 }) 397 398 filteredToType := lo.Filter(asTuples, func(item *core.RelationTuple, _ int) bool { 399 return item.ResourceAndRelation.Namespace == resourceType 400 }) 401 402 sort.Slice(filteredToType, func(i, j int) bool { 403 lhsResource := tuple.StringONR(filteredToType[i].ResourceAndRelation) 404 lhsSubject := tuple.StringONR(filteredToType[i].Subject) 405 rhsResource := tuple.StringONR(filteredToType[j].ResourceAndRelation) 406 rhsSubject := tuple.StringONR(filteredToType[j].Subject) 407 switch order { 408 case options.ByResource: 409 return lhsResource < rhsResource || (lhsResource == rhsResource && lhsSubject < rhsSubject) 410 case options.BySubject: 411 return lhsSubject < rhsSubject || (lhsSubject == rhsSubject && lhsResource < rhsResource) 412 default: 413 panic("request for sorted test data with no sort order") 414 } 415 }) 416 417 return filteredToType 418 } 419 420 func sortedStandardDataBySubject(subjectType string, order options.SortOrder) []*core.RelationTuple { 421 asTuples := lo.Map(testfixtures.StandardTuples, func(item string, _ int) *core.RelationTuple { 422 return tuple.Parse(item) 423 }) 424 425 filteredToType := lo.Filter(asTuples, func(item *core.RelationTuple, _ int) bool { 426 if subjectType == "" { 427 return true 428 } 429 return item.Subject.Namespace == subjectType 430 }) 431 432 sort.Slice(filteredToType, func(i, j int) bool { 433 lhsResource := tuple.StringONR(filteredToType[i].ResourceAndRelation) 434 lhsSubject := tuple.StringONR(filteredToType[i].Subject) 435 rhsResource := tuple.StringONR(filteredToType[j].ResourceAndRelation) 436 rhsSubject := tuple.StringONR(filteredToType[j].Subject) 437 switch order { 438 case options.ByResource: 439 return lhsResource < rhsResource || (lhsResource == rhsResource && lhsSubject < rhsSubject) 440 default: 441 panic("request for sorted test data with no sort order") 442 } 443 }) 444 445 return filteredToType 446 }