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