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

     1  package mysql
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"database/sql"
     7  	"database/sql/driver"
     8  	"encoding/json"
     9  	"errors"
    10  	"fmt"
    11  	"regexp"
    12  	"strings"
    13  
    14  	sq "github.com/Masterminds/squirrel"
    15  	v1 "github.com/authzed/authzed-go/proto/authzed/api/v1"
    16  	"github.com/go-sql-driver/mysql"
    17  	"github.com/jzelinskie/stringz"
    18  	"google.golang.org/protobuf/proto"
    19  
    20  	"github.com/authzed/spicedb/internal/datastore/common"
    21  	log "github.com/authzed/spicedb/internal/logging"
    22  	"github.com/authzed/spicedb/pkg/datastore"
    23  	"github.com/authzed/spicedb/pkg/datastore/options"
    24  	core "github.com/authzed/spicedb/pkg/proto/core/v1"
    25  	"github.com/authzed/spicedb/pkg/spiceerrors"
    26  	"github.com/authzed/spicedb/pkg/tuple"
    27  )
    28  
    29  const (
    30  	errUnableToWriteRelationships     = "unable to write relationships: %w"
    31  	errUnableToBulkWriteRelationships = "unable to bulk write relationships: %w"
    32  	errUnableToDeleteRelationships    = "unable to delete relationships: %w"
    33  	errUnableToWriteConfig            = "unable to write namespace config: %w"
    34  	errUnableToDeleteConfig           = "unable to delete namespace config: %w"
    35  
    36  	bulkInsertRowsLimit = 1_000
    37  )
    38  
    39  var (
    40  	duplicateEntryRegex          = regexp.MustCompile(`^Duplicate entry '(.+)' for key 'uq_relation_tuple_living'$`)
    41  	duplicateEntryFullIndexRegex = regexp.MustCompile(`^Duplicate entry '(.+)' for key 'relation_tuple.uq_relation_tuple_living'$`)
    42  )
    43  
    44  type mysqlReadWriteTXN struct {
    45  	*mysqlReader
    46  
    47  	tupleTableName string
    48  	tx             *sql.Tx
    49  	newTxnID       uint64
    50  }
    51  
    52  // caveatContextWrapper is used to marshall maps into MySQLs JSON data type
    53  type caveatContextWrapper map[string]any
    54  
    55  func (cc *caveatContextWrapper) Scan(val any) error {
    56  	v, ok := val.([]byte)
    57  	if !ok {
    58  		return fmt.Errorf("unsupported type: %T", v)
    59  	}
    60  	return json.Unmarshal(v, &cc)
    61  }
    62  
    63  func (cc *caveatContextWrapper) Value() (driver.Value, error) {
    64  	return json.Marshal(&cc)
    65  }
    66  
    67  // WriteRelationships takes a list of existing relationships that must exist, and a list of
    68  // tuple mutations and applies it to the datastore for the specified namespace.
    69  func (rwt *mysqlReadWriteTXN) WriteRelationships(ctx context.Context, mutations []*core.RelationTupleUpdate) error {
    70  	// TODO(jschorr): Determine if we can do this in a more efficient manner using ON CONFLICT UPDATE
    71  	// rather than SELECT FOR UPDATE as we've been doing.
    72  	bulkWrite := rwt.WriteTupleQuery
    73  	bulkWriteHasValues := false
    74  
    75  	selectForUpdateQuery := rwt.QueryTuplesWithIdsQuery
    76  
    77  	clauses := sq.Or{}
    78  	createAndTouchMutationsByTuple := make(map[string]*core.RelationTupleUpdate, len(mutations))
    79  
    80  	// Collect all TOUCH and DELETE operations. CREATE is handled below.
    81  	for _, mut := range mutations {
    82  		tpl := mut.Tuple
    83  		tplString := tuple.StringWithoutCaveat(tpl)
    84  
    85  		switch mut.Operation {
    86  		case core.RelationTupleUpdate_CREATE:
    87  			createAndTouchMutationsByTuple[tplString] = mut
    88  
    89  		case core.RelationTupleUpdate_TOUCH:
    90  			createAndTouchMutationsByTuple[tplString] = mut
    91  			clauses = append(clauses, exactRelationshipClause(tpl))
    92  
    93  		case core.RelationTupleUpdate_DELETE:
    94  			clauses = append(clauses, exactRelationshipClause(tpl))
    95  
    96  		default:
    97  			return spiceerrors.MustBugf("unknown mutation operation")
    98  		}
    99  	}
   100  
   101  	if len(clauses) > 0 {
   102  		query, args, err := selectForUpdateQuery.
   103  			Where(clauses).
   104  			Where(sq.GtOrEq{colDeletedTxn: rwt.newTxnID}).
   105  			ToSql()
   106  		if err != nil {
   107  			return fmt.Errorf(errUnableToWriteRelationships, err)
   108  		}
   109  
   110  		rows, err := rwt.tx.QueryContext(ctx, query, args...)
   111  		if err != nil {
   112  			return fmt.Errorf(errUnableToWriteRelationships, err)
   113  		}
   114  		defer common.LogOnError(ctx, rows.Close)
   115  
   116  		foundTpl := &core.RelationTuple{
   117  			ResourceAndRelation: &core.ObjectAndRelation{},
   118  			Subject:             &core.ObjectAndRelation{},
   119  		}
   120  
   121  		var caveatName string
   122  		var caveatContext caveatContextWrapper
   123  
   124  		tupleIdsToDelete := make([]int64, 0, len(clauses))
   125  		for rows.Next() {
   126  			var tupleID int64
   127  			if err := rows.Scan(
   128  				&tupleID,
   129  				&foundTpl.ResourceAndRelation.Namespace,
   130  				&foundTpl.ResourceAndRelation.ObjectId,
   131  				&foundTpl.ResourceAndRelation.Relation,
   132  				&foundTpl.Subject.Namespace,
   133  				&foundTpl.Subject.ObjectId,
   134  				&foundTpl.Subject.Relation,
   135  				&caveatName,
   136  				&caveatContext,
   137  			); err != nil {
   138  				return fmt.Errorf(errUnableToWriteRelationships, err)
   139  			}
   140  
   141  			// if the relationship to be deleted is for a TOUCH operation and the caveat
   142  			// name or context has not changed, then remove it from delete and create.
   143  			tplString := tuple.StringWithoutCaveat(foundTpl)
   144  			if mut, ok := createAndTouchMutationsByTuple[tplString]; ok {
   145  				foundTpl.Caveat, err = common.ContextualizedCaveatFrom(caveatName, caveatContext)
   146  				if err != nil {
   147  					return fmt.Errorf(errUnableToQueryTuples, err)
   148  				}
   149  
   150  				// Ensure the tuples are the same.
   151  				if tuple.Equal(mut.Tuple, foundTpl) {
   152  					delete(createAndTouchMutationsByTuple, tplString)
   153  					continue
   154  				}
   155  			}
   156  
   157  			tupleIdsToDelete = append(tupleIdsToDelete, tupleID)
   158  		}
   159  
   160  		if rows.Err() != nil {
   161  			return fmt.Errorf(errUnableToWriteRelationships, rows.Err())
   162  		}
   163  
   164  		if len(tupleIdsToDelete) > 0 {
   165  			query, args, err := rwt.
   166  				DeleteTupleQuery.
   167  				Where(sq.Eq{colID: tupleIdsToDelete}).
   168  				Set(colDeletedTxn, rwt.newTxnID).
   169  				ToSql()
   170  			if err != nil {
   171  				return fmt.Errorf(errUnableToWriteRelationships, err)
   172  			}
   173  			if _, err := rwt.tx.ExecContext(ctx, query, args...); err != nil {
   174  				return fmt.Errorf(errUnableToWriteRelationships, err)
   175  			}
   176  		}
   177  	}
   178  
   179  	for _, mut := range createAndTouchMutationsByTuple {
   180  		tpl := mut.Tuple
   181  
   182  		var caveatName string
   183  		var caveatContext caveatContextWrapper
   184  		if tpl.Caveat != nil {
   185  			caveatName = tpl.Caveat.CaveatName
   186  			caveatContext = tpl.Caveat.Context.AsMap()
   187  		}
   188  		bulkWrite = bulkWrite.Values(
   189  			tpl.ResourceAndRelation.Namespace,
   190  			tpl.ResourceAndRelation.ObjectId,
   191  			tpl.ResourceAndRelation.Relation,
   192  			tpl.Subject.Namespace,
   193  			tpl.Subject.ObjectId,
   194  			tpl.Subject.Relation,
   195  			caveatName,
   196  			&caveatContext,
   197  			rwt.newTxnID,
   198  		)
   199  		bulkWriteHasValues = true
   200  	}
   201  
   202  	if bulkWriteHasValues {
   203  		query, args, err := bulkWrite.ToSql()
   204  		if err != nil {
   205  			return fmt.Errorf(errUnableToWriteRelationships, err)
   206  		}
   207  
   208  		_, err = rwt.tx.ExecContext(ctx, query, args...)
   209  		if err != nil {
   210  			return fmt.Errorf(errUnableToWriteRelationships, err)
   211  		}
   212  	}
   213  
   214  	return nil
   215  }
   216  
   217  func (rwt *mysqlReadWriteTXN) DeleteRelationships(ctx context.Context, filter *v1.RelationshipFilter, opts ...options.DeleteOptionsOption) (bool, error) {
   218  	// Add clauses for the ResourceFilter
   219  	query := rwt.DeleteTupleQuery
   220  	if filter.ResourceType != "" {
   221  		query = query.Where(sq.Eq{colNamespace: filter.ResourceType})
   222  	}
   223  	if filter.OptionalResourceId != "" {
   224  		query = query.Where(sq.Eq{colObjectID: filter.OptionalResourceId})
   225  	}
   226  	if filter.OptionalRelation != "" {
   227  		query = query.Where(sq.Eq{colRelation: filter.OptionalRelation})
   228  	}
   229  	if filter.OptionalResourceIdPrefix != "" {
   230  		if strings.Contains(filter.OptionalResourceIdPrefix, "%") {
   231  			return false, fmt.Errorf("unable to delete relationships with a prefix containing the %% character")
   232  		}
   233  
   234  		query = query.Where(sq.Like{colObjectID: filter.OptionalResourceIdPrefix + "%"})
   235  	}
   236  
   237  	// Add clauses for the SubjectFilter
   238  	if subjectFilter := filter.OptionalSubjectFilter; subjectFilter != nil {
   239  		query = query.Where(sq.Eq{colUsersetNamespace: subjectFilter.SubjectType})
   240  		if subjectFilter.OptionalSubjectId != "" {
   241  			query = query.Where(sq.Eq{colUsersetObjectID: subjectFilter.OptionalSubjectId})
   242  		}
   243  		if relationFilter := subjectFilter.OptionalRelation; relationFilter != nil {
   244  			query = query.Where(sq.Eq{colUsersetRelation: stringz.DefaultEmpty(relationFilter.Relation, datastore.Ellipsis)})
   245  		}
   246  	}
   247  
   248  	query = query.Set(colDeletedTxn, rwt.newTxnID)
   249  
   250  	// Add the limit, if any.
   251  	delOpts := options.NewDeleteOptionsWithOptionsAndDefaults(opts...)
   252  	var delLimit uint64
   253  	if delOpts.DeleteLimit != nil && *delOpts.DeleteLimit > 0 {
   254  		delLimit = *delOpts.DeleteLimit
   255  	}
   256  
   257  	if delLimit > 0 {
   258  		query = query.Limit(delLimit)
   259  	}
   260  
   261  	querySQL, args, err := query.ToSql()
   262  	if err != nil {
   263  		return false, fmt.Errorf(errUnableToDeleteRelationships, err)
   264  	}
   265  
   266  	modified, err := rwt.tx.ExecContext(ctx, querySQL, args...)
   267  	if err != nil {
   268  		return false, fmt.Errorf(errUnableToDeleteRelationships, err)
   269  	}
   270  
   271  	rowsAffected, err := modified.RowsAffected()
   272  	if err != nil {
   273  		return false, fmt.Errorf(errUnableToDeleteRelationships, err)
   274  	}
   275  
   276  	if delLimit > 0 && uint64(rowsAffected) == delLimit {
   277  		return true, nil
   278  	}
   279  
   280  	return false, nil
   281  }
   282  
   283  func (rwt *mysqlReadWriteTXN) WriteNamespaces(ctx context.Context, newNamespaces ...*core.NamespaceDefinition) error {
   284  	deletedNamespaceClause := sq.Or{}
   285  	writeQuery := rwt.WriteNamespaceQuery
   286  
   287  	for _, newNamespace := range newNamespaces {
   288  		serialized, err := proto.Marshal(newNamespace)
   289  		if err != nil {
   290  			return fmt.Errorf(errUnableToWriteConfig, err)
   291  		}
   292  
   293  		deletedNamespaceClause = append(deletedNamespaceClause, sq.Eq{colNamespace: newNamespace.Name})
   294  		writeQuery = writeQuery.Values(newNamespace.Name, serialized, rwt.newTxnID)
   295  	}
   296  
   297  	delSQL, delArgs, err := rwt.DeleteNamespaceQuery.
   298  		Set(colDeletedTxn, rwt.newTxnID).
   299  		Where(sq.And{sq.Eq{colDeletedTxn: liveDeletedTxnID}, deletedNamespaceClause}).
   300  		ToSql()
   301  	if err != nil {
   302  		return fmt.Errorf(errUnableToWriteConfig, err)
   303  	}
   304  
   305  	_, err = rwt.tx.ExecContext(ctx, delSQL, delArgs...)
   306  	if err != nil {
   307  		return fmt.Errorf(errUnableToWriteConfig, err)
   308  	}
   309  
   310  	query, args, err := writeQuery.ToSql()
   311  	if err != nil {
   312  		return fmt.Errorf(errUnableToWriteConfig, err)
   313  	}
   314  
   315  	_, err = rwt.tx.ExecContext(ctx, query, args...)
   316  	if err != nil {
   317  		return fmt.Errorf(errUnableToWriteConfig, err)
   318  	}
   319  
   320  	return nil
   321  }
   322  
   323  func (rwt *mysqlReadWriteTXN) DeleteNamespaces(ctx context.Context, nsNames ...string) error {
   324  	// For each namespace, check they exist and collect predicates for the
   325  	// "WHERE" clause to delete the namespaces and associated tuples.
   326  	nsClauses := make([]sq.Sqlizer, 0, len(nsNames))
   327  	tplClauses := make([]sq.Sqlizer, 0, len(nsNames))
   328  	for _, nsName := range nsNames {
   329  		// TODO(jzelinskie): check these in one query
   330  		baseQuery := rwt.ReadNamespaceQuery.Where(sq.Eq{colDeletedTxn: liveDeletedTxnID})
   331  		_, createdAt, err := loadNamespace(ctx, nsName, rwt.tx, baseQuery)
   332  		switch {
   333  		case errors.As(err, &datastore.ErrNamespaceNotFound{}):
   334  			// TODO(jzelinskie): return the name of the missing namespace
   335  			return err
   336  		case err == nil:
   337  			nsClauses = append(nsClauses, sq.Eq{colNamespace: nsName, colCreatedTxn: createdAt})
   338  			tplClauses = append(tplClauses, sq.Eq{colNamespace: nsName})
   339  		default:
   340  			return fmt.Errorf(errUnableToDeleteConfig, err)
   341  		}
   342  	}
   343  
   344  	delSQL, delArgs, err := rwt.DeleteNamespaceQuery.
   345  		Set(colDeletedTxn, rwt.newTxnID).
   346  		Where(sq.Or(nsClauses)).
   347  		ToSql()
   348  	if err != nil {
   349  		return fmt.Errorf(errUnableToDeleteConfig, err)
   350  	}
   351  
   352  	_, err = rwt.tx.ExecContext(ctx, delSQL, delArgs...)
   353  	if err != nil {
   354  		return fmt.Errorf(errUnableToDeleteConfig, err)
   355  	}
   356  
   357  	deleteTupleSQL, deleteTupleArgs, err := rwt.DeleteNamespaceTuplesQuery.
   358  		Set(colDeletedTxn, rwt.newTxnID).
   359  		Where(sq.Or(tplClauses)).
   360  		ToSql()
   361  	if err != nil {
   362  		return fmt.Errorf(errUnableToDeleteConfig, err)
   363  	}
   364  
   365  	_, err = rwt.tx.ExecContext(ctx, deleteTupleSQL, deleteTupleArgs...)
   366  	if err != nil {
   367  		return fmt.Errorf(errUnableToDeleteConfig, err)
   368  	}
   369  
   370  	return nil
   371  }
   372  
   373  func (rwt *mysqlReadWriteTXN) BulkLoad(ctx context.Context, iter datastore.BulkWriteRelationshipSource) (uint64, error) {
   374  	var sqlStmt bytes.Buffer
   375  
   376  	sql, _, err := rwt.WriteTupleQuery.Values(1, 2, 3, 4, 5, 6, 7, 8, 9).ToSql()
   377  	if err != nil {
   378  		return 0, err
   379  	}
   380  
   381  	var numWritten uint64
   382  	var tpl *core.RelationTuple
   383  
   384  	// Bootstrap the loop
   385  	tpl, err = iter.Next(ctx)
   386  
   387  	for tpl != nil && err == nil {
   388  		sqlStmt.Reset()
   389  		sqlStmt.WriteString(sql)
   390  		var args []interface{}
   391  		var batchLen uint64
   392  
   393  		for ; tpl != nil && err == nil && batchLen < bulkInsertRowsLimit; tpl, err = iter.Next(ctx) {
   394  			if batchLen != 0 {
   395  				sqlStmt.WriteString(",(?,?,?,?,?,?,?,?,?)")
   396  			}
   397  
   398  			var caveatName string
   399  			var caveatContext caveatContextWrapper
   400  			if tpl.Caveat != nil {
   401  				caveatName = tpl.Caveat.CaveatName
   402  				caveatContext = tpl.Caveat.Context.AsMap()
   403  			}
   404  			args = append(args,
   405  				tpl.ResourceAndRelation.Namespace,
   406  				tpl.ResourceAndRelation.ObjectId,
   407  				tpl.ResourceAndRelation.Relation,
   408  				tpl.Subject.Namespace,
   409  				tpl.Subject.ObjectId,
   410  				tpl.Subject.Relation,
   411  				caveatName,
   412  				&caveatContext,
   413  				rwt.newTxnID,
   414  			)
   415  			batchLen++
   416  		}
   417  		if err != nil {
   418  			return 0, fmt.Errorf(errUnableToBulkWriteRelationships, err)
   419  		}
   420  
   421  		if batchLen > 0 {
   422  			log.Warn().Uint64("count", batchLen).Uint64("written", numWritten).Msg("writing batch")
   423  			if _, err := rwt.tx.Exec(sqlStmt.String(), args...); err != nil {
   424  				return 0, fmt.Errorf(errUnableToBulkWriteRelationships, fmt.Errorf("error writing batch: %w", err))
   425  			}
   426  		}
   427  
   428  		numWritten += batchLen
   429  	}
   430  	if err != nil {
   431  		return 0, fmt.Errorf(errUnableToBulkWriteRelationships, err)
   432  	}
   433  
   434  	return numWritten, nil
   435  }
   436  
   437  func convertToWriteConstraintError(err error) error {
   438  	var mysqlErr *mysql.MySQLError
   439  	if errors.As(err, &mysqlErr) && mysqlErr.Number == errMysqlDuplicateEntry {
   440  		found := duplicateEntryRegex.FindStringSubmatch(mysqlErr.Message)
   441  		if found != nil {
   442  			parts := strings.Split(found[1], "-")
   443  			if len(parts) == 7 {
   444  				return common.NewCreateRelationshipExistsError(&core.RelationTuple{
   445  					ResourceAndRelation: &core.ObjectAndRelation{
   446  						Namespace: parts[0],
   447  						ObjectId:  parts[1],
   448  						Relation:  parts[2],
   449  					},
   450  					Subject: &core.ObjectAndRelation{
   451  						Namespace: parts[3],
   452  						ObjectId:  parts[4],
   453  						Relation:  parts[5],
   454  					},
   455  				})
   456  			}
   457  		}
   458  
   459  		found = duplicateEntryFullIndexRegex.FindStringSubmatch(mysqlErr.Message)
   460  		if found != nil {
   461  			parts := strings.Split(found[1], "-")
   462  			if len(parts) == 7 {
   463  				return common.NewCreateRelationshipExistsError(&core.RelationTuple{
   464  					ResourceAndRelation: &core.ObjectAndRelation{
   465  						Namespace: parts[0],
   466  						ObjectId:  parts[1],
   467  						Relation:  parts[2],
   468  					},
   469  					Subject: &core.ObjectAndRelation{
   470  						Namespace: parts[3],
   471  						ObjectId:  parts[4],
   472  						Relation:  parts[5],
   473  					},
   474  				})
   475  			}
   476  		}
   477  
   478  		return common.NewCreateRelationshipExistsError(nil)
   479  	}
   480  	return nil
   481  }
   482  
   483  func exactRelationshipClause(r *core.RelationTuple) sq.Eq {
   484  	return sq.Eq{
   485  		colNamespace:        r.ResourceAndRelation.Namespace,
   486  		colObjectID:         r.ResourceAndRelation.ObjectId,
   487  		colRelation:         r.ResourceAndRelation.Relation,
   488  		colUsersetNamespace: r.Subject.Namespace,
   489  		colUsersetObjectID:  r.Subject.ObjectId,
   490  		colUsersetRelation:  r.Subject.Relation,
   491  	}
   492  }
   493  
   494  var _ datastore.ReadWriteTransaction = &mysqlReadWriteTXN{}