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

     1  package crdb
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"time"
     8  
     9  	sq "github.com/Masterminds/squirrel"
    10  	"github.com/jackc/pgx/v5"
    11  
    12  	"github.com/authzed/spicedb/internal/datastore/common"
    13  	pgxcommon "github.com/authzed/spicedb/internal/datastore/postgres/common"
    14  	"github.com/authzed/spicedb/internal/datastore/revisions"
    15  	"github.com/authzed/spicedb/pkg/datastore"
    16  	"github.com/authzed/spicedb/pkg/datastore/options"
    17  	core "github.com/authzed/spicedb/pkg/proto/core/v1"
    18  )
    19  
    20  const (
    21  	errUnableToReadConfig     = "unable to read namespace config: %w"
    22  	errUnableToListNamespaces = "unable to list namespaces: %w"
    23  )
    24  
    25  var (
    26  	queryReadNamespace = psql.Select(colConfig, colTimestamp)
    27  
    28  	queryTuples = psql.Select(
    29  		colNamespace,
    30  		colObjectID,
    31  		colRelation,
    32  		colUsersetNamespace,
    33  		colUsersetObjectID,
    34  		colUsersetRelation,
    35  		colCaveatContextName,
    36  		colCaveatContext,
    37  	)
    38  
    39  	schema = common.NewSchemaInformation(
    40  		colNamespace,
    41  		colObjectID,
    42  		colRelation,
    43  		colUsersetNamespace,
    44  		colUsersetObjectID,
    45  		colUsersetRelation,
    46  		colCaveatContextName,
    47  		common.ExpandedLogicComparison,
    48  	)
    49  )
    50  
    51  type crdbReader struct {
    52  	query         pgxcommon.DBFuncQuerier
    53  	executor      common.QueryExecutor
    54  	keyer         overlapKeyer
    55  	overlapKeySet keySet
    56  	fromBuilder   func(query sq.SelectBuilder, fromStr string) sq.SelectBuilder
    57  }
    58  
    59  func (cr *crdbReader) ReadNamespaceByName(
    60  	ctx context.Context,
    61  	nsName string,
    62  ) (*core.NamespaceDefinition, datastore.Revision, error) {
    63  	config, timestamp, err := cr.loadNamespace(ctx, cr.query, nsName)
    64  	if err != nil {
    65  		if errors.As(err, &datastore.ErrNamespaceNotFound{}) {
    66  			return nil, datastore.NoRevision, err
    67  		}
    68  		return nil, datastore.NoRevision, fmt.Errorf(errUnableToReadConfig, err)
    69  	}
    70  
    71  	return config, revisions.NewHLCForTime(timestamp), nil
    72  }
    73  
    74  func (cr *crdbReader) ListAllNamespaces(ctx context.Context) ([]datastore.RevisionedNamespace, error) {
    75  	nsDefs, err := loadAllNamespaces(ctx, cr.query, cr.fromBuilder)
    76  	if err != nil {
    77  		return nil, fmt.Errorf(errUnableToListNamespaces, err)
    78  	}
    79  	return nsDefs, nil
    80  }
    81  
    82  func (cr *crdbReader) LookupNamespacesWithNames(ctx context.Context, nsNames []string) ([]datastore.RevisionedNamespace, error) {
    83  	if len(nsNames) == 0 {
    84  		return nil, nil
    85  	}
    86  	nsDefs, err := cr.lookupNamespaces(ctx, cr.query, nsNames)
    87  	if err != nil {
    88  		return nil, fmt.Errorf(errUnableToListNamespaces, err)
    89  	}
    90  	return nsDefs, nil
    91  }
    92  
    93  func (cr *crdbReader) QueryRelationships(
    94  	ctx context.Context,
    95  	filter datastore.RelationshipsFilter,
    96  	opts ...options.QueryOptionsOption,
    97  ) (iter datastore.RelationshipIterator, err error) {
    98  	query := cr.fromBuilder(queryTuples, tableTuple)
    99  	qBuilder, err := common.NewSchemaQueryFilterer(schema, query).FilterWithRelationshipsFilter(filter)
   100  	if err != nil {
   101  		return nil, err
   102  	}
   103  
   104  	return cr.executor.ExecuteQuery(ctx, qBuilder, opts...)
   105  }
   106  
   107  func (cr *crdbReader) ReverseQueryRelationships(
   108  	ctx context.Context,
   109  	subjectsFilter datastore.SubjectsFilter,
   110  	opts ...options.ReverseQueryOptionsOption,
   111  ) (iter datastore.RelationshipIterator, err error) {
   112  	query := cr.fromBuilder(queryTuples, tableTuple)
   113  	qBuilder, err := common.NewSchemaQueryFilterer(schema, query).
   114  		FilterWithSubjectsSelectors(subjectsFilter.AsSelector())
   115  	if err != nil {
   116  		return nil, err
   117  	}
   118  
   119  	queryOpts := options.NewReverseQueryOptionsWithOptions(opts...)
   120  
   121  	if queryOpts.ResRelation != nil {
   122  		qBuilder = qBuilder.
   123  			FilterToResourceType(queryOpts.ResRelation.Namespace).
   124  			FilterToRelation(queryOpts.ResRelation.Relation)
   125  	}
   126  
   127  	return cr.executor.ExecuteQuery(
   128  		ctx,
   129  		qBuilder,
   130  		options.WithLimit(queryOpts.LimitForReverse),
   131  		options.WithAfter(queryOpts.AfterForReverse),
   132  		options.WithSort(queryOpts.SortForReverse))
   133  }
   134  
   135  func (cr crdbReader) loadNamespace(ctx context.Context, tx pgxcommon.DBFuncQuerier, nsName string) (*core.NamespaceDefinition, time.Time, error) {
   136  	query := cr.fromBuilder(queryReadNamespace, tableNamespace).Where(sq.Eq{colNamespace: nsName})
   137  
   138  	sql, args, err := query.ToSql()
   139  	if err != nil {
   140  		return nil, time.Time{}, err
   141  	}
   142  
   143  	var config []byte
   144  	var timestamp time.Time
   145  
   146  	err = tx.QueryRowFunc(ctx, func(ctx context.Context, row pgx.Row) error {
   147  		return row.Scan(&config, &timestamp)
   148  	}, sql, args...)
   149  	if err != nil {
   150  		if errors.Is(err, pgx.ErrNoRows) {
   151  			err = datastore.NewNamespaceNotFoundErr(nsName)
   152  		}
   153  		return nil, time.Time{}, err
   154  	}
   155  
   156  	loaded := &core.NamespaceDefinition{}
   157  	if err := loaded.UnmarshalVT(config); err != nil {
   158  		return nil, time.Time{}, err
   159  	}
   160  
   161  	return loaded, timestamp, nil
   162  }
   163  
   164  func (cr crdbReader) lookupNamespaces(ctx context.Context, tx pgxcommon.DBFuncQuerier, nsNames []string) ([]datastore.RevisionedNamespace, error) {
   165  	clause := sq.Or{}
   166  	for _, nsName := range nsNames {
   167  		clause = append(clause, sq.Eq{colNamespace: nsName})
   168  	}
   169  
   170  	query := cr.fromBuilder(queryReadNamespace, tableNamespace).Where(clause)
   171  
   172  	sql, args, err := query.ToSql()
   173  	if err != nil {
   174  		return nil, err
   175  	}
   176  
   177  	var nsDefs []datastore.RevisionedNamespace
   178  
   179  	err = tx.QueryFunc(ctx, func(ctx context.Context, rows pgx.Rows) error {
   180  		for rows.Next() {
   181  			var config []byte
   182  			var timestamp time.Time
   183  			if err := rows.Scan(&config, &timestamp); err != nil {
   184  				return err
   185  			}
   186  
   187  			loaded := &core.NamespaceDefinition{}
   188  			if err := loaded.UnmarshalVT(config); err != nil {
   189  				return fmt.Errorf(errUnableToReadConfig, err)
   190  			}
   191  
   192  			nsDefs = append(nsDefs, datastore.RevisionedNamespace{
   193  				Definition:          loaded,
   194  				LastWrittenRevision: revisions.NewHLCForTime(timestamp),
   195  			})
   196  		}
   197  
   198  		if rows.Err() != nil {
   199  			return fmt.Errorf(errUnableToReadConfig, rows.Err())
   200  		}
   201  		return nil
   202  	}, sql, args...)
   203  	if err != nil {
   204  		return nil, err
   205  	}
   206  
   207  	return nsDefs, nil
   208  }
   209  
   210  func loadAllNamespaces(ctx context.Context, tx pgxcommon.DBFuncQuerier, fromBuilder func(sq.SelectBuilder, string) sq.SelectBuilder) ([]datastore.RevisionedNamespace, error) {
   211  	query := fromBuilder(queryReadNamespace, tableNamespace)
   212  
   213  	sql, args, err := query.ToSql()
   214  	if err != nil {
   215  		return nil, err
   216  	}
   217  
   218  	var nsDefs []datastore.RevisionedNamespace
   219  
   220  	err = tx.QueryFunc(ctx, func(ctx context.Context, rows pgx.Rows) error {
   221  		for rows.Next() {
   222  			var config []byte
   223  			var timestamp time.Time
   224  			if err := rows.Scan(&config, &timestamp); err != nil {
   225  				return err
   226  			}
   227  
   228  			loaded := &core.NamespaceDefinition{}
   229  			if err := loaded.UnmarshalVT(config); err != nil {
   230  				return fmt.Errorf(errUnableToReadConfig, err)
   231  			}
   232  
   233  			nsDefs = append(nsDefs, datastore.RevisionedNamespace{
   234  				Definition:          loaded,
   235  				LastWrittenRevision: revisions.NewHLCForTime(timestamp),
   236  			})
   237  		}
   238  
   239  		if rows.Err() != nil {
   240  			return fmt.Errorf(errUnableToReadConfig, rows.Err())
   241  		}
   242  		return nil
   243  	}, sql, args...)
   244  	if err != nil {
   245  		return nil, err
   246  	}
   247  
   248  	return nsDefs, nil
   249  }
   250  
   251  func (cr *crdbReader) addOverlapKey(namespace string) {
   252  	cr.keyer.addKey(cr.overlapKeySet, namespace)
   253  }
   254  
   255  var _ datastore.Reader = &crdbReader{}