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{}