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

     1  package postgres
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"strings"
     8  
     9  	"github.com/authzed/spicedb/pkg/datastore/options"
    10  	"github.com/authzed/spicedb/pkg/genutil/mapz"
    11  	"github.com/authzed/spicedb/pkg/spiceerrors"
    12  	"github.com/authzed/spicedb/pkg/typesystem"
    13  
    14  	sq "github.com/Masterminds/squirrel"
    15  	v1 "github.com/authzed/authzed-go/proto/authzed/api/v1"
    16  	"github.com/jackc/pgx/v5"
    17  	"github.com/jzelinskie/stringz"
    18  	"google.golang.org/protobuf/proto"
    19  
    20  	pgxcommon "github.com/authzed/spicedb/internal/datastore/postgres/common"
    21  	"github.com/authzed/spicedb/pkg/datastore"
    22  	core "github.com/authzed/spicedb/pkg/proto/core/v1"
    23  	"github.com/authzed/spicedb/pkg/tuple"
    24  )
    25  
    26  const (
    27  	errUnableToWriteConfig         = "unable to write namespace config: %w"
    28  	errUnableToDeleteConfig        = "unable to delete namespace config: %w"
    29  	errUnableToWriteRelationships  = "unable to write relationships: %w"
    30  	errUnableToDeleteRelationships = "unable to delete relationships: %w"
    31  )
    32  
    33  var (
    34  	writeNamespace = psql.Insert(tableNamespace).Columns(
    35  		colNamespace,
    36  		colConfig,
    37  	)
    38  
    39  	deleteNamespace = psql.Update(tableNamespace).Where(sq.Eq{colDeletedXid: liveDeletedTxnID})
    40  
    41  	deleteNamespaceTuples = psql.Update(tableTuple).Where(sq.Eq{colDeletedXid: liveDeletedTxnID})
    42  
    43  	writeTuple = psql.Insert(tableTuple).Columns(
    44  		colNamespace,
    45  		colObjectID,
    46  		colRelation,
    47  		colUsersetNamespace,
    48  		colUsersetObjectID,
    49  		colUsersetRelation,
    50  		colCaveatContextName,
    51  		colCaveatContext,
    52  	)
    53  
    54  	deleteTuple     = psql.Update(tableTuple).Where(sq.Eq{colDeletedXid: liveDeletedTxnID})
    55  	selectForDelete = psql.Select(
    56  		colNamespace,
    57  		colObjectID,
    58  		colRelation,
    59  		colUsersetNamespace,
    60  		colUsersetObjectID,
    61  		colUsersetRelation,
    62  		colCreatedXid,
    63  	).From(tableTuple).Where(sq.Eq{colDeletedXid: liveDeletedTxnID})
    64  )
    65  
    66  type pgReadWriteTXN struct {
    67  	*pgReader
    68  	tx     pgx.Tx
    69  	newXID xid8
    70  }
    71  
    72  func appendForInsertion(builder sq.InsertBuilder, tpl *core.RelationTuple) sq.InsertBuilder {
    73  	var caveatName string
    74  	var caveatContext map[string]any
    75  	if tpl.Caveat != nil {
    76  		caveatName = tpl.Caveat.CaveatName
    77  		caveatContext = tpl.Caveat.Context.AsMap()
    78  	}
    79  
    80  	valuesToWrite := []interface{}{
    81  		tpl.ResourceAndRelation.Namespace,
    82  		tpl.ResourceAndRelation.ObjectId,
    83  		tpl.ResourceAndRelation.Relation,
    84  		tpl.Subject.Namespace,
    85  		tpl.Subject.ObjectId,
    86  		tpl.Subject.Relation,
    87  		caveatName,
    88  		caveatContext, // PGX driver serializes map[string]any to JSONB type columns
    89  	}
    90  
    91  	return builder.Values(valuesToWrite...)
    92  }
    93  
    94  func (rwt *pgReadWriteTXN) collectSimplifiedTouchTypes(ctx context.Context, mutations []*core.RelationTupleUpdate) (*mapz.Set[string], error) {
    95  	// Collect the list of namespaces used for resources for relationships being TOUCHed.
    96  	touchedResourceNamespaces := mapz.NewSet[string]()
    97  	for _, mut := range mutations {
    98  		if mut.Operation == core.RelationTupleUpdate_TOUCH {
    99  			touchedResourceNamespaces.Add(mut.Tuple.ResourceAndRelation.Namespace)
   100  		}
   101  	}
   102  
   103  	// Load the namespaces for any resources that are being TOUCHed and check if the relation being touched
   104  	// *can* have a caveat. If not, mark the relation as supported simplified TOUCH operations.
   105  	relationSupportSimplifiedTouch := mapz.NewSet[string]()
   106  	if touchedResourceNamespaces.IsEmpty() {
   107  		return relationSupportSimplifiedTouch, nil
   108  	}
   109  
   110  	namespaces, err := rwt.LookupNamespacesWithNames(ctx, touchedResourceNamespaces.AsSlice())
   111  	if err != nil {
   112  		return nil, fmt.Errorf(errUnableToWriteRelationships, err)
   113  	}
   114  
   115  	if len(namespaces) == 0 {
   116  		return relationSupportSimplifiedTouch, nil
   117  	}
   118  
   119  	nsDefByName := make(map[string]*core.NamespaceDefinition, len(namespaces))
   120  	for _, ns := range namespaces {
   121  		nsDefByName[ns.Definition.Name] = ns.Definition
   122  	}
   123  
   124  	for _, mut := range mutations {
   125  		tpl := mut.Tuple
   126  
   127  		if mut.Operation != core.RelationTupleUpdate_TOUCH {
   128  			continue
   129  		}
   130  
   131  		nsDef, ok := nsDefByName[tpl.ResourceAndRelation.Namespace]
   132  		if !ok {
   133  			continue
   134  		}
   135  
   136  		vts, err := typesystem.NewNamespaceTypeSystem(nsDef, typesystem.ResolverForDatastoreReader(rwt))
   137  		if err != nil {
   138  			return nil, fmt.Errorf(errUnableToWriteRelationships, err)
   139  		}
   140  
   141  		notAllowed, err := vts.RelationDoesNotAllowCaveatsForSubject(tpl.ResourceAndRelation.Relation, tpl.Subject.Namespace)
   142  		if err != nil {
   143  			// Ignore errors and just fallback to the less efficient path.
   144  			continue
   145  		}
   146  
   147  		if notAllowed {
   148  			relationSupportSimplifiedTouch.Add(nsDef.Name + "#" + tpl.ResourceAndRelation.Relation + "@" + tpl.Subject.Namespace)
   149  			continue
   150  		}
   151  	}
   152  
   153  	return relationSupportSimplifiedTouch, nil
   154  }
   155  
   156  func (rwt *pgReadWriteTXN) WriteRelationships(ctx context.Context, mutations []*core.RelationTupleUpdate) error {
   157  	touchMutationsByNonCaveat := make(map[string]*core.RelationTupleUpdate, len(mutations))
   158  	hasCreateInserts := false
   159  
   160  	createInserts := writeTuple
   161  	touchInserts := writeTuple
   162  
   163  	deleteClauses := sq.Or{}
   164  
   165  	// Determine the set of relation+subject types for whom a "simplified" TOUCH operation can be used. A
   166  	// simplified TOUCH operation is one in which the relationship does not support caveats for the subject
   167  	// type. In such cases, the "DELETE" operation is unnecessary because the relationship does not support
   168  	// caveats for the subject type, and thus the relationship can be TOUCHed without needing to check for
   169  	// the existence of a relationship with a different caveat name and/or context which might need to be
   170  	// replaced.
   171  	relationSupportSimplifiedTouch, err := rwt.collectSimplifiedTouchTypes(ctx, mutations)
   172  	if err != nil {
   173  		return err
   174  	}
   175  
   176  	// Parse the updates, building inserts for CREATE/TOUCH and deletes for DELETE.
   177  	for _, mut := range mutations {
   178  		tpl := mut.Tuple
   179  
   180  		switch mut.Operation {
   181  		case core.RelationTupleUpdate_CREATE:
   182  			createInserts = appendForInsertion(createInserts, tpl)
   183  			hasCreateInserts = true
   184  
   185  		case core.RelationTupleUpdate_TOUCH:
   186  			touchInserts = appendForInsertion(touchInserts, tpl)
   187  			touchMutationsByNonCaveat[tuple.StringWithoutCaveat(tpl)] = mut
   188  
   189  		case core.RelationTupleUpdate_DELETE:
   190  			deleteClauses = append(deleteClauses, exactRelationshipClause(tpl))
   191  
   192  		default:
   193  			return spiceerrors.MustBugf("unknown tuple mutation: %v", mut)
   194  		}
   195  	}
   196  
   197  	// Run CREATE insertions, if any.
   198  	if hasCreateInserts {
   199  		sql, args, err := createInserts.ToSql()
   200  		if err != nil {
   201  			return fmt.Errorf(errUnableToWriteRelationships, err)
   202  		}
   203  
   204  		_, err = rwt.tx.Exec(ctx, sql, args...)
   205  		if err != nil {
   206  			return fmt.Errorf(errUnableToWriteRelationships, err)
   207  		}
   208  	}
   209  
   210  	// For each of the TOUCH operations, invoke the INSERTs, but with `ON CONFLICT DO NOTHING` to ensure
   211  	// that the operations over existing relationships no-op.
   212  	if len(touchMutationsByNonCaveat) > 0 {
   213  		touchInserts = touchInserts.Suffix(fmt.Sprintf("ON CONFLICT DO NOTHING RETURNING %s, %s, %s, %s, %s, %s",
   214  			colNamespace,
   215  			colObjectID,
   216  			colRelation,
   217  			colUsersetNamespace,
   218  			colUsersetObjectID,
   219  			colUsersetRelation,
   220  		))
   221  
   222  		sql, args, err := touchInserts.ToSql()
   223  		if err != nil {
   224  			return fmt.Errorf(errUnableToWriteRelationships, err)
   225  		}
   226  
   227  		rows, err := rwt.tx.Query(ctx, sql, args...)
   228  		if err != nil {
   229  			return fmt.Errorf(errUnableToWriteRelationships, err)
   230  		}
   231  		defer rows.Close()
   232  
   233  		// Remove from the TOUCH map of operations each row that was successfully inserted.
   234  		// This will cover any TOUCH that created an entirely new relationship, acting like
   235  		// a CREATE.
   236  		tpl := &core.RelationTuple{
   237  			ResourceAndRelation: &core.ObjectAndRelation{},
   238  			Subject:             &core.ObjectAndRelation{},
   239  		}
   240  
   241  		for rows.Next() {
   242  			err := rows.Scan(
   243  				&tpl.ResourceAndRelation.Namespace,
   244  				&tpl.ResourceAndRelation.ObjectId,
   245  				&tpl.ResourceAndRelation.Relation,
   246  				&tpl.Subject.Namespace,
   247  				&tpl.Subject.ObjectId,
   248  				&tpl.Subject.Relation,
   249  			)
   250  			if err != nil {
   251  				return fmt.Errorf(errUnableToWriteRelationships, err)
   252  			}
   253  
   254  			tplString := tuple.StringWithoutCaveat(tpl)
   255  			_, ok := touchMutationsByNonCaveat[tplString]
   256  			if !ok {
   257  				return spiceerrors.MustBugf("missing expected completed TOUCH mutation")
   258  			}
   259  
   260  			delete(touchMutationsByNonCaveat, tplString)
   261  		}
   262  		rows.Close()
   263  
   264  		// For each remaining TOUCH mutation, add a "DELETE" operation for the row such that if the caveat and/or
   265  		// context has changed, the row will be deleted. For ones in which the caveat name and/or context did cause
   266  		// the deletion (because of a change), the row will be re-inserted with the new caveat name and/or context.
   267  		for _, mut := range touchMutationsByNonCaveat {
   268  			// If the relation support a simplified TOUCH operation, then skip the DELETE operation, as it is unnecessary
   269  			// because the relation does not support a caveat for a subject of this type.
   270  			if relationSupportSimplifiedTouch.Has(mut.Tuple.ResourceAndRelation.Namespace + "#" + mut.Tuple.ResourceAndRelation.Relation + "@" + mut.Tuple.Subject.Namespace) {
   271  				continue
   272  			}
   273  
   274  			deleteClauses = append(deleteClauses, exactRelationshipDifferentCaveatClause(mut.Tuple))
   275  		}
   276  	}
   277  
   278  	// Execute the "DELETE" operation (an UPDATE with setting the deletion ID to the current transaction ID)
   279  	// for any DELETE mutations or TOUCH mutations that matched existing relationships and whose caveat name
   280  	// or context is different in some manner. We use RETURNING to determine which TOUCHed relationships were
   281  	// deleted by virtue of their caveat name and/or context being changed.
   282  	if len(deleteClauses) == 0 {
   283  		// Nothing more to do.
   284  		return nil
   285  	}
   286  
   287  	builder := deleteTuple.
   288  		Where(deleteClauses).
   289  		Suffix(fmt.Sprintf("RETURNING %s, %s, %s, %s, %s, %s",
   290  			colNamespace,
   291  			colObjectID,
   292  			colRelation,
   293  			colUsersetNamespace,
   294  			colUsersetObjectID,
   295  			colUsersetRelation,
   296  		))
   297  
   298  	sql, args, err := builder.
   299  		Set(colDeletedXid, rwt.newXID).
   300  		ToSql()
   301  	if err != nil {
   302  		return fmt.Errorf(errUnableToWriteRelationships, err)
   303  	}
   304  
   305  	rows, err := rwt.tx.Query(ctx, sql, args...)
   306  	if err != nil {
   307  		return fmt.Errorf(errUnableToWriteRelationships, err)
   308  	}
   309  	defer rows.Close()
   310  
   311  	// For each deleted row representing a TOUCH, recreate with the new caveat and/or context.
   312  	touchWrite := writeTuple
   313  	touchWriteHasValues := false
   314  
   315  	deletedTpl := &core.RelationTuple{
   316  		ResourceAndRelation: &core.ObjectAndRelation{},
   317  		Subject:             &core.ObjectAndRelation{},
   318  	}
   319  
   320  	for rows.Next() {
   321  		err := rows.Scan(
   322  			&deletedTpl.ResourceAndRelation.Namespace,
   323  			&deletedTpl.ResourceAndRelation.ObjectId,
   324  			&deletedTpl.ResourceAndRelation.Relation,
   325  			&deletedTpl.Subject.Namespace,
   326  			&deletedTpl.Subject.ObjectId,
   327  			&deletedTpl.Subject.Relation,
   328  		)
   329  		if err != nil {
   330  			return fmt.Errorf(errUnableToWriteRelationships, err)
   331  		}
   332  
   333  		tplString := tuple.StringWithoutCaveat(deletedTpl)
   334  		mutation, ok := touchMutationsByNonCaveat[tplString]
   335  		if !ok {
   336  			// This did not represent a TOUCH operation.
   337  			continue
   338  		}
   339  
   340  		touchWrite = appendForInsertion(touchWrite, mutation.Tuple)
   341  		touchWriteHasValues = true
   342  	}
   343  	rows.Close()
   344  
   345  	// If no INSERTs are necessary to update caveats, then nothing more to do.
   346  	if !touchWriteHasValues {
   347  		return nil
   348  	}
   349  
   350  	// Otherwise execute the INSERTs for the caveated-changes TOUCHed relationships.
   351  	sql, args, err = touchWrite.ToSql()
   352  	if err != nil {
   353  		return fmt.Errorf(errUnableToWriteRelationships, err)
   354  	}
   355  
   356  	_, err = rwt.tx.Exec(ctx, sql, args...)
   357  	if err != nil {
   358  		return fmt.Errorf(errUnableToWriteRelationships, err)
   359  	}
   360  
   361  	return nil
   362  }
   363  
   364  func (rwt *pgReadWriteTXN) DeleteRelationships(ctx context.Context, filter *v1.RelationshipFilter, opts ...options.DeleteOptionsOption) (bool, error) {
   365  	delOpts := options.NewDeleteOptionsWithOptionsAndDefaults(opts...)
   366  	if delOpts.DeleteLimit != nil && *delOpts.DeleteLimit > 0 {
   367  		return rwt.deleteRelationshipsWithLimit(ctx, filter, *delOpts.DeleteLimit)
   368  	}
   369  
   370  	return false, rwt.deleteRelationships(ctx, filter)
   371  }
   372  
   373  func (rwt *pgReadWriteTXN) deleteRelationshipsWithLimit(ctx context.Context, filter *v1.RelationshipFilter, limit uint64) (bool, error) {
   374  	// Construct a select query for the relationships to be removed.
   375  	query := selectForDelete
   376  
   377  	if filter.ResourceType != "" {
   378  		query = query.Where(sq.Eq{colNamespace: filter.ResourceType})
   379  	}
   380  	if filter.OptionalResourceId != "" {
   381  		query = query.Where(sq.Eq{colObjectID: filter.OptionalResourceId})
   382  	}
   383  	if filter.OptionalRelation != "" {
   384  		query = query.Where(sq.Eq{colRelation: filter.OptionalRelation})
   385  	}
   386  	if filter.OptionalResourceIdPrefix != "" {
   387  		if strings.Contains(filter.OptionalResourceIdPrefix, "%") {
   388  			return false, fmt.Errorf("unable to delete relationships with a prefix containing the %% character")
   389  		}
   390  
   391  		query = query.Where(sq.Like{colObjectID: filter.OptionalResourceIdPrefix + "%"})
   392  	}
   393  
   394  	// Add clauses for the SubjectFilter
   395  	if subjectFilter := filter.OptionalSubjectFilter; subjectFilter != nil {
   396  		query = query.Where(sq.Eq{colUsersetNamespace: subjectFilter.SubjectType})
   397  		if subjectFilter.OptionalSubjectId != "" {
   398  			query = query.Where(sq.Eq{colUsersetObjectID: subjectFilter.OptionalSubjectId})
   399  		}
   400  		if relationFilter := subjectFilter.OptionalRelation; relationFilter != nil {
   401  			query = query.Where(sq.Eq{colUsersetRelation: stringz.DefaultEmpty(relationFilter.Relation, datastore.Ellipsis)})
   402  		}
   403  	}
   404  
   405  	query = query.Limit(limit)
   406  
   407  	selectSQL, args, err := query.ToSql()
   408  	if err != nil {
   409  		return false, fmt.Errorf(errUnableToDeleteRelationships, err)
   410  	}
   411  
   412  	args = append(args, rwt.newXID)
   413  
   414  	// Construct a CTE to update the relationships as removed.
   415  	cteSQL := fmt.Sprintf(
   416  		"WITH found_tuples AS (%s)\nUPDATE %s SET %s = $%d WHERE (%s, %s, %s, %s, %s, %s, %s) IN (select * from found_tuples)",
   417  		selectSQL,
   418  		tableTuple,
   419  		colDeletedXid,
   420  		len(args),
   421  		colNamespace,
   422  		colObjectID,
   423  		colRelation,
   424  		colUsersetNamespace,
   425  		colUsersetObjectID,
   426  		colUsersetRelation,
   427  		colCreatedXid,
   428  	)
   429  
   430  	result, err := rwt.tx.Exec(ctx, cteSQL, args...)
   431  	if err != nil {
   432  		return false, fmt.Errorf(errUnableToDeleteRelationships, err)
   433  	}
   434  
   435  	return result.RowsAffected() == int64(limit), nil
   436  }
   437  
   438  func (rwt *pgReadWriteTXN) deleteRelationships(ctx context.Context, filter *v1.RelationshipFilter) error {
   439  	// Add clauses for the ResourceFilter
   440  	query := deleteTuple
   441  	if filter.ResourceType != "" {
   442  		query = query.Where(sq.Eq{colNamespace: filter.ResourceType})
   443  	}
   444  	if filter.OptionalResourceId != "" {
   445  		query = query.Where(sq.Eq{colObjectID: filter.OptionalResourceId})
   446  	}
   447  	if filter.OptionalRelation != "" {
   448  		query = query.Where(sq.Eq{colRelation: filter.OptionalRelation})
   449  	}
   450  	if filter.OptionalResourceIdPrefix != "" {
   451  		if strings.Contains(filter.OptionalResourceIdPrefix, "%") {
   452  			return fmt.Errorf("unable to delete relationships with a prefix containing the %% character")
   453  		}
   454  
   455  		query = query.Where(sq.Like{colObjectID: filter.OptionalResourceIdPrefix + "%"})
   456  	}
   457  
   458  	// Add clauses for the SubjectFilter
   459  	if subjectFilter := filter.OptionalSubjectFilter; subjectFilter != nil {
   460  		query = query.Where(sq.Eq{colUsersetNamespace: subjectFilter.SubjectType})
   461  		if subjectFilter.OptionalSubjectId != "" {
   462  			query = query.Where(sq.Eq{colUsersetObjectID: subjectFilter.OptionalSubjectId})
   463  		}
   464  		if relationFilter := subjectFilter.OptionalRelation; relationFilter != nil {
   465  			query = query.Where(sq.Eq{colUsersetRelation: stringz.DefaultEmpty(relationFilter.Relation, datastore.Ellipsis)})
   466  		}
   467  	}
   468  
   469  	sql, args, err := query.Set(colDeletedXid, rwt.newXID).ToSql()
   470  	if err != nil {
   471  		return fmt.Errorf(errUnableToDeleteRelationships, err)
   472  	}
   473  
   474  	if _, err := rwt.tx.Exec(ctx, sql, args...); err != nil {
   475  		return fmt.Errorf(errUnableToDeleteRelationships, err)
   476  	}
   477  
   478  	return nil
   479  }
   480  
   481  func (rwt *pgReadWriteTXN) WriteNamespaces(ctx context.Context, newConfigs ...*core.NamespaceDefinition) error {
   482  	deletedNamespaceClause := sq.Or{}
   483  	writeQuery := writeNamespace
   484  
   485  	for _, newNamespace := range newConfigs {
   486  		serialized, err := proto.Marshal(newNamespace)
   487  		if err != nil {
   488  			return fmt.Errorf(errUnableToWriteConfig, err)
   489  		}
   490  
   491  		deletedNamespaceClause = append(deletedNamespaceClause, sq.Eq{colNamespace: newNamespace.Name})
   492  
   493  		valuesToWrite := []interface{}{newNamespace.Name, serialized}
   494  
   495  		writeQuery = writeQuery.Values(valuesToWrite...)
   496  	}
   497  
   498  	delSQL, delArgs, err := deleteNamespace.
   499  		Set(colDeletedXid, rwt.newXID).
   500  		Where(sq.And{sq.Eq{colDeletedXid: liveDeletedTxnID}, deletedNamespaceClause}).
   501  		ToSql()
   502  	if err != nil {
   503  		return fmt.Errorf(errUnableToWriteConfig, err)
   504  	}
   505  
   506  	_, err = rwt.tx.Exec(ctx, delSQL, delArgs...)
   507  	if err != nil {
   508  		return fmt.Errorf(errUnableToWriteConfig, err)
   509  	}
   510  
   511  	sql, args, err := writeQuery.ToSql()
   512  	if err != nil {
   513  		return fmt.Errorf(errUnableToWriteConfig, err)
   514  	}
   515  
   516  	if _, err = rwt.tx.Exec(ctx, sql, args...); err != nil {
   517  		return fmt.Errorf(errUnableToWriteConfig, err)
   518  	}
   519  
   520  	return nil
   521  }
   522  
   523  func (rwt *pgReadWriteTXN) DeleteNamespaces(ctx context.Context, nsNames ...string) error {
   524  	filterer := func(original sq.SelectBuilder) sq.SelectBuilder {
   525  		return original.Where(sq.Eq{colDeletedXid: liveDeletedTxnID})
   526  	}
   527  
   528  	nsClauses := make([]sq.Sqlizer, 0, len(nsNames))
   529  	tplClauses := make([]sq.Sqlizer, 0, len(nsNames))
   530  	querier := pgxcommon.QuerierFuncsFor(rwt.tx)
   531  	for _, nsName := range nsNames {
   532  		_, _, err := rwt.loadNamespace(ctx, nsName, querier, filterer)
   533  		switch {
   534  		case errors.As(err, &datastore.ErrNamespaceNotFound{}):
   535  			return err
   536  
   537  		case err == nil:
   538  			nsClauses = append(nsClauses, sq.Eq{colNamespace: nsName})
   539  			tplClauses = append(tplClauses, sq.Eq{colNamespace: nsName})
   540  
   541  		default:
   542  			return fmt.Errorf(errUnableToDeleteConfig, err)
   543  		}
   544  	}
   545  
   546  	delSQL, delArgs, err := deleteNamespace.
   547  		Set(colDeletedXid, rwt.newXID).
   548  		Where(sq.Or(nsClauses)).
   549  		ToSql()
   550  	if err != nil {
   551  		return fmt.Errorf(errUnableToDeleteConfig, err)
   552  	}
   553  
   554  	_, err = rwt.tx.Exec(ctx, delSQL, delArgs...)
   555  	if err != nil {
   556  		return fmt.Errorf(errUnableToDeleteConfig, err)
   557  	}
   558  
   559  	deleteTupleSQL, deleteTupleArgs, err := deleteNamespaceTuples.
   560  		Set(colDeletedXid, rwt.newXID).
   561  		Where(sq.Or(tplClauses)).
   562  		ToSql()
   563  	if err != nil {
   564  		return fmt.Errorf(errUnableToDeleteConfig, err)
   565  	}
   566  
   567  	_, err = rwt.tx.Exec(ctx, deleteTupleSQL, deleteTupleArgs...)
   568  	if err != nil {
   569  		return fmt.Errorf(errUnableToDeleteConfig, err)
   570  	}
   571  
   572  	return nil
   573  }
   574  
   575  var copyCols = []string{
   576  	colNamespace,
   577  	colObjectID,
   578  	colRelation,
   579  	colUsersetNamespace,
   580  	colUsersetObjectID,
   581  	colUsersetRelation,
   582  	colCaveatContextName,
   583  	colCaveatContext,
   584  }
   585  
   586  func (rwt *pgReadWriteTXN) BulkLoad(ctx context.Context, iter datastore.BulkWriteRelationshipSource) (uint64, error) {
   587  	return pgxcommon.BulkLoad(ctx, rwt.tx, tableTuple, copyCols, iter)
   588  }
   589  
   590  func exactRelationshipClause(r *core.RelationTuple) sq.Eq {
   591  	return sq.Eq{
   592  		colNamespace:        r.ResourceAndRelation.Namespace,
   593  		colObjectID:         r.ResourceAndRelation.ObjectId,
   594  		colRelation:         r.ResourceAndRelation.Relation,
   595  		colUsersetNamespace: r.Subject.Namespace,
   596  		colUsersetObjectID:  r.Subject.ObjectId,
   597  		colUsersetRelation:  r.Subject.Relation,
   598  	}
   599  }
   600  
   601  func exactRelationshipDifferentCaveatClause(r *core.RelationTuple) sq.And {
   602  	var caveatName string
   603  	var caveatContext map[string]any
   604  	if r.Caveat != nil {
   605  		caveatName = r.Caveat.CaveatName
   606  		caveatContext = r.Caveat.Context.AsMap()
   607  	}
   608  
   609  	return sq.And{
   610  		sq.Eq{
   611  			colNamespace:        r.ResourceAndRelation.Namespace,
   612  			colObjectID:         r.ResourceAndRelation.ObjectId,
   613  			colRelation:         r.ResourceAndRelation.Relation,
   614  			colUsersetNamespace: r.Subject.Namespace,
   615  			colUsersetObjectID:  r.Subject.ObjectId,
   616  			colUsersetRelation:  r.Subject.Relation,
   617  		},
   618  		sq.Or{
   619  			sq.NotEq{
   620  				colCaveatContextName: caveatName,
   621  			},
   622  			sq.NotEq{
   623  				colCaveatContext: caveatContext,
   624  			},
   625  		},
   626  	}
   627  }
   628  
   629  var _ datastore.ReadWriteTransaction = &pgReadWriteTXN{}