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

     1  package crdb
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"strings"
     8  
     9  	sq "github.com/Masterminds/squirrel"
    10  	v1 "github.com/authzed/authzed-go/proto/authzed/api/v1"
    11  	"github.com/jackc/pgx/v5"
    12  	"github.com/jzelinskie/stringz"
    13  	"google.golang.org/protobuf/proto"
    14  
    15  	pgxcommon "github.com/authzed/spicedb/internal/datastore/postgres/common"
    16  	log "github.com/authzed/spicedb/internal/logging"
    17  	"github.com/authzed/spicedb/pkg/datastore"
    18  	"github.com/authzed/spicedb/pkg/datastore/options"
    19  	core "github.com/authzed/spicedb/pkg/proto/core/v1"
    20  )
    21  
    22  const (
    23  	errUnableToWriteConfig         = "unable to write namespace config: %w"
    24  	errUnableToDeleteConfig        = "unable to delete namespace config: %w"
    25  	errUnableToWriteRelationships  = "unable to write relationships: %w"
    26  	errUnableToDeleteRelationships = "unable to delete relationships: %w"
    27  )
    28  
    29  var (
    30  	upsertNamespaceSuffix = fmt.Sprintf(
    31  		"ON CONFLICT (%s) DO UPDATE SET %s = excluded.%s",
    32  		colNamespace,
    33  		colConfig,
    34  		colConfig,
    35  	)
    36  	queryWriteNamespace = psql.Insert(tableNamespace).Columns(
    37  		colNamespace,
    38  		colConfig,
    39  	).Suffix(upsertNamespaceSuffix)
    40  
    41  	queryDeleteNamespace = psql.Delete(tableNamespace)
    42  )
    43  
    44  type crdbReadWriteTXN struct {
    45  	*crdbReader
    46  	tx             pgx.Tx
    47  	relCountChange int64
    48  }
    49  
    50  var (
    51  	upsertTupleSuffix = fmt.Sprintf(
    52  		"ON CONFLICT (%s,%s,%s,%s,%s,%s) DO UPDATE SET %s = now(), %s = excluded.%s, %s = excluded.%s WHERE (relation_tuple.%s <> excluded.%s OR relation_tuple.%s <> excluded.%s)",
    53  		colNamespace,
    54  		colObjectID,
    55  		colRelation,
    56  		colUsersetNamespace,
    57  		colUsersetObjectID,
    58  		colUsersetRelation,
    59  		colTimestamp,
    60  		colCaveatContextName,
    61  		colCaveatContextName,
    62  		colCaveatContext,
    63  		colCaveatContext,
    64  		colCaveatContextName,
    65  		colCaveatContextName,
    66  		colCaveatContext,
    67  		colCaveatContext,
    68  	)
    69  
    70  	queryWriteTuple = psql.Insert(tableTuple).Columns(
    71  		colNamespace,
    72  		colObjectID,
    73  		colRelation,
    74  		colUsersetNamespace,
    75  		colUsersetObjectID,
    76  		colUsersetRelation,
    77  		colCaveatContextName,
    78  		colCaveatContext,
    79  	)
    80  
    81  	queryTouchTuple = queryWriteTuple.Suffix(upsertTupleSuffix)
    82  
    83  	queryDeleteTuples = psql.Delete(tableTuple)
    84  
    85  	queryTouchTransaction = fmt.Sprintf(
    86  		"INSERT INTO %s (%s) VALUES ($1::text) ON CONFLICT (%s) DO UPDATE SET %s = now()",
    87  		tableTransactions,
    88  		colTransactionKey,
    89  		colTransactionKey,
    90  		colTimestamp,
    91  	)
    92  )
    93  
    94  func (rwt *crdbReadWriteTXN) WriteRelationships(ctx context.Context, mutations []*core.RelationTupleUpdate) error {
    95  	bulkWrite := queryWriteTuple
    96  	var bulkWriteCount int64
    97  
    98  	bulkTouch := queryTouchTuple
    99  	var bulkTouchCount int64
   100  
   101  	bulkDelete := queryDeleteTuples
   102  	bulkDeleteOr := sq.Or{}
   103  	var bulkDeleteCount int64
   104  
   105  	// Process the actual updates
   106  	for _, mutation := range mutations {
   107  		rel := mutation.Tuple
   108  
   109  		var caveatContext map[string]any
   110  		var caveatName string
   111  		if rel.Caveat != nil {
   112  			caveatName = rel.Caveat.CaveatName
   113  			caveatContext = rel.Caveat.Context.AsMap()
   114  		}
   115  
   116  		rwt.addOverlapKey(rel.ResourceAndRelation.Namespace)
   117  		rwt.addOverlapKey(rel.Subject.Namespace)
   118  
   119  		switch mutation.Operation {
   120  		case core.RelationTupleUpdate_TOUCH:
   121  			rwt.relCountChange++
   122  			bulkTouch = bulkTouch.Values(
   123  				rel.ResourceAndRelation.Namespace,
   124  				rel.ResourceAndRelation.ObjectId,
   125  				rel.ResourceAndRelation.Relation,
   126  				rel.Subject.Namespace,
   127  				rel.Subject.ObjectId,
   128  				rel.Subject.Relation,
   129  				caveatName,
   130  				caveatContext,
   131  			)
   132  			bulkTouchCount++
   133  		case core.RelationTupleUpdate_CREATE:
   134  			rwt.relCountChange++
   135  			bulkWrite = bulkWrite.Values(
   136  				rel.ResourceAndRelation.Namespace,
   137  				rel.ResourceAndRelation.ObjectId,
   138  				rel.ResourceAndRelation.Relation,
   139  				rel.Subject.Namespace,
   140  				rel.Subject.ObjectId,
   141  				rel.Subject.Relation,
   142  				caveatName,
   143  				caveatContext,
   144  			)
   145  			bulkWriteCount++
   146  		case core.RelationTupleUpdate_DELETE:
   147  			rwt.relCountChange--
   148  			bulkDeleteOr = append(bulkDeleteOr, exactRelationshipClause(rel))
   149  			bulkDeleteCount++
   150  
   151  		default:
   152  			log.Ctx(ctx).Error().Stringer("operation", mutation.Operation).Msg("unknown operation type")
   153  			return fmt.Errorf("unknown mutation operation: %s", mutation.Operation)
   154  		}
   155  	}
   156  
   157  	if bulkDeleteCount > 0 {
   158  		bulkDelete = bulkDelete.Where(bulkDeleteOr)
   159  		sql, args, err := bulkDelete.ToSql()
   160  		if err != nil {
   161  			return fmt.Errorf(errUnableToWriteRelationships, err)
   162  		}
   163  
   164  		if _, err := rwt.tx.Exec(ctx, sql, args...); err != nil {
   165  			return fmt.Errorf(errUnableToWriteRelationships, err)
   166  		}
   167  	}
   168  
   169  	bulkUpdateQueries := make([]sq.InsertBuilder, 0, 2)
   170  	if bulkWriteCount > 0 {
   171  		bulkUpdateQueries = append(bulkUpdateQueries, bulkWrite)
   172  	}
   173  	if bulkTouchCount > 0 {
   174  		bulkUpdateQueries = append(bulkUpdateQueries, bulkTouch)
   175  	}
   176  
   177  	for _, updateQuery := range bulkUpdateQueries {
   178  		sql, args, err := updateQuery.ToSql()
   179  		if err != nil {
   180  			return fmt.Errorf(errUnableToWriteRelationships, err)
   181  		}
   182  
   183  		if _, err := rwt.tx.Exec(ctx, sql, args...); err != nil {
   184  			return fmt.Errorf(errUnableToWriteRelationships, err)
   185  		}
   186  	}
   187  
   188  	return nil
   189  }
   190  
   191  func exactRelationshipClause(r *core.RelationTuple) sq.Eq {
   192  	return sq.Eq{
   193  		colNamespace:        r.ResourceAndRelation.Namespace,
   194  		colObjectID:         r.ResourceAndRelation.ObjectId,
   195  		colRelation:         r.ResourceAndRelation.Relation,
   196  		colUsersetNamespace: r.Subject.Namespace,
   197  		colUsersetObjectID:  r.Subject.ObjectId,
   198  		colUsersetRelation:  r.Subject.Relation,
   199  	}
   200  }
   201  
   202  func (rwt *crdbReadWriteTXN) DeleteRelationships(ctx context.Context, filter *v1.RelationshipFilter, opts ...options.DeleteOptionsOption) (bool, error) {
   203  	// Add clauses for the ResourceFilter
   204  	query := queryDeleteTuples
   205  
   206  	if filter.ResourceType != "" {
   207  		query = query.Where(sq.Eq{colNamespace: filter.ResourceType})
   208  	}
   209  	if filter.OptionalResourceId != "" {
   210  		query = query.Where(sq.Eq{colObjectID: filter.OptionalResourceId})
   211  	}
   212  	if filter.OptionalRelation != "" {
   213  		query = query.Where(sq.Eq{colRelation: filter.OptionalRelation})
   214  	}
   215  	if filter.OptionalResourceIdPrefix != "" {
   216  		if strings.Contains(filter.OptionalResourceIdPrefix, "%") {
   217  			return false, fmt.Errorf("unable to delete relationships with a prefix containing the %% character")
   218  		}
   219  
   220  		query = query.Where(sq.Like{colObjectID: filter.OptionalResourceIdPrefix + "%"})
   221  	}
   222  
   223  	rwt.addOverlapKey(filter.ResourceType)
   224  
   225  	// Add clauses for the SubjectFilter
   226  	if subjectFilter := filter.OptionalSubjectFilter; subjectFilter != nil {
   227  		query = query.Where(sq.Eq{colUsersetNamespace: subjectFilter.SubjectType})
   228  		if subjectFilter.OptionalSubjectId != "" {
   229  			query = query.Where(sq.Eq{colUsersetObjectID: subjectFilter.OptionalSubjectId})
   230  		}
   231  		if relationFilter := subjectFilter.OptionalRelation; relationFilter != nil {
   232  			query = query.Where(sq.Eq{colUsersetRelation: stringz.DefaultEmpty(relationFilter.Relation, datastore.Ellipsis)})
   233  		}
   234  		rwt.addOverlapKey(subjectFilter.SubjectType)
   235  	}
   236  
   237  	// Add the limit, if any.
   238  	delOpts := options.NewDeleteOptionsWithOptionsAndDefaults(opts...)
   239  	var delLimit uint64
   240  	if delOpts.DeleteLimit != nil && *delOpts.DeleteLimit > 0 {
   241  		delLimit = *delOpts.DeleteLimit
   242  	}
   243  
   244  	if delLimit > 0 {
   245  		query = query.Limit(delLimit)
   246  	}
   247  
   248  	sql, args, err := query.ToSql()
   249  	if err != nil {
   250  		return false, fmt.Errorf(errUnableToDeleteRelationships, err)
   251  	}
   252  
   253  	modified, err := rwt.tx.Exec(ctx, sql, args...)
   254  	if err != nil {
   255  		return false, fmt.Errorf(errUnableToDeleteRelationships, err)
   256  	}
   257  
   258  	rwt.relCountChange -= modified.RowsAffected()
   259  	if delLimit > 0 && uint64(modified.RowsAffected()) == delLimit {
   260  		return true, nil
   261  	}
   262  
   263  	return false, nil
   264  }
   265  
   266  func (rwt *crdbReadWriteTXN) WriteNamespaces(ctx context.Context, newConfigs ...*core.NamespaceDefinition) error {
   267  	query := queryWriteNamespace
   268  
   269  	for _, newConfig := range newConfigs {
   270  		rwt.addOverlapKey(newConfig.Name)
   271  
   272  		serialized, err := proto.Marshal(newConfig)
   273  		if err != nil {
   274  			return fmt.Errorf(errUnableToWriteConfig, err)
   275  		}
   276  		query = query.Values(newConfig.Name, serialized)
   277  	}
   278  
   279  	writeSQL, writeArgs, err := query.ToSql()
   280  	if err != nil {
   281  		return fmt.Errorf(errUnableToWriteConfig, err)
   282  	}
   283  
   284  	if _, err := rwt.tx.Exec(ctx, writeSQL, writeArgs...); err != nil {
   285  		return fmt.Errorf(errUnableToWriteConfig, err)
   286  	}
   287  
   288  	return nil
   289  }
   290  
   291  func (rwt *crdbReadWriteTXN) DeleteNamespaces(ctx context.Context, nsNames ...string) error {
   292  	querier := pgxcommon.QuerierFuncsFor(rwt.tx)
   293  	// For each namespace, check they exist and collect predicates for the
   294  	// "WHERE" clause to delete the namespaces and associated tuples.
   295  	nsClauses := make([]sq.Sqlizer, 0, len(nsNames))
   296  	tplClauses := make([]sq.Sqlizer, 0, len(nsNames))
   297  	for _, nsName := range nsNames {
   298  		_, timestamp, err := rwt.loadNamespace(ctx, querier, nsName)
   299  		if err != nil {
   300  			if errors.As(err, &datastore.ErrNamespaceNotFound{}) {
   301  				return err
   302  			}
   303  			return fmt.Errorf(errUnableToDeleteConfig, err)
   304  		}
   305  
   306  		for _, nsName := range nsNames {
   307  			nsClauses = append(nsClauses, sq.Eq{colNamespace: nsName, colTimestamp: timestamp})
   308  			tplClauses = append(tplClauses, sq.Eq{colNamespace: nsName})
   309  		}
   310  	}
   311  
   312  	delSQL, delArgs, err := queryDeleteNamespace.Where(sq.Or(nsClauses)).ToSql()
   313  	if err != nil {
   314  		return fmt.Errorf(errUnableToDeleteConfig, err)
   315  	}
   316  
   317  	_, err = rwt.tx.Exec(ctx, delSQL, delArgs...)
   318  	if err != nil {
   319  		return fmt.Errorf(errUnableToDeleteConfig, err)
   320  	}
   321  
   322  	deleteTupleSQL, deleteTupleArgs, err := queryDeleteTuples.Where(sq.Or(tplClauses)).ToSql()
   323  	if err != nil {
   324  		return fmt.Errorf(errUnableToDeleteConfig, err)
   325  	}
   326  
   327  	modified, err := rwt.tx.Exec(ctx, deleteTupleSQL, deleteTupleArgs...)
   328  	if err != nil {
   329  		return fmt.Errorf(errUnableToDeleteConfig, err)
   330  	}
   331  
   332  	numRowsDeleted := modified.RowsAffected()
   333  	rwt.relCountChange -= numRowsDeleted
   334  
   335  	return nil
   336  }
   337  
   338  var copyCols = []string{
   339  	colNamespace,
   340  	colObjectID,
   341  	colRelation,
   342  	colUsersetNamespace,
   343  	colUsersetObjectID,
   344  	colUsersetRelation,
   345  	colCaveatContextName,
   346  	colCaveatContext,
   347  }
   348  
   349  func (rwt *crdbReadWriteTXN) BulkLoad(ctx context.Context, iter datastore.BulkWriteRelationshipSource) (uint64, error) {
   350  	return pgxcommon.BulkLoad(ctx, rwt.tx, tableTuple, copyCols, iter)
   351  }
   352  
   353  var _ datastore.ReadWriteTransaction = &crdbReadWriteTXN{}