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 }