From ad08395639eb6369c978f6813cf2989a0d482679 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Thu, 10 Sep 2026 17:04:02 -0400 Subject: [PATCH] Add optional cursor to DeleteRelationships for resumable batched deletion Adds optional_cursor to DeleteRelationshipsRequest and after_result_cursor to DeleteRelationshipsResponse, mirroring the cursor pattern already used by ReadRelationships and LookupResources. Together they let a large partial deletion be performed as a series of calls that each resume where the previous one left off, rather than re-examining the relationships already deleted by earlier calls. On datastores whose deletion can be ordered and resumed (e.g. CockroachDB via a primary-key cursor) this avoids the per-batch rescan of prior deletions' tombstones that makes a naive limit loop degrade on large tables. The fields require optional_limit and optional_allow_partial_deletions, and are supported only by datastores that can order and resume deletion; others return an error when a cursor is supplied. The change is additive and backward compatible (buf breaking passes against main). Signed-off-by: Joseph Schorr --- authzed/api/v1/permission_service.proto | 18 ++++++++++++++++++ docs/apidocs.swagger.json | 8 ++++++++ 2 files changed, 26 insertions(+) diff --git a/authzed/api/v1/permission_service.proto b/authzed/api/v1/permission_service.proto index a7e1c7d..402f011 100644 --- a/authzed/api/v1/permission_service.proto +++ b/authzed/api/v1/permission_service.proto @@ -431,6 +431,17 @@ message DeleteRelationshipsRequest { (validate.rules).message.required = false, (buf.validate.field).required = false ]; + + // optional_cursor, if specified, indicates the cursor after which deletion should resume. It is used to + // continue a batched, partial deletion where a previous call left off, by passing back the + // after_result_cursor returned on the previous DeleteRelationshipsResponse. + // + // A cursor allows a large deletion to be performed as a series of calls without re-examining the + // relationships already deleted by earlier calls. It therefore requires optional_limit and + // optional_allow_partial_deletions to be set, and is only supported by datastores whose deletion can be + // ordered and resumed; datastores that do not support cursored deletion will return an error if a cursor + // is provided. + Cursor optional_cursor = 6; } message DeleteRelationshipsResponse { @@ -456,6 +467,13 @@ message DeleteRelationshipsResponse { // relationships_deleted_count is the number of relationships that were deleted. uint64 relationships_deleted_count = 3; + + // after_result_cursor holds a cursor that can be used to resume the deletion after the relationships + // deleted by this call, by supplying it as the optional_cursor on a subsequent DeleteRelationshipsRequest. + // + // It is populated only when deletion_progress is DELETION_PROGRESS_PARTIAL and the datastore supports + // cursored deletion; it is unset once DELETION_PROGRESS_COMPLETE is returned. + Cursor after_result_cursor = 4; } // CheckPermissionRequest issues a check on whether a subject has a permission diff --git a/docs/apidocs.swagger.json b/docs/apidocs.swagger.json index a0ce995..01c22aa 100644 --- a/docs/apidocs.swagger.json +++ b/docs/apidocs.swagger.json @@ -1525,6 +1525,10 @@ "optionalTransactionMetadata": { "type": "object", "description": "optional_transaction_metadata is an optional field that can be used to store metadata about the transaction.\nIf specified, this metadata will be supplied in the WatchResponse for the deletions associated with\nthis transaction." + }, + "optionalCursor": { + "$ref": "#/definitions/v1.Cursor", + "description": "optional_cursor, if specified, indicates the cursor after which deletion should resume. It is used to\ncontinue a batched, partial deletion where a previous call left off, by passing back the\nafter_result_cursor returned on the previous DeleteRelationshipsResponse.\n\nA cursor allows a large deletion to be performed as a series of calls without re-examining the\nrelationships already deleted by earlier calls. It therefore requires optional_limit and\noptional_allow_partial_deletions to be set, and is only supported by datastores whose deletion can be\nordered and resumed; datastores that do not support cursored deletion will return an error if a cursor\nis provided." } }, "description": "DeleteRelationshipsRequest specifies which Relationships should be deleted,\nrequesting the delete of *ALL* relationships that match the specified\nfilters. If the optional_preconditions parameter is included, all of the\nspecified preconditions must also be satisfied before the delete will be\nexecuted." @@ -1544,6 +1548,10 @@ "type": "string", "format": "uint64", "description": "relationships_deleted_count is the number of relationships that were deleted." + }, + "afterResultCursor": { + "$ref": "#/definitions/v1.Cursor", + "description": "after_result_cursor holds a cursor that can be used to resume the deletion after the relationships\ndeleted by this call, by supplying it as the optional_cursor on a subsequent DeleteRelationshipsRequest.\n\nIt is populated only when deletion_progress is DELETION_PROGRESS_PARTIAL and the datastore supports\ncursored deletion; it is unset once DELETION_PROGRESS_COMPLETE is returned." } } },