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