github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/internal/datastore/postgres/reader.go (about) 1 package postgres 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 8 sq "github.com/Masterminds/squirrel" 9 "github.com/jackc/pgx/v5" 10 11 "github.com/authzed/spicedb/internal/datastore/common" 12 pgxcommon "github.com/authzed/spicedb/internal/datastore/postgres/common" 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 pgReader struct { 19 query pgxcommon.DBFuncQuerier 20 executor common.QueryExecutor 21 filterer queryFilterer 22 } 23 24 type queryFilterer func(original sq.SelectBuilder) sq.SelectBuilder 25 26 var ( 27 queryTuples = psql.Select( 28 colNamespace, 29 colObjectID, 30 colRelation, 31 colUsersetNamespace, 32 colUsersetObjectID, 33 colUsersetRelation, 34 colCaveatContextName, 35 colCaveatContext, 36 ).From(tableTuple) 37 38 schema = common.NewSchemaInformation( 39 colNamespace, 40 colObjectID, 41 colRelation, 42 colUsersetNamespace, 43 colUsersetObjectID, 44 colUsersetRelation, 45 colCaveatContextName, 46 common.TupleComparison, 47 ) 48 49 readNamespace = psql. 50 Select(colConfig, colCreatedXid). 51 From(tableNamespace) 52 ) 53 54 const ( 55 errUnableToReadConfig = "unable to read namespace config: %w" 56 errUnableToListNamespaces = "unable to list namespaces: %w" 57 ) 58 59 func (r *pgReader) QueryRelationships( 60 ctx context.Context, 61 filter datastore.RelationshipsFilter, 62 opts ...options.QueryOptionsOption, 63 ) (iter datastore.RelationshipIterator, err error) { 64 qBuilder, err := common.NewSchemaQueryFilterer(schema, r.filterer(queryTuples)).FilterWithRelationshipsFilter(filter) 65 if err != nil { 66 return nil, err 67 } 68 69 return r.executor.ExecuteQuery(ctx, qBuilder, opts...) 70 } 71 72 func (r *pgReader) ReverseQueryRelationships( 73 ctx context.Context, 74 subjectsFilter datastore.SubjectsFilter, 75 opts ...options.ReverseQueryOptionsOption, 76 ) (iter datastore.RelationshipIterator, err error) { 77 qBuilder, err := common.NewSchemaQueryFilterer(schema, r.filterer(queryTuples)). 78 FilterWithSubjectsSelectors(subjectsFilter.AsSelector()) 79 if err != nil { 80 return nil, err 81 } 82 83 queryOpts := options.NewReverseQueryOptionsWithOptions(opts...) 84 85 if queryOpts.ResRelation != nil { 86 qBuilder = qBuilder. 87 FilterToResourceType(queryOpts.ResRelation.Namespace). 88 FilterToRelation(queryOpts.ResRelation.Relation) 89 } 90 91 return r.executor.ExecuteQuery(ctx, 92 qBuilder, 93 options.WithLimit(queryOpts.LimitForReverse), 94 options.WithAfter(queryOpts.AfterForReverse), 95 options.WithSort(queryOpts.SortForReverse), 96 ) 97 } 98 99 func (r *pgReader) ReadNamespaceByName(ctx context.Context, nsName string) (*core.NamespaceDefinition, datastore.Revision, error) { 100 loaded, version, err := r.loadNamespace(ctx, nsName, r.query, r.filterer) 101 switch { 102 case errors.As(err, &datastore.ErrNamespaceNotFound{}): 103 return nil, datastore.NoRevision, err 104 case err == nil: 105 return loaded, version, nil 106 default: 107 return nil, datastore.NoRevision, fmt.Errorf(errUnableToReadConfig, err) 108 } 109 } 110 111 func (r *pgReader) loadNamespace(ctx context.Context, namespace string, tx pgxcommon.DBFuncQuerier, filterer queryFilterer) (*core.NamespaceDefinition, postgresRevision, error) { 112 ctx, span := tracer.Start(ctx, "loadNamespace") 113 defer span.End() 114 115 defs, err := loadAllNamespaces(ctx, tx, func(original sq.SelectBuilder) sq.SelectBuilder { 116 return filterer(original).Where(sq.Eq{colNamespace: namespace}) 117 }) 118 if err != nil { 119 return nil, postgresRevision{}, err 120 } 121 122 if len(defs) < 1 { 123 return nil, postgresRevision{}, datastore.NewNamespaceNotFoundErr(namespace) 124 } 125 126 return defs[0].Definition, defs[0].LastWrittenRevision.(postgresRevision), nil 127 } 128 129 func (r *pgReader) ListAllNamespaces(ctx context.Context) ([]datastore.RevisionedNamespace, error) { 130 nsDefsWithRevisions, err := loadAllNamespaces(ctx, r.query, r.filterer) 131 if err != nil { 132 return nil, fmt.Errorf(errUnableToListNamespaces, err) 133 } 134 135 return nsDefsWithRevisions, err 136 } 137 138 func (r *pgReader) LookupNamespacesWithNames(ctx context.Context, nsNames []string) ([]datastore.RevisionedNamespace, error) { 139 if len(nsNames) == 0 { 140 return nil, nil 141 } 142 143 clause := sq.Or{} 144 for _, nsName := range nsNames { 145 clause = append(clause, sq.Eq{colNamespace: nsName}) 146 } 147 148 nsDefsWithRevisions, err := loadAllNamespaces(ctx, r.query, func(original sq.SelectBuilder) sq.SelectBuilder { 149 return r.filterer(original).Where(clause) 150 }) 151 if err != nil { 152 return nil, fmt.Errorf(errUnableToListNamespaces, err) 153 } 154 155 return nsDefsWithRevisions, err 156 } 157 158 func loadAllNamespaces( 159 ctx context.Context, 160 tx pgxcommon.DBFuncQuerier, 161 filterer queryFilterer, 162 ) ([]datastore.RevisionedNamespace, error) { 163 sql, args, err := filterer(readNamespace).ToSql() 164 if err != nil { 165 return nil, err 166 } 167 168 var nsDefs []datastore.RevisionedNamespace 169 err = tx.QueryFunc(ctx, func(ctx context.Context, rows pgx.Rows) error { 170 for rows.Next() { 171 var config []byte 172 var version xid8 173 174 if err := rows.Scan(&config, &version); err != nil { 175 return err 176 } 177 178 loaded := &core.NamespaceDefinition{} 179 if err := loaded.UnmarshalVT(config); err != nil { 180 return fmt.Errorf(errUnableToReadConfig, err) 181 } 182 183 revision := revisionForVersion(version) 184 185 nsDefs = append(nsDefs, datastore.RevisionedNamespace{Definition: loaded, LastWrittenRevision: revision}) 186 } 187 return rows.Err() 188 }, sql, args...) 189 if err != nil { 190 return nil, err 191 } 192 193 return nsDefs, nil 194 } 195 196 // revisionForVersion synthesizes a snapshot where the specified version is always visible. 197 func revisionForVersion(version xid8) postgresRevision { 198 return postgresRevision{pgSnapshot{ 199 xmin: version.Uint64 + 1, 200 xmax: version.Uint64 + 1, 201 }} 202 } 203 204 var _ datastore.Reader = &pgReader{}