github.com/openfga/openfga@v1.5.4-rc1/pkg/storage/mysql/mysql.go (about) 1 package mysql 2 3 import ( 4 "context" 5 "database/sql" 6 "encoding/json" 7 "errors" 8 "fmt" 9 "strings" 10 "time" 11 12 sq "github.com/Masterminds/squirrel" 13 "github.com/cenkalti/backoff/v4" 14 "github.com/go-sql-driver/mysql" 15 openfgav1 "github.com/openfga/api/proto/openfga/v1" 16 "github.com/prometheus/client_golang/prometheus" 17 "github.com/prometheus/client_golang/prometheus/collectors" 18 "go.opentelemetry.io/otel" 19 "go.uber.org/zap" 20 "google.golang.org/protobuf/proto" 21 "google.golang.org/protobuf/types/known/structpb" 22 "google.golang.org/protobuf/types/known/timestamppb" 23 24 "github.com/openfga/openfga/pkg/logger" 25 "github.com/openfga/openfga/pkg/storage" 26 "github.com/openfga/openfga/pkg/storage/sqlcommon" 27 tupleUtils "github.com/openfga/openfga/pkg/tuple" 28 ) 29 30 var tracer = otel.Tracer("openfga/pkg/storage/mysql") 31 32 // MySQL provides a MySQL based implementation of [storage.OpenFGADatastore]. 33 type MySQL struct { 34 stbl sq.StatementBuilderType 35 db *sql.DB 36 dbInfo *sqlcommon.DBInfo 37 logger logger.Logger 38 dbStatsCollector prometheus.Collector 39 maxTuplesPerWriteField int 40 maxTypesPerModelField int 41 } 42 43 // Ensures that MySQL implements the OpenFGADatastore interface. 44 var _ storage.OpenFGADatastore = (*MySQL)(nil) 45 46 // New creates a new [MySQL] storage. 47 func New(uri string, cfg *sqlcommon.Config) (*MySQL, error) { 48 if cfg.Username != "" || cfg.Password != "" { 49 dsnCfg, err := mysql.ParseDSN(uri) 50 if err != nil { 51 return nil, fmt.Errorf("parse mysql connection dsn: %w", err) 52 } 53 54 if cfg.Username != "" { 55 dsnCfg.User = cfg.Username 56 } 57 if cfg.Password != "" { 58 dsnCfg.Passwd = cfg.Password 59 } 60 61 uri = dsnCfg.FormatDSN() 62 } 63 64 db, err := sql.Open("mysql", uri) 65 if err != nil { 66 return nil, fmt.Errorf("initialize mysql connection: %w", err) 67 } 68 69 if cfg.MaxOpenConns != 0 { 70 db.SetMaxOpenConns(cfg.MaxOpenConns) 71 } 72 73 if cfg.MaxIdleConns != 0 { 74 db.SetMaxIdleConns(cfg.MaxIdleConns) 75 } 76 77 if cfg.ConnMaxIdleTime != 0 { 78 db.SetConnMaxIdleTime(cfg.ConnMaxIdleTime) 79 } 80 81 if cfg.ConnMaxLifetime != 0 { 82 db.SetConnMaxLifetime(cfg.ConnMaxLifetime) 83 } 84 85 policy := backoff.NewExponentialBackOff() 86 policy.MaxElapsedTime = 1 * time.Minute 87 attempt := 1 88 err = backoff.Retry(func() error { 89 err = db.PingContext(context.Background()) 90 if err != nil { 91 cfg.Logger.Info("waiting for mysql", zap.Int("attempt", attempt)) 92 attempt++ 93 return err 94 } 95 return nil 96 }, policy) 97 if err != nil { 98 return nil, fmt.Errorf("ping db: %w", err) 99 } 100 101 var collector prometheus.Collector 102 if cfg.ExportMetrics { 103 collector = collectors.NewDBStatsCollector(db, "openfga") 104 if err := prometheus.Register(collector); err != nil { 105 return nil, fmt.Errorf("initialize metrics: %w", err) 106 } 107 } 108 109 stbl := sq.StatementBuilder.RunWith(db) 110 dbInfo := sqlcommon.NewDBInfo(db, stbl, sq.Expr("NOW()")) 111 112 return &MySQL{ 113 stbl: stbl, 114 db: db, 115 dbInfo: dbInfo, 116 logger: cfg.Logger, 117 dbStatsCollector: collector, 118 maxTuplesPerWriteField: cfg.MaxTuplesPerWriteField, 119 maxTypesPerModelField: cfg.MaxTypesPerModelField, 120 }, nil 121 } 122 123 // Close see [storage.OpenFGADatastore].Close. 124 func (m *MySQL) Close() { 125 if m.dbStatsCollector != nil { 126 prometheus.Unregister(m.dbStatsCollector) 127 } 128 m.db.Close() 129 } 130 131 // Read see [storage.RelationshipTupleReader].Read. 132 func (m *MySQL) Read(ctx context.Context, store string, tupleKey *openfgav1.TupleKey) (storage.TupleIterator, error) { 133 ctx, span := tracer.Start(ctx, "mysql.Read") 134 defer span.End() 135 136 return m.read(ctx, store, tupleKey, nil) 137 } 138 139 // ReadPage see [storage.RelationshipTupleReader].ReadPage. 140 func (m *MySQL) ReadPage( 141 ctx context.Context, 142 store string, 143 tupleKey *openfgav1.TupleKey, 144 opts storage.PaginationOptions, 145 ) ([]*openfgav1.Tuple, []byte, error) { 146 ctx, span := tracer.Start(ctx, "mysql.ReadPage") 147 defer span.End() 148 149 iter, err := m.read(ctx, store, tupleKey, &opts) 150 if err != nil { 151 return nil, nil, err 152 } 153 defer iter.Stop() 154 155 return iter.ToArray(opts) 156 } 157 158 func (m *MySQL) read(ctx context.Context, store string, tupleKey *openfgav1.TupleKey, opts *storage.PaginationOptions) (*sqlcommon.SQLTupleIterator, error) { 159 ctx, span := tracer.Start(ctx, "mysql.read") 160 defer span.End() 161 162 sb := m.stbl. 163 Select( 164 "store", "object_type", "object_id", "relation", "_user", 165 "condition_name", "condition_context", "ulid", "inserted_at", 166 ). 167 From("tuple"). 168 Where(sq.Eq{"store": store}) 169 if opts != nil { 170 sb = sb.OrderBy("ulid") 171 } 172 objectType, objectID := tupleUtils.SplitObject(tupleKey.GetObject()) 173 if objectType != "" { 174 sb = sb.Where(sq.Eq{"object_type": objectType}) 175 } 176 if objectID != "" { 177 sb = sb.Where(sq.Eq{"object_id": objectID}) 178 } 179 if tupleKey.GetRelation() != "" { 180 sb = sb.Where(sq.Eq{"relation": tupleKey.GetRelation()}) 181 } 182 if tupleKey.GetUser() != "" { 183 sb = sb.Where(sq.Eq{"_user": tupleKey.GetUser()}) 184 } 185 if opts != nil && opts.From != "" { 186 token, err := sqlcommon.UnmarshallContToken(opts.From) 187 if err != nil { 188 return nil, err 189 } 190 sb = sb.Where(sq.GtOrEq{"ulid": token.Ulid}) 191 } 192 if opts != nil && opts.PageSize != 0 { 193 sb = sb.Limit(uint64(opts.PageSize + 1)) // + 1 is used to determine whether to return a continuation token. 194 } 195 196 rows, err := sb.QueryContext(ctx) 197 if err != nil { 198 return nil, sqlcommon.HandleSQLError(err) 199 } 200 201 return sqlcommon.NewSQLTupleIterator(rows), nil 202 } 203 204 // Write see [storage.RelationshipTupleWriter].Write. 205 func (m *MySQL) Write(ctx context.Context, store string, deletes storage.Deletes, writes storage.Writes) error { 206 ctx, span := tracer.Start(ctx, "mysql.Write") 207 defer span.End() 208 209 if len(deletes)+len(writes) > m.MaxTuplesPerWrite() { 210 return storage.ErrExceededWriteBatchLimit 211 } 212 213 now := time.Now().UTC() 214 215 return sqlcommon.Write(ctx, m.dbInfo, store, deletes, writes, now) 216 } 217 218 // ReadUserTuple see [storage.RelationshipTupleReader].ReadUserTuple. 219 func (m *MySQL) ReadUserTuple(ctx context.Context, store string, tupleKey *openfgav1.TupleKey) (*openfgav1.Tuple, error) { 220 ctx, span := tracer.Start(ctx, "mysql.ReadUserTuple") 221 defer span.End() 222 223 objectType, objectID := tupleUtils.SplitObject(tupleKey.GetObject()) 224 userType := tupleUtils.GetUserTypeFromUser(tupleKey.GetUser()) 225 226 var conditionName sql.NullString 227 var conditionContext []byte 228 var record storage.TupleRecord 229 err := m.stbl. 230 Select( 231 "object_type", "object_id", "relation", "_user", 232 "condition_name", "condition_context", 233 ). 234 From("tuple"). 235 Where(sq.Eq{ 236 "store": store, 237 "object_type": objectType, 238 "object_id": objectID, 239 "relation": tupleKey.GetRelation(), 240 "_user": tupleKey.GetUser(), 241 "user_type": userType, 242 }). 243 QueryRowContext(ctx). 244 Scan( 245 &record.ObjectType, 246 &record.ObjectID, 247 &record.Relation, 248 &record.User, 249 &conditionName, 250 &conditionContext, 251 ) 252 if err != nil { 253 return nil, sqlcommon.HandleSQLError(err) 254 } 255 256 if conditionName.String != "" { 257 record.ConditionName = conditionName.String 258 259 if conditionContext != nil { 260 var conditionContextStruct structpb.Struct 261 if err := proto.Unmarshal(conditionContext, &conditionContextStruct); err != nil { 262 return nil, err 263 } 264 record.ConditionContext = &conditionContextStruct 265 } 266 } 267 268 return record.AsTuple(), nil 269 } 270 271 // ReadUsersetTuples see [storage.RelationshipTupleReader].ReadUsersetTuples. 272 func (m *MySQL) ReadUsersetTuples( 273 ctx context.Context, 274 store string, 275 filter storage.ReadUsersetTuplesFilter, 276 ) (storage.TupleIterator, error) { 277 ctx, span := tracer.Start(ctx, "mysql.ReadUsersetTuples") 278 defer span.End() 279 280 sb := m.stbl. 281 Select( 282 "store", "object_type", "object_id", "relation", "_user", 283 "condition_name", "condition_context", "ulid", "inserted_at", 284 ). 285 From("tuple"). 286 Where(sq.Eq{"store": store}). 287 Where(sq.Eq{"user_type": tupleUtils.UserSet}) 288 289 objectType, objectID := tupleUtils.SplitObject(filter.Object) 290 if objectType != "" { 291 sb = sb.Where(sq.Eq{"object_type": objectType}) 292 } 293 if objectID != "" { 294 sb = sb.Where(sq.Eq{"object_id": objectID}) 295 } 296 if filter.Relation != "" { 297 sb = sb.Where(sq.Eq{"relation": filter.Relation}) 298 } 299 if len(filter.AllowedUserTypeRestrictions) > 0 { 300 orConditions := sq.Or{} 301 for _, userset := range filter.AllowedUserTypeRestrictions { 302 if _, ok := userset.GetRelationOrWildcard().(*openfgav1.RelationReference_Relation); ok { 303 orConditions = append(orConditions, sq.Like{"_user": userset.GetType() + ":%#" + userset.GetRelation()}) 304 } 305 if _, ok := userset.GetRelationOrWildcard().(*openfgav1.RelationReference_Wildcard); ok { 306 orConditions = append(orConditions, sq.Eq{"_user": userset.GetType() + ":*"}) 307 } 308 } 309 sb = sb.Where(orConditions) 310 } 311 rows, err := sb.QueryContext(ctx) 312 if err != nil { 313 return nil, sqlcommon.HandleSQLError(err) 314 } 315 316 return sqlcommon.NewSQLTupleIterator(rows), nil 317 } 318 319 // ReadStartingWithUser see [storage.RelationshipTupleReader].ReadStartingWithUser. 320 func (m *MySQL) ReadStartingWithUser( 321 ctx context.Context, 322 store string, 323 opts storage.ReadStartingWithUserFilter, 324 ) (storage.TupleIterator, error) { 325 ctx, span := tracer.Start(ctx, "mysql.ReadStartingWithUser") 326 defer span.End() 327 328 var targetUsersArg []string 329 for _, u := range opts.UserFilter { 330 targetUser := u.GetObject() 331 if u.GetRelation() != "" { 332 targetUser = strings.Join([]string{u.GetObject(), u.GetRelation()}, "#") 333 } 334 targetUsersArg = append(targetUsersArg, targetUser) 335 } 336 337 rows, err := m.stbl. 338 Select( 339 "store", "object_type", "object_id", "relation", "_user", 340 "condition_name", "condition_context", "ulid", "inserted_at", 341 ). 342 From("tuple"). 343 Where(sq.Eq{ 344 "store": store, 345 "object_type": opts.ObjectType, 346 "relation": opts.Relation, 347 "_user": targetUsersArg, 348 }).QueryContext(ctx) 349 if err != nil { 350 return nil, sqlcommon.HandleSQLError(err) 351 } 352 353 return sqlcommon.NewSQLTupleIterator(rows), nil 354 } 355 356 // MaxTuplesPerWrite see [storage.RelationshipTupleWriter].MaxTuplesPerWrite. 357 func (m *MySQL) MaxTuplesPerWrite() int { 358 return m.maxTuplesPerWriteField 359 } 360 361 // ReadAuthorizationModel see [storage.AuthorizationModelReadBackend].ReadAuthorizationModel. 362 func (m *MySQL) ReadAuthorizationModel(ctx context.Context, store string, modelID string) (*openfgav1.AuthorizationModel, error) { 363 ctx, span := tracer.Start(ctx, "mysql.ReadAuthorizationModel") 364 defer span.End() 365 366 return sqlcommon.ReadAuthorizationModel(ctx, m.dbInfo, store, modelID) 367 } 368 369 // ReadAuthorizationModels see [storage.AuthorizationModelReadBackend].ReadAuthorizationModels. 370 func (m *MySQL) ReadAuthorizationModels( 371 ctx context.Context, 372 store string, 373 opts storage.PaginationOptions, 374 ) ([]*openfgav1.AuthorizationModel, []byte, error) { 375 ctx, span := tracer.Start(ctx, "mysql.ReadAuthorizationModels") 376 defer span.End() 377 378 sb := m.stbl.Select("authorization_model_id"). 379 Distinct(). 380 From("authorization_model"). 381 Where(sq.Eq{"store": store}). 382 OrderBy("authorization_model_id desc") 383 384 if opts.From != "" { 385 token, err := sqlcommon.UnmarshallContToken(opts.From) 386 if err != nil { 387 return nil, nil, err 388 } 389 sb = sb.Where(sq.LtOrEq{"authorization_model_id": token.Ulid}) 390 } 391 if opts.PageSize > 0 { 392 sb = sb.Limit(uint64(opts.PageSize + 1)) // + 1 is used to determine whether to return a continuation token. 393 } 394 395 rows, err := sb.QueryContext(ctx) 396 if err != nil { 397 return nil, nil, sqlcommon.HandleSQLError(err) 398 } 399 defer rows.Close() 400 401 var modelIDs []string 402 var modelID string 403 404 for rows.Next() { 405 err = rows.Scan(&modelID) 406 if err != nil { 407 return nil, nil, sqlcommon.HandleSQLError(err) 408 } 409 410 modelIDs = append(modelIDs, modelID) 411 } 412 413 if err := rows.Err(); err != nil { 414 return nil, nil, sqlcommon.HandleSQLError(err) 415 } 416 417 var token []byte 418 numModelIDs := len(modelIDs) 419 if len(modelIDs) > opts.PageSize { 420 numModelIDs = opts.PageSize 421 token, err = json.Marshal(sqlcommon.NewContToken(modelID, "")) 422 if err != nil { 423 return nil, nil, err 424 } 425 } 426 427 // TODO: make this concurrent with a maximum of 5 goroutines. This may be helpful: 428 // https://stackoverflow.com/questions/25306073/always-have-x-number-of-goroutines-running-at-any-time 429 models := make([]*openfgav1.AuthorizationModel, 0, numModelIDs) 430 // We use numModelIDs here to avoid retrieving possibly one extra model. 431 for i := 0; i < numModelIDs; i++ { 432 model, err := m.ReadAuthorizationModel(ctx, store, modelIDs[i]) 433 if err != nil { 434 return nil, nil, err 435 } 436 models = append(models, model) 437 } 438 439 return models, token, nil 440 } 441 442 // FindLatestAuthorizationModel see [storage.AuthorizationModelReadBackend].FindLatestAuthorizationModel. 443 func (m *MySQL) FindLatestAuthorizationModel(ctx context.Context, store string) (*openfgav1.AuthorizationModel, error) { 444 ctx, span := tracer.Start(ctx, "mysql.FindLatestAuthorizationModel") 445 defer span.End() 446 447 return sqlcommon.FindLatestAuthorizationModel(ctx, m.dbInfo, store) 448 } 449 450 // MaxTypesPerAuthorizationModel see [storage.TypeDefinitionWriteBackend].MaxTypesPerAuthorizationModel. 451 func (m *MySQL) MaxTypesPerAuthorizationModel() int { 452 return m.maxTypesPerModelField 453 } 454 455 // WriteAuthorizationModel see [storage.TypeDefinitionWriteBackend].WriteAuthorizationModel. 456 func (m *MySQL) WriteAuthorizationModel(ctx context.Context, store string, model *openfgav1.AuthorizationModel) error { 457 ctx, span := tracer.Start(ctx, "mysql.WriteAuthorizationModel") 458 defer span.End() 459 460 typeDefinitions := model.GetTypeDefinitions() 461 462 if len(typeDefinitions) > m.MaxTypesPerAuthorizationModel() { 463 return storage.ExceededMaxTypeDefinitionsLimitError(m.maxTypesPerModelField) 464 } 465 466 return sqlcommon.WriteAuthorizationModel(ctx, m.dbInfo, store, model) 467 } 468 469 // CreateStore adds a new store to the MySQL storage. 470 func (m *MySQL) CreateStore(ctx context.Context, store *openfgav1.Store) (*openfgav1.Store, error) { 471 ctx, span := tracer.Start(ctx, "mysql.CreateStore") 472 defer span.End() 473 474 txn, err := m.db.BeginTx(ctx, &sql.TxOptions{}) 475 if err != nil { 476 return nil, sqlcommon.HandleSQLError(err) 477 } 478 defer func() { 479 _ = txn.Rollback() 480 }() 481 482 _, err = m.stbl. 483 Insert("store"). 484 Columns("id", "name", "created_at", "updated_at"). 485 Values(store.GetId(), store.GetName(), sq.Expr("NOW()"), sq.Expr("NOW()")). 486 RunWith(txn). 487 ExecContext(ctx) 488 if err != nil { 489 return nil, sqlcommon.HandleSQLError(err) 490 } 491 492 var createdAt time.Time 493 var id, name string 494 err = m.stbl. 495 Select("id", "name", "created_at"). 496 From("store"). 497 Where(sq.Eq{"id": store.GetId()}). 498 RunWith(txn). 499 QueryRowContext(ctx). 500 Scan(&id, &name, &createdAt) 501 if err != nil { 502 return nil, sqlcommon.HandleSQLError(err) 503 } 504 505 err = txn.Commit() 506 if err != nil { 507 return nil, sqlcommon.HandleSQLError(err) 508 } 509 510 return &openfgav1.Store{ 511 Id: id, 512 Name: name, 513 CreatedAt: timestamppb.New(createdAt), 514 UpdatedAt: timestamppb.New(createdAt), 515 }, nil 516 } 517 518 // GetStore retrieves the details of a specific store from the MySQL using its storeID. 519 func (m *MySQL) GetStore(ctx context.Context, id string) (*openfgav1.Store, error) { 520 ctx, span := tracer.Start(ctx, "mysql.GetStore") 521 defer span.End() 522 523 row := m.stbl. 524 Select("id", "name", "created_at", "updated_at"). 525 From("store"). 526 Where(sq.Eq{ 527 "id": id, 528 "deleted_at": nil, 529 }). 530 QueryRowContext(ctx) 531 532 var storeID, name string 533 var createdAt, updatedAt time.Time 534 err := row.Scan(&storeID, &name, &createdAt, &updatedAt) 535 if err != nil { 536 if errors.Is(err, sql.ErrNoRows) { 537 return nil, storage.ErrNotFound 538 } 539 return nil, sqlcommon.HandleSQLError(err) 540 } 541 542 return &openfgav1.Store{ 543 Id: storeID, 544 Name: name, 545 CreatedAt: timestamppb.New(createdAt), 546 UpdatedAt: timestamppb.New(updatedAt), 547 }, nil 548 } 549 550 // ListStores provides a paginated list of all stores present in the MySQL storage. 551 func (m *MySQL) ListStores(ctx context.Context, opts storage.PaginationOptions) ([]*openfgav1.Store, []byte, error) { 552 ctx, span := tracer.Start(ctx, "mysql.ListStores") 553 defer span.End() 554 555 sb := m.stbl. 556 Select("id", "name", "created_at", "updated_at"). 557 From("store"). 558 Where(sq.Eq{"deleted_at": nil}). 559 OrderBy("id") 560 561 if opts.From != "" { 562 token, err := sqlcommon.UnmarshallContToken(opts.From) 563 if err != nil { 564 return nil, nil, err 565 } 566 sb = sb.Where(sq.GtOrEq{"id": token.Ulid}) 567 } 568 if opts.PageSize > 0 { 569 sb = sb.Limit(uint64(opts.PageSize + 1)) // + 1 is used to determine whether to return a continuation token. 570 } 571 572 rows, err := sb.QueryContext(ctx) 573 if err != nil { 574 return nil, nil, sqlcommon.HandleSQLError(err) 575 } 576 defer rows.Close() 577 578 var stores []*openfgav1.Store 579 var id string 580 for rows.Next() { 581 var name string 582 var createdAt, updatedAt time.Time 583 err := rows.Scan(&id, &name, &createdAt, &updatedAt) 584 if err != nil { 585 return nil, nil, sqlcommon.HandleSQLError(err) 586 } 587 588 stores = append(stores, &openfgav1.Store{ 589 Id: id, 590 Name: name, 591 CreatedAt: timestamppb.New(createdAt), 592 UpdatedAt: timestamppb.New(updatedAt), 593 }) 594 } 595 596 if err := rows.Err(); err != nil { 597 return nil, nil, sqlcommon.HandleSQLError(err) 598 } 599 600 if len(stores) > opts.PageSize { 601 contToken, err := json.Marshal(sqlcommon.NewContToken(id, "")) 602 if err != nil { 603 return nil, nil, err 604 } 605 return stores[:opts.PageSize], contToken, nil 606 } 607 608 return stores, nil, nil 609 } 610 611 // DeleteStore removes a store from the MySQL storage. 612 func (m *MySQL) DeleteStore(ctx context.Context, id string) error { 613 ctx, span := tracer.Start(ctx, "mysql.DeleteStore") 614 defer span.End() 615 616 _, err := m.stbl. 617 Update("store"). 618 Set("deleted_at", sq.Expr("NOW()")). 619 Where(sq.Eq{"id": id}). 620 ExecContext(ctx) 621 if err != nil { 622 return sqlcommon.HandleSQLError(err) 623 } 624 625 return nil 626 } 627 628 // WriteAssertions see [storage.AssertionsBackend].WriteAssertions. 629 func (m *MySQL) WriteAssertions(ctx context.Context, store, modelID string, assertions []*openfgav1.Assertion) error { 630 ctx, span := tracer.Start(ctx, "mysql.WriteAssertions") 631 defer span.End() 632 633 marshalledAssertions, err := proto.Marshal(&openfgav1.Assertions{Assertions: assertions}) 634 if err != nil { 635 return err 636 } 637 638 _, err = m.stbl. 639 Insert("assertion"). 640 Columns("store", "authorization_model_id", "assertions"). 641 Values(store, modelID, marshalledAssertions). 642 Suffix("ON DUPLICATE KEY UPDATE assertions = ?", marshalledAssertions). 643 ExecContext(ctx) 644 if err != nil { 645 return sqlcommon.HandleSQLError(err) 646 } 647 648 return nil 649 } 650 651 // ReadAssertions see [storage.AssertionsBackend].ReadAssertions. 652 func (m *MySQL) ReadAssertions(ctx context.Context, store, modelID string) ([]*openfgav1.Assertion, error) { 653 ctx, span := tracer.Start(ctx, "mysql.ReadAssertions") 654 defer span.End() 655 656 var marshalledAssertions []byte 657 err := m.stbl. 658 Select("assertions"). 659 From("assertion"). 660 Where(sq.Eq{ 661 "store": store, 662 "authorization_model_id": modelID, 663 }). 664 QueryRowContext(ctx). 665 Scan(&marshalledAssertions) 666 if err != nil { 667 if errors.Is(err, sql.ErrNoRows) { 668 return []*openfgav1.Assertion{}, nil 669 } 670 return nil, sqlcommon.HandleSQLError(err) 671 } 672 673 var assertions openfgav1.Assertions 674 err = proto.Unmarshal(marshalledAssertions, &assertions) 675 if err != nil { 676 return nil, err 677 } 678 679 return assertions.GetAssertions(), nil 680 } 681 682 // ReadChanges see [storage.ChangelogBackend].ReadChanges. 683 func (m *MySQL) ReadChanges( 684 ctx context.Context, 685 store, objectTypeFilter string, 686 opts storage.PaginationOptions, 687 horizonOffset time.Duration, 688 ) ([]*openfgav1.TupleChange, []byte, error) { 689 ctx, span := tracer.Start(ctx, "mysql.ReadChanges") 690 defer span.End() 691 692 sb := m.stbl. 693 Select( 694 "ulid", "object_type", "object_id", "relation", "_user", "operation", 695 "condition_name", "condition_context", "inserted_at", 696 ). 697 From("changelog"). 698 Where(sq.Eq{"store": store}). 699 Where(fmt.Sprintf("inserted_at <= NOW() - INTERVAL %d MICROSECOND", horizonOffset.Microseconds())). 700 OrderBy("ulid asc") 701 702 if objectTypeFilter != "" { 703 sb = sb.Where(sq.Eq{"object_type": objectTypeFilter}) 704 } 705 if opts.From != "" { 706 token, err := sqlcommon.UnmarshallContToken(opts.From) 707 if err != nil { 708 return nil, nil, err 709 } 710 if token.ObjectType != objectTypeFilter { 711 return nil, nil, storage.ErrMismatchObjectType 712 } 713 714 sb = sb.Where(sq.Gt{"ulid": token.Ulid}) // > as we always return a continuation token. 715 } 716 if opts.PageSize > 0 { 717 sb = sb.Limit(uint64(opts.PageSize)) // + 1 is NOT used here as we always return a continuation token. 718 } 719 720 rows, err := sb.QueryContext(ctx) 721 if err != nil { 722 return nil, nil, sqlcommon.HandleSQLError(err) 723 } 724 defer rows.Close() 725 726 var changes []*openfgav1.TupleChange 727 var ulid string 728 for rows.Next() { 729 var objectType, objectID, relation, user string 730 var operation int 731 var insertedAt time.Time 732 var conditionName sql.NullString 733 var conditionContext []byte 734 735 err = rows.Scan( 736 &ulid, 737 &objectType, 738 &objectID, 739 &relation, 740 &user, 741 &operation, 742 &conditionName, 743 &conditionContext, 744 &insertedAt, 745 ) 746 if err != nil { 747 return nil, nil, sqlcommon.HandleSQLError(err) 748 } 749 750 var conditionContextStruct structpb.Struct 751 if conditionName.String != "" { 752 if conditionContext != nil { 753 if err := proto.Unmarshal(conditionContext, &conditionContextStruct); err != nil { 754 return nil, nil, err 755 } 756 } 757 } 758 759 tk := tupleUtils.NewTupleKeyWithCondition( 760 tupleUtils.BuildObject(objectType, objectID), 761 relation, 762 user, 763 conditionName.String, 764 &conditionContextStruct, 765 ) 766 767 changes = append(changes, &openfgav1.TupleChange{ 768 TupleKey: tk, 769 Operation: openfgav1.TupleOperation(operation), 770 Timestamp: timestamppb.New(insertedAt.UTC()), 771 }) 772 } 773 774 if len(changes) == 0 { 775 return nil, nil, storage.ErrNotFound 776 } 777 778 contToken, err := json.Marshal(sqlcommon.NewContToken(ulid, objectTypeFilter)) 779 if err != nil { 780 return nil, nil, err 781 } 782 783 return changes, contToken, nil 784 } 785 786 // IsReady see [sqlcommon.IsReady]. 787 func (m *MySQL) IsReady(ctx context.Context) (storage.ReadinessStatus, error) { 788 return sqlcommon.IsReady(ctx, m.db) 789 }