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