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  }