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  }