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