github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/internal/datastore/mysql/datastore_test.go (about)

     1  //go:build ci && docker
     2  // +build ci,docker
     3  
     4  package mysql
     5  
     6  import (
     7  	"context"
     8  	"database/sql"
     9  	"fmt"
    10  	"testing"
    11  	"time"
    12  
    13  	sq "github.com/Masterminds/squirrel"
    14  	"github.com/go-sql-driver/mysql"
    15  	"github.com/prometheus/client_golang/prometheus"
    16  	"github.com/stretchr/testify/require"
    17  
    18  	"github.com/authzed/spicedb/internal/datastore/common"
    19  	"github.com/authzed/spicedb/internal/datastore/mysql/migrations"
    20  	"github.com/authzed/spicedb/internal/datastore/revisions"
    21  	"github.com/authzed/spicedb/internal/testfixtures"
    22  	testdatastore "github.com/authzed/spicedb/internal/testserver/datastore"
    23  	"github.com/authzed/spicedb/pkg/datastore"
    24  	"github.com/authzed/spicedb/pkg/datastore/test"
    25  	"github.com/authzed/spicedb/pkg/migrate"
    26  	"github.com/authzed/spicedb/pkg/namespace"
    27  	corev1 "github.com/authzed/spicedb/pkg/proto/core/v1"
    28  	"github.com/authzed/spicedb/pkg/tuple"
    29  )
    30  
    31  const (
    32  	chunkRelationshipCount = 2000
    33  )
    34  
    35  // Implement TestableDatastore interface
    36  func (mds *Datastore) ExampleRetryableError() error {
    37  	return &mysql.MySQLError{
    38  		Number: errMysqlDeadlock,
    39  	}
    40  }
    41  
    42  type datastoreTester struct {
    43  	b      testdatastore.RunningEngineForTest
    44  	t      *testing.T
    45  	prefix string
    46  }
    47  
    48  func (dst *datastoreTester) createDatastore(revisionQuantization, gcInterval, gcWindow time.Duration, _ uint16) (datastore.Datastore, error) {
    49  	ctx := context.Background()
    50  	ds := dst.b.NewDatastore(dst.t, func(engine, uri string) datastore.Datastore {
    51  		ds, err := newMySQLDatastore(ctx, uri,
    52  			RevisionQuantization(revisionQuantization),
    53  			GCWindow(gcWindow),
    54  			GCInterval(gcInterval),
    55  			TablePrefix(dst.prefix),
    56  			DebugAnalyzeBeforeStatistics(),
    57  			OverrideLockWaitTimeout(1),
    58  		)
    59  		require.NoError(dst.t, err)
    60  		return ds
    61  	})
    62  	_, err := ds.ReadyState(context.Background())
    63  	require.NoError(dst.t, err)
    64  	return ds, nil
    65  }
    66  
    67  func failOnError(t *testing.T, f func() error) {
    68  	require.NoError(t, f())
    69  }
    70  
    71  var defaultOptions = []Option{
    72  	RevisionQuantization(0 * time.Millisecond),
    73  	GCWindow(1 * time.Millisecond),
    74  	GCInterval(0 * time.Second),
    75  	DebugAnalyzeBeforeStatistics(),
    76  	OverrideLockWaitTimeout(1),
    77  }
    78  
    79  type datastoreTestFunc func(t *testing.T, ds datastore.Datastore)
    80  
    81  func createDatastoreTest(b testdatastore.RunningEngineForTest, tf datastoreTestFunc, options ...Option) func(*testing.T) {
    82  	return func(t *testing.T) {
    83  		ctx := context.Background()
    84  		ds := b.NewDatastore(t, func(engine, uri string) datastore.Datastore {
    85  			ds, err := newMySQLDatastore(ctx, uri, options...)
    86  			require.NoError(t, err)
    87  			return ds
    88  		})
    89  		defer failOnError(t, ds.Close)
    90  
    91  		tf(t, ds)
    92  	}
    93  }
    94  
    95  func TestMySQLDatastoreDSNWithoutParseTime(t *testing.T) {
    96  	_, err := NewMySQLDatastore(context.Background(), "root:password@(localhost:1234)/mysql")
    97  	require.ErrorContains(t, err, "https://spicedb.dev/d/parse-time-mysql")
    98  }
    99  
   100  func TestMySQL8Datastore(t *testing.T) {
   101  	b := testdatastore.RunMySQLForTestingWithOptions(t, testdatastore.MySQLTesterOptions{MigrateForNewDatastore: true}, "")
   102  	dst := datastoreTester{b: b, t: t}
   103  	test.AllWithExceptions(t, test.DatastoreTesterFunc(dst.createDatastore), test.WithCategories(test.WatchSchemaCategory, test.WatchCheckpointsCategory))
   104  	additionalMySQLTests(t, b)
   105  }
   106  
   107  func additionalMySQLTests(t *testing.T, b testdatastore.RunningEngineForTest) {
   108  	reg := prometheus.NewRegistry()
   109  	prometheus.DefaultGatherer = reg
   110  	prometheus.DefaultRegisterer = reg
   111  
   112  	t.Run("DatabaseSeeding", createDatastoreTest(b, DatabaseSeedingTest))
   113  	t.Run("PrometheusCollector", createDatastoreTest(
   114  		b,
   115  		PrometheusCollectorTest,
   116  		WithEnablePrometheusStats(true),
   117  	))
   118  	t.Run("GarbageCollection", createDatastoreTest(b, GarbageCollectionTest, defaultOptions...))
   119  	t.Run("GarbageCollectionByTime", createDatastoreTest(b, GarbageCollectionByTimeTest, defaultOptions...))
   120  	t.Run("ChunkedGarbageCollection", createDatastoreTest(b, ChunkedGarbageCollectionTest, defaultOptions...))
   121  	t.Run("EmptyGarbageCollection", createDatastoreTest(b, EmptyGarbageCollectionTest, defaultOptions...))
   122  	t.Run("NoRelationshipsGarbageCollection", createDatastoreTest(b, NoRelationshipsGarbageCollectionTest, defaultOptions...))
   123  	t.Run("TransactionTimestamps", createDatastoreTest(b, TransactionTimestampsTest, defaultOptions...))
   124  	t.Run("QuantizedRevisions", func(t *testing.T) {
   125  		QuantizedRevisionTest(t, b)
   126  	})
   127  }
   128  
   129  func DatabaseSeedingTest(t *testing.T, ds datastore.Datastore) {
   130  	req := require.New(t)
   131  
   132  	// ensure datastore is seeded right after initialization
   133  	ctx := context.Background()
   134  	isSeeded, err := ds.(*Datastore).isSeeded(ctx)
   135  	req.NoError(err)
   136  	req.True(isSeeded, "expected datastore to be seeded after initialization")
   137  
   138  	r, err := ds.ReadyState(ctx)
   139  	req.NoError(err)
   140  	req.True(r.IsReady)
   141  }
   142  
   143  func PrometheusCollectorTest(t *testing.T, ds datastore.Datastore) {
   144  	req := require.New(t)
   145  
   146  	// cause some use of the SQL connection pool to generate metrics
   147  	_, err := ds.ReadyState(context.Background())
   148  	req.NoError(err)
   149  
   150  	metrics, err := prometheus.DefaultGatherer.Gather()
   151  	req.NoError(err, metrics)
   152  	var collectorStatsFound, connectorStatsFound bool
   153  	for _, metric := range metrics {
   154  		if metric.GetName() == "go_sql_stats_connections_open" {
   155  			collectorStatsFound = true
   156  		}
   157  		if metric.GetName() == "spicedb_datastore_mysql_connect_count_total" {
   158  			connectorStatsFound = true
   159  		}
   160  	}
   161  	req.True(collectorStatsFound, "mysql datastore did not issue prometheus metrics")
   162  	req.True(connectorStatsFound, "mysql datastore connector did not issue prometheus metrics")
   163  }
   164  
   165  func GarbageCollectionTest(t *testing.T, ds datastore.Datastore) {
   166  	req := require.New(t)
   167  
   168  	ctx := context.Background()
   169  	r, err := ds.ReadyState(ctx)
   170  	req.NoError(err)
   171  	req.True(r.IsReady)
   172  
   173  	// Write basic namespaces.
   174  	writtenAt, err := ds.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error {
   175  		return rwt.WriteNamespaces(
   176  			ctx,
   177  			namespace.Namespace(
   178  				"resource",
   179  				namespace.MustRelation("reader", nil),
   180  			),
   181  			namespace.Namespace("user"),
   182  		)
   183  	})
   184  	req.NoError(err)
   185  
   186  	// Run GC at the transaction and ensure no relationships are removed.
   187  	mds := ds.(*Datastore)
   188  
   189  	removed, err := mds.DeleteBeforeTx(ctx, writtenAt)
   190  	req.NoError(err)
   191  	req.Zero(removed.Relationships)
   192  	req.Zero(removed.Namespaces)
   193  
   194  	// Replace the namespace with a new one.
   195  	writtenAt, err = ds.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error {
   196  		return rwt.WriteNamespaces(
   197  			ctx,
   198  			namespace.Namespace(
   199  				"resource",
   200  				namespace.MustRelation("reader", nil),
   201  				namespace.MustRelation("unused", nil),
   202  			),
   203  			namespace.Namespace("user"),
   204  		)
   205  	})
   206  	req.NoError(err)
   207  
   208  	// Run GC to remove the old namespace
   209  	removed, err = mds.DeleteBeforeTx(ctx, writtenAt)
   210  	req.NoError(err)
   211  	req.Zero(removed.Relationships)
   212  	req.Equal(int64(1), removed.Transactions)
   213  	req.Equal(int64(2), removed.Namespaces)
   214  
   215  	// Write a relationship.
   216  	tpl := tuple.Parse("resource:someresource#reader@user:someuser#...")
   217  	relWrittenAt, err := common.WriteTuples(ctx, ds, corev1.RelationTupleUpdate_CREATE, tpl)
   218  	req.NoError(err)
   219  
   220  	// Run GC at the transaction and ensure no relationships are removed, but 1 transaction (the previous write namespace) is.
   221  	removed, err = mds.DeleteBeforeTx(ctx, relWrittenAt)
   222  	req.NoError(err)
   223  	req.Zero(removed.Relationships)
   224  	req.Equal(int64(1), removed.Transactions)
   225  	req.Zero(removed.Namespaces)
   226  
   227  	// Run GC again and ensure there are no changes.
   228  	removed, err = mds.DeleteBeforeTx(ctx, relWrittenAt)
   229  	req.NoError(err)
   230  	req.Zero(removed.Relationships)
   231  	req.Zero(removed.Transactions)
   232  	req.Zero(removed.Namespaces)
   233  
   234  	// Ensure the relationship is still present.
   235  	tRequire := testfixtures.TupleChecker{Require: req, DS: ds}
   236  	tRequire.TupleExists(ctx, tpl, relWrittenAt)
   237  
   238  	// Overwrite the relationship.
   239  	ctpl := tuple.MustWithCaveat(tpl, "somecaveat")
   240  	relOverwrittenAt, err := common.WriteTuples(ctx, ds, corev1.RelationTupleUpdate_TOUCH, ctpl)
   241  	req.NoError(err)
   242  
   243  	// Run GC at the transaction and ensure the (older copy of the) relationship is removed, as well as 1 transaction (the write).
   244  	removed, err = mds.DeleteBeforeTx(ctx, relOverwrittenAt)
   245  	req.NoError(err)
   246  	req.Equal(int64(1), removed.Relationships)
   247  	req.Equal(int64(1), removed.Transactions)
   248  	req.Zero(removed.Namespaces)
   249  
   250  	// Run GC again and ensure there are no changes.
   251  	removed, err = mds.DeleteBeforeTx(ctx, relOverwrittenAt)
   252  	req.NoError(err)
   253  	req.Zero(removed.Relationships)
   254  	req.Zero(removed.Transactions)
   255  	req.Zero(removed.Namespaces)
   256  
   257  	// Ensure the relationship is still present.
   258  	tRequire.TupleExists(ctx, ctpl, relOverwrittenAt)
   259  
   260  	// Delete the relationship.
   261  	relDeletedAt, err := common.WriteTuples(ctx, ds, corev1.RelationTupleUpdate_DELETE, ctpl)
   262  	req.NoError(err)
   263  
   264  	// Ensure the relationship is gone.
   265  	tRequire.NoTupleExists(ctx, ctpl, relDeletedAt)
   266  
   267  	// Run GC at the transaction and ensure the relationship is removed, as well as 1 transaction (the overwrite).
   268  	removed, err = mds.DeleteBeforeTx(ctx, relDeletedAt)
   269  	req.NoError(err)
   270  	req.Equal(int64(1), removed.Relationships)
   271  	req.Equal(int64(1), removed.Transactions)
   272  	req.Zero(removed.Namespaces)
   273  
   274  	// Run GC again and ensure there are no changes.
   275  	removed, err = mds.DeleteBeforeTx(ctx, relDeletedAt)
   276  	req.NoError(err)
   277  	req.Zero(removed.Relationships)
   278  	req.Zero(removed.Transactions)
   279  	req.Zero(removed.Namespaces)
   280  
   281  	// Write the relationship a few times.
   282  	ctpl1 := tuple.MustWithCaveat(tpl, "somecaveat1")
   283  	ctpl2 := tuple.MustWithCaveat(tpl, "somecaveat2")
   284  	ctpl3 := tuple.MustWithCaveat(tpl, "somecaveat3")
   285  	_, err = common.WriteTuples(ctx, ds, corev1.RelationTupleUpdate_TOUCH, ctpl1)
   286  	req.NoError(err)
   287  
   288  	_, err = common.WriteTuples(ctx, ds, corev1.RelationTupleUpdate_TOUCH, ctpl2)
   289  	req.NoError(err)
   290  
   291  	relLastWriteAt, err := common.WriteTuples(ctx, ds, corev1.RelationTupleUpdate_TOUCH, ctpl3)
   292  	req.NoError(err)
   293  
   294  	// Run GC at the transaction and ensure the older copies of the relationships are removed,
   295  	// as well as the 2 older write transactions and the older delete transaction.
   296  	removed, err = mds.DeleteBeforeTx(ctx, relLastWriteAt)
   297  	req.NoError(err)
   298  	req.Equal(int64(2), removed.Relationships)
   299  	req.Equal(int64(3), removed.Transactions)
   300  	req.Zero(removed.Namespaces)
   301  
   302  	// Ensure the relationship is still present.
   303  	tRequire.TupleExists(ctx, ctpl3, relLastWriteAt)
   304  }
   305  
   306  func GarbageCollectionByTimeTest(t *testing.T, ds datastore.Datastore) {
   307  	req := require.New(t)
   308  
   309  	ctx := context.Background()
   310  	r, err := ds.ReadyState(ctx)
   311  	req.NoError(err)
   312  	req.True(r.IsReady)
   313  
   314  	// Write basic namespaces.
   315  	_, err = ds.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error {
   316  		return rwt.WriteNamespaces(
   317  			ctx,
   318  			namespace.Namespace(
   319  				"resource",
   320  				namespace.MustRelation("reader", nil),
   321  			),
   322  			namespace.Namespace("user"),
   323  		)
   324  	})
   325  	req.NoError(err)
   326  
   327  	mds := ds.(*Datastore)
   328  
   329  	// Sleep 1ms to ensure GC will delete the previous transaction.
   330  	time.Sleep(1 * time.Millisecond)
   331  
   332  	// Write a relationship.
   333  	tpl := tuple.Parse("resource:someresource#reader@user:someuser#...")
   334  
   335  	relLastWriteAt, err := common.WriteTuples(ctx, ds, corev1.RelationTupleUpdate_CREATE, tpl)
   336  	req.NoError(err)
   337  
   338  	// Run GC and ensure only transactions were removed.
   339  	afterWrite, err := mds.Now(ctx)
   340  	req.NoError(err)
   341  
   342  	afterWriteTx, err := mds.TxIDBefore(ctx, afterWrite)
   343  	req.NoError(err)
   344  
   345  	removed, err := mds.DeleteBeforeTx(ctx, afterWriteTx)
   346  	req.NoError(err)
   347  	req.Zero(removed.Relationships)
   348  	req.NotZero(removed.Transactions)
   349  	req.Zero(removed.Namespaces)
   350  
   351  	// Ensure the relationship is still present.
   352  	tRequire := testfixtures.TupleChecker{Require: req, DS: ds}
   353  	tRequire.TupleExists(ctx, tpl, relLastWriteAt)
   354  
   355  	// Sleep 1ms to ensure GC will delete the previous write.
   356  	time.Sleep(1 * time.Millisecond)
   357  
   358  	// Delete the relationship.
   359  	relDeletedAt, err := common.WriteTuples(ctx, ds, corev1.RelationTupleUpdate_DELETE, tpl)
   360  	req.NoError(err)
   361  
   362  	// Run GC and ensure the relationship is removed.
   363  	afterDelete, err := mds.Now(ctx)
   364  	req.NoError(err)
   365  
   366  	afterDeleteTx, err := mds.TxIDBefore(ctx, afterDelete)
   367  	req.NoError(err)
   368  
   369  	removed, err = mds.DeleteBeforeTx(ctx, afterDeleteTx)
   370  	req.NoError(err)
   371  	req.Equal(int64(1), removed.Relationships)
   372  	req.Equal(int64(1), removed.Transactions)
   373  	req.Zero(removed.Namespaces)
   374  
   375  	// Ensure the relationship is still not present.
   376  	tRequire.NoTupleExists(ctx, tpl, relDeletedAt)
   377  }
   378  
   379  func EmptyGarbageCollectionTest(t *testing.T, ds datastore.Datastore) {
   380  	req := require.New(t)
   381  
   382  	ctx := context.Background()
   383  	r, err := ds.ReadyState(ctx)
   384  	req.NoError(err)
   385  	req.True(r.IsReady)
   386  
   387  	gc := ds.(common.GarbageCollector)
   388  
   389  	now, err := gc.Now(ctx)
   390  	req.NoError(err)
   391  
   392  	watermark, err := gc.TxIDBefore(ctx, now.Add(-1*time.Minute))
   393  	req.NoError(err)
   394  
   395  	collected, err := gc.DeleteBeforeTx(ctx, watermark)
   396  	req.NoError(err)
   397  
   398  	req.Equal(int64(0), collected.Relationships)
   399  	req.Equal(int64(0), collected.Transactions)
   400  	req.Equal(int64(0), collected.Namespaces)
   401  }
   402  
   403  func NoRelationshipsGarbageCollectionTest(t *testing.T, ds datastore.Datastore) {
   404  	req := require.New(t)
   405  
   406  	ctx := context.Background()
   407  	r, err := ds.ReadyState(ctx)
   408  	req.NoError(err)
   409  	req.True(r.IsReady)
   410  
   411  	// Write basic namespaces.
   412  	_, err = ds.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error {
   413  		return rwt.WriteNamespaces(
   414  			ctx,
   415  			namespace.Namespace(
   416  				"resource",
   417  				namespace.MustRelation("reader", nil),
   418  			),
   419  			namespace.Namespace("user"),
   420  		)
   421  	})
   422  	req.NoError(err)
   423  
   424  	gc := ds.(common.GarbageCollector)
   425  
   426  	now, err := gc.Now(ctx)
   427  	req.NoError(err)
   428  
   429  	watermark, err := gc.TxIDBefore(ctx, now.Add(-1*time.Minute))
   430  	req.NoError(err)
   431  
   432  	collected, err := gc.DeleteBeforeTx(ctx, watermark)
   433  	req.NoError(err)
   434  
   435  	req.Equal(int64(0), collected.Relationships)
   436  	req.Equal(int64(0), collected.Transactions)
   437  	req.Equal(int64(0), collected.Namespaces)
   438  }
   439  
   440  func ChunkedGarbageCollectionTest(t *testing.T, ds datastore.Datastore) {
   441  	req := require.New(t)
   442  
   443  	ctx := context.Background()
   444  	r, err := ds.ReadyState(ctx)
   445  	req.NoError(err)
   446  	req.True(r.IsReady)
   447  
   448  	// Write basic namespaces.
   449  	_, err = ds.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error {
   450  		return rwt.WriteNamespaces(
   451  			ctx,
   452  			namespace.Namespace(
   453  				"resource",
   454  				namespace.MustRelation("reader", nil),
   455  			),
   456  			namespace.Namespace("user"),
   457  		)
   458  	})
   459  	req.NoError(err)
   460  
   461  	mds := ds.(*Datastore)
   462  
   463  	// Prepare relationships to write.
   464  	var tuples []*corev1.RelationTuple
   465  	for i := 0; i < chunkRelationshipCount; i++ {
   466  		tpl := tuple.Parse(fmt.Sprintf("resource:resource-%d#reader@user:someuser#...", i))
   467  		tuples = append(tuples, tpl)
   468  	}
   469  
   470  	// Write a large number of relationships.
   471  	writtenAt, err := common.WriteTuples(ctx, ds, corev1.RelationTupleUpdate_CREATE, tuples...)
   472  	req.NoError(err)
   473  
   474  	// Ensure the relationships were written.
   475  	tRequire := testfixtures.TupleChecker{Require: req, DS: ds}
   476  	for _, tpl := range tuples {
   477  		tRequire.TupleExists(ctx, tpl, writtenAt)
   478  	}
   479  
   480  	// Run GC and ensure only transactions were removed.
   481  	afterWrite, err := mds.Now(ctx)
   482  	req.NoError(err)
   483  
   484  	afterWriteTx, err := mds.TxIDBefore(ctx, afterWrite)
   485  	req.NoError(err)
   486  
   487  	removed, err := mds.DeleteBeforeTx(ctx, afterWriteTx)
   488  	req.NoError(err)
   489  	req.Zero(removed.Relationships)
   490  	req.NotZero(removed.Transactions)
   491  	req.Zero(removed.Namespaces)
   492  
   493  	// Sleep to ensure the relationships will GC.
   494  	time.Sleep(1 * time.Millisecond)
   495  
   496  	// Delete all the relationships.
   497  	deletedAt, err := common.WriteTuples(ctx, ds, corev1.RelationTupleUpdate_DELETE, tuples...)
   498  	req.NoError(err)
   499  
   500  	// Ensure the relationships were deleted.
   501  	for _, tpl := range tuples {
   502  		tRequire.NoTupleExists(ctx, tpl, deletedAt)
   503  	}
   504  
   505  	// Sleep to ensure GC.
   506  	time.Sleep(1 * time.Millisecond)
   507  
   508  	// Run GC and ensure all the stale relationships are removed.
   509  	afterDelete, err := mds.Now(ctx)
   510  	req.NoError(err)
   511  
   512  	afterDeleteTx, err := mds.TxIDBefore(ctx, afterDelete)
   513  	req.NoError(err)
   514  
   515  	removed, err = mds.DeleteBeforeTx(ctx, afterDeleteTx)
   516  	req.NoError(err)
   517  	req.Equal(int64(chunkRelationshipCount), removed.Relationships)
   518  	req.Equal(int64(1), removed.Transactions)
   519  	req.Zero(removed.Namespaces)
   520  }
   521  
   522  func QuantizedRevisionTest(t *testing.T, b testdatastore.RunningEngineForTest) {
   523  	testCases := []struct {
   524  		testName         string
   525  		quantization     time.Duration
   526  		relativeTimes    []time.Duration
   527  		expectedRevision uint64
   528  	}{
   529  		{
   530  			"DefaultRevision",
   531  			1 * time.Second,
   532  			[]time.Duration{},
   533  			1,
   534  		},
   535  		{
   536  			"OnlyPastRevisions",
   537  			1 * time.Second,
   538  			[]time.Duration{-2 * time.Second},
   539  			2,
   540  		},
   541  		{
   542  			"OnlyFutureRevisions",
   543  			1 * time.Second,
   544  			[]time.Duration{2 * time.Second},
   545  			2,
   546  		},
   547  		{
   548  			"QuantizedLower",
   549  			1 * time.Second,
   550  			[]time.Duration{-2 * time.Second, -1 * time.Nanosecond, 0},
   551  			3,
   552  		},
   553  		{
   554  			"QuantizationDisabled",
   555  			1 * time.Nanosecond,
   556  			[]time.Duration{-2 * time.Second, -1 * time.Nanosecond, 0},
   557  			4,
   558  		},
   559  	}
   560  
   561  	for _, tc := range testCases {
   562  		t.Run(tc.testName, func(t *testing.T) {
   563  			require := require.New(t)
   564  			ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
   565  			defer cancel()
   566  
   567  			ds := b.NewDatastore(t, func(engine, uri string) datastore.Datastore {
   568  				ds, err := newMySQLDatastore(
   569  					ctx,
   570  					uri,
   571  					RevisionQuantization(5*time.Second),
   572  					GCWindow(24*time.Hour),
   573  					WatchBufferLength(1),
   574  				)
   575  				require.NoError(err)
   576  				return ds
   577  			})
   578  			mds := ds.(*Datastore)
   579  
   580  			dbNow, err := mds.Now(ctx)
   581  			require.NoError(err)
   582  
   583  			tx, err := mds.db.BeginTx(ctx, nil)
   584  			require.NoError(err)
   585  
   586  			if len(tc.relativeTimes) > 0 {
   587  				bulkWrite := sb.Insert(mds.driver.RelationTupleTransaction()).Columns(colTimestamp)
   588  
   589  				for _, offset := range tc.relativeTimes {
   590  					bulkWrite = bulkWrite.Values(dbNow.Add(offset))
   591  				}
   592  
   593  				sql, args, err := bulkWrite.ToSql()
   594  				require.NoError(err)
   595  
   596  				_, err = tx.ExecContext(ctx, sql, args...)
   597  				require.NoError(err)
   598  			}
   599  
   600  			queryRevision := fmt.Sprintf(
   601  				querySelectRevision,
   602  				colID,
   603  				mds.driver.RelationTupleTransaction(),
   604  				colTimestamp,
   605  				tc.quantization.Nanoseconds(),
   606  			)
   607  
   608  			var revision uint64
   609  			var validFor time.Duration
   610  			err = tx.QueryRowContext(ctx, queryRevision).Scan(&revision, &validFor)
   611  			require.NoError(err)
   612  			require.Greater(validFor, time.Duration(0))
   613  			require.LessOrEqual(validFor, tc.quantization.Nanoseconds())
   614  			require.Equal(tc.expectedRevision, revision)
   615  		})
   616  	}
   617  }
   618  
   619  // From https://dev.mysql.com/doc/refman/8.0/en/datetime.html
   620  // By default, the current time zone for each connection is the server's time.
   621  // The time zone can be set on a per-connection basis.
   622  func TransactionTimestampsTest(t *testing.T, ds datastore.Datastore) {
   623  	req := require.New(t)
   624  
   625  	// Setting db default time zone to before UTC
   626  	ctx := context.Background()
   627  	db := ds.(*Datastore).db
   628  	_, err := db.ExecContext(ctx, "SET GLOBAL time_zone = 'America/New_York';")
   629  	req.NoError(err)
   630  
   631  	r, err := ds.ReadyState(ctx)
   632  	req.NoError(err)
   633  	req.True(r.IsReady)
   634  
   635  	// Get timestamp in UTC as reference
   636  	startTimeUTC, err := ds.(*Datastore).Now(ctx)
   637  	req.NoError(err)
   638  
   639  	// Transaction timestamp should not be stored in system time zone
   640  	tx, err := db.BeginTx(ctx, nil)
   641  	req.NoError(err)
   642  	txID, err := ds.(*Datastore).createNewTransaction(ctx, tx)
   643  	req.NoError(err)
   644  	err = tx.Commit()
   645  	req.NoError(err)
   646  
   647  	var ts time.Time
   648  	query, args, err := sb.Select(colTimestamp).From(ds.(*Datastore).driver.RelationTupleTransaction()).Where(sq.Eq{colID: txID}).ToSql()
   649  	req.NoError(err)
   650  	err = db.QueryRowContext(ctx, query, args...).Scan(&ts)
   651  	req.NoError(err)
   652  
   653  	// Let's make sure both Now() and transactionCreated() have timezones aligned
   654  	req.True(ts.Sub(startTimeUTC) < 5*time.Minute)
   655  
   656  	revision, err := ds.OptimizedRevision(ctx)
   657  	req.NoError(err)
   658  	req.Equal(revisions.NewForTransactionID(txID), revision)
   659  }
   660  
   661  func TestMySQLMigrations(t *testing.T) {
   662  	req := require.New(t)
   663  
   664  	db := datastoreDB(t, false)
   665  	migrationDriver := migrations.NewMySQLDriverFromDB(db, "")
   666  
   667  	version, err := migrationDriver.Version(context.Background())
   668  	req.NoError(err)
   669  	req.Equal("", version)
   670  
   671  	err = migrations.Manager.Run(context.Background(), migrationDriver, migrate.Head, migrate.LiveRun)
   672  	req.NoError(err)
   673  
   674  	version, err = migrationDriver.Version(context.Background())
   675  	req.NoError(err)
   676  
   677  	headVersion, err := migrations.Manager.HeadRevision()
   678  	req.NoError(err)
   679  	req.Equal(headVersion, version)
   680  }
   681  
   682  func TestMySQLMigrationsWithPrefix(t *testing.T) {
   683  	req := require.New(t)
   684  
   685  	prefix := "spicedb_"
   686  	db := datastoreDB(t, false)
   687  	migrationDriver := migrations.NewMySQLDriverFromDB(db, prefix)
   688  
   689  	version, err := migrationDriver.Version(context.Background())
   690  	req.NoError(err)
   691  	req.Equal("", version)
   692  
   693  	err = migrations.Manager.Run(context.Background(), migrationDriver, migrate.Head, migrate.LiveRun)
   694  	req.NoError(err)
   695  
   696  	version, err = migrationDriver.Version(context.Background())
   697  	req.NoError(err)
   698  
   699  	headVersion, err := migrations.Manager.HeadRevision()
   700  	req.NoError(err)
   701  	req.Equal(headVersion, version)
   702  
   703  	rows, err := db.Query("SHOW TABLES;")
   704  	req.NoError(err)
   705  
   706  	for rows.Next() {
   707  		var tbl string
   708  		req.NoError(rows.Scan(&tbl))
   709  		req.Contains(tbl, prefix)
   710  	}
   711  	req.NoError(rows.Err())
   712  }
   713  
   714  func TestMySQLWithAWSIAMCredentialsProvider(t *testing.T) {
   715  	// set up the environment, so we don't make any external calls to AWS
   716  	t.Setenv("AWS_CONFIG_FILE", "file_not_exists")
   717  	t.Setenv("AWS_SHARED_CREDENTIALS_FILE", "file_not_exists")
   718  	t.Setenv("AWS_ENDPOINT_URL", "http://169.254.169.254/aws")
   719  	t.Setenv("AWS_ACCESS_KEY", "access_key")
   720  	t.Setenv("AWS_SECRET_KEY", "secret_key")
   721  	t.Setenv("AWS_REGION", "us-east-1")
   722  
   723  	// initialize the datastore using the AWS IAM credentials provider, and point it to a database that does not exist
   724  	_, err := NewMySQLDatastore(context.Background(), "root:password@(localhost:1234)/mysql?parseTime=True&tls=skip-verify", CredentialsProviderName("aws-iam"))
   725  
   726  	// we expect the connection attempt to fail
   727  	// which means that the credentials provider was wired and called successfully before making the connection attempt
   728  	require.ErrorContains(t, err, ":1234: connect: connection refused")
   729  }
   730  
   731  func datastoreDB(t *testing.T, migrate bool) *sql.DB {
   732  	var databaseURI string
   733  	testdatastore.RunMySQLForTestingWithOptions(t, testdatastore.MySQLTesterOptions{MigrateForNewDatastore: migrate}, "").NewDatastore(t, func(engine, uri string) datastore.Datastore {
   734  		databaseURI = uri
   735  		return nil
   736  	})
   737  
   738  	db, err := sql.Open("mysql", databaseURI)
   739  	require.NoError(t, err)
   740  	return db
   741  }