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

     1  package mysql
     2  
     3  import (
     4  	"context"
     5  	"database/sql"
     6  	"errors"
     7  	"fmt"
     8  
     9  	sq "github.com/Masterminds/squirrel"
    10  
    11  	"github.com/authzed/spicedb/internal/datastore/common"
    12  	"github.com/authzed/spicedb/internal/datastore/revisions"
    13  	"github.com/authzed/spicedb/pkg/datastore"
    14  	"github.com/authzed/spicedb/pkg/datastore/options"
    15  	core "github.com/authzed/spicedb/pkg/proto/core/v1"
    16  )
    17  
    18  type txCleanupFunc func() error
    19  
    20  type txFactory func(context.Context) (*sql.Tx, txCleanupFunc, error)
    21  
    22  type mysqlReader struct {
    23  	*QueryBuilder
    24  
    25  	txSource txFactory
    26  	executor common.QueryExecutor
    27  	filterer queryFilterer
    28  }
    29  
    30  type queryFilterer func(original sq.SelectBuilder) sq.SelectBuilder
    31  
    32  const (
    33  	errUnableToReadConfig     = "unable to read namespace config: %w"
    34  	errUnableToListNamespaces = "unable to list namespaces: %w"
    35  	errUnableToQueryTuples    = "unable to query tuples: %w"
    36  )
    37  
    38  var schema = common.NewSchemaInformation(
    39  	colNamespace,
    40  	colObjectID,
    41  	colRelation,
    42  	colUsersetNamespace,
    43  	colUsersetObjectID,
    44  	colUsersetRelation,
    45  	colCaveatName,
    46  	common.ExpandedLogicComparison,
    47  )
    48  
    49  func (mr *mysqlReader) QueryRelationships(
    50  	ctx context.Context,
    51  	filter datastore.RelationshipsFilter,
    52  	opts ...options.QueryOptionsOption,
    53  ) (iter datastore.RelationshipIterator, err error) {
    54  	qBuilder, err := common.NewSchemaQueryFilterer(schema, mr.filterer(mr.QueryTuplesQuery)).FilterWithRelationshipsFilter(filter)
    55  	if err != nil {
    56  		return nil, err
    57  	}
    58  
    59  	return mr.executor.ExecuteQuery(ctx, qBuilder, opts...)
    60  }
    61  
    62  func (mr *mysqlReader) ReverseQueryRelationships(
    63  	ctx context.Context,
    64  	subjectsFilter datastore.SubjectsFilter,
    65  	opts ...options.ReverseQueryOptionsOption,
    66  ) (iter datastore.RelationshipIterator, err error) {
    67  	qBuilder, err := common.NewSchemaQueryFilterer(schema, mr.filterer(mr.QueryTuplesQuery)).
    68  		FilterWithSubjectsSelectors(subjectsFilter.AsSelector())
    69  	if err != nil {
    70  		return nil, err
    71  	}
    72  
    73  	queryOpts := options.NewReverseQueryOptionsWithOptions(opts...)
    74  
    75  	if queryOpts.ResRelation != nil {
    76  		qBuilder = qBuilder.
    77  			FilterToResourceType(queryOpts.ResRelation.Namespace).
    78  			FilterToRelation(queryOpts.ResRelation.Relation)
    79  	}
    80  
    81  	return mr.executor.ExecuteQuery(
    82  		ctx,
    83  		qBuilder,
    84  		options.WithLimit(queryOpts.LimitForReverse),
    85  		options.WithAfter(queryOpts.AfterForReverse),
    86  		options.WithSort(queryOpts.SortForReverse),
    87  	)
    88  }
    89  
    90  func (mr *mysqlReader) ReadNamespaceByName(ctx context.Context, nsName string) (*core.NamespaceDefinition, datastore.Revision, error) {
    91  	tx, txCleanup, err := mr.txSource(ctx)
    92  	if err != nil {
    93  		return nil, datastore.NoRevision, fmt.Errorf(errUnableToReadConfig, err)
    94  	}
    95  	defer common.LogOnError(ctx, txCleanup)
    96  
    97  	loaded, version, err := loadNamespace(ctx, nsName, tx, mr.filterer(mr.ReadNamespaceQuery))
    98  	switch {
    99  	case errors.As(err, &datastore.ErrNamespaceNotFound{}):
   100  		return nil, datastore.NoRevision, err
   101  	case err == nil:
   102  		return loaded, version, nil
   103  	default:
   104  		return nil, datastore.NoRevision, fmt.Errorf(errUnableToReadConfig, err)
   105  	}
   106  }
   107  
   108  func loadNamespace(ctx context.Context, namespace string, tx *sql.Tx, baseQuery sq.SelectBuilder) (*core.NamespaceDefinition, datastore.Revision, error) {
   109  	ctx, span := tracer.Start(ctx, "loadNamespace")
   110  	defer span.End()
   111  
   112  	query, args, err := baseQuery.Where(sq.Eq{colNamespace: namespace}).ToSql()
   113  	if err != nil {
   114  		return nil, datastore.NoRevision, err
   115  	}
   116  
   117  	var config []byte
   118  	var txID uint64
   119  	err = tx.QueryRowContext(ctx, query, args...).Scan(&config, &txID)
   120  	if err != nil {
   121  		if errors.Is(err, sql.ErrNoRows) {
   122  			err = datastore.NewNamespaceNotFoundErr(namespace)
   123  		}
   124  		return nil, datastore.NoRevision, err
   125  	}
   126  
   127  	loaded := &core.NamespaceDefinition{}
   128  	if err := loaded.UnmarshalVT(config); err != nil {
   129  		return nil, datastore.NoRevision, err
   130  	}
   131  
   132  	return loaded, revisions.NewForTransactionID(txID), nil
   133  }
   134  
   135  func (mr *mysqlReader) ListAllNamespaces(ctx context.Context) ([]datastore.RevisionedNamespace, error) {
   136  	tx, txCleanup, err := mr.txSource(ctx)
   137  	if err != nil {
   138  		return nil, err
   139  	}
   140  	defer common.LogOnError(ctx, txCleanup)
   141  
   142  	query := mr.filterer(mr.ReadNamespaceQuery)
   143  
   144  	nsDefs, err := loadAllNamespaces(ctx, tx, query)
   145  	if err != nil {
   146  		return nil, fmt.Errorf(errUnableToListNamespaces, err)
   147  	}
   148  
   149  	return nsDefs, err
   150  }
   151  
   152  func (mr *mysqlReader) LookupNamespacesWithNames(ctx context.Context, nsNames []string) ([]datastore.RevisionedNamespace, error) {
   153  	if len(nsNames) == 0 {
   154  		return nil, nil
   155  	}
   156  
   157  	tx, txCleanup, err := mr.txSource(ctx)
   158  	if err != nil {
   159  		return nil, err
   160  	}
   161  	defer common.LogOnError(ctx, txCleanup)
   162  
   163  	clause := sq.Or{}
   164  	for _, nsName := range nsNames {
   165  		clause = append(clause, sq.Eq{colNamespace: nsName})
   166  	}
   167  
   168  	query := mr.filterer(mr.ReadNamespaceQuery.Where(clause))
   169  
   170  	nsDefs, err := loadAllNamespaces(ctx, tx, query)
   171  	if err != nil {
   172  		return nil, fmt.Errorf(errUnableToListNamespaces, err)
   173  	}
   174  
   175  	return nsDefs, err
   176  }
   177  
   178  func loadAllNamespaces(ctx context.Context, tx *sql.Tx, queryBuilder sq.SelectBuilder) ([]datastore.RevisionedNamespace, error) {
   179  	query, args, err := queryBuilder.ToSql()
   180  	if err != nil {
   181  		return nil, err
   182  	}
   183  
   184  	var nsDefs []datastore.RevisionedNamespace
   185  
   186  	rows, err := tx.QueryContext(ctx, query, args...)
   187  	if err != nil {
   188  		return nil, err
   189  	}
   190  	defer common.LogOnError(ctx, rows.Close)
   191  
   192  	for rows.Next() {
   193  		var config []byte
   194  		var txID uint64
   195  		if err := rows.Scan(&config, &txID); err != nil {
   196  			return nil, err
   197  		}
   198  
   199  		loaded := &core.NamespaceDefinition{}
   200  		if err := loaded.UnmarshalVT(config); err != nil {
   201  			return nil, fmt.Errorf(errUnableToReadConfig, err)
   202  		}
   203  
   204  		nsDefs = append(nsDefs, datastore.RevisionedNamespace{
   205  			Definition:          loaded,
   206  			LastWrittenRevision: revisions.NewForTransactionID(txID),
   207  		})
   208  	}
   209  	if rows.Err() != nil {
   210  		return nil, rows.Err()
   211  	}
   212  
   213  	return nsDefs, nil
   214  }
   215  
   216  var _ datastore.Reader = &mysqlReader{}