Skip to content

StatementOperation is a NoDataReturnedCall: no false ConcurrencyException after UndoDeleteWhere / HardDeleteWhere (#5743) - #5749

Merged
jeremydmiller merged 1 commit into
masterfrom
gh-5743-statement-operation-no-data
Oct 23, 2026
Merged

jeremydmiller merged 1 commit into
masterfrom
gh-5743-statement-operation-no-data

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Closes #5743, found today while implementing #5740's undelete.

Root cause

When ApplyCallbacksAsync post-processes a batch, it calls NextResult after every operation that is not a NoDataReturnedCall.

  • UndoDeleteWhere and HardDeleteWhere queue a bare StatementOperation. So does the projection UnDelete path.
  • StatementOperation was not marked NoDataReturnedCall, although its subclasses Deletion (through IDeletion) and PatchOperation always were.
  • Its UPDATE or DELETE produces no result set, so that NextResult skipped the next operation's result instead.
  • A numeric-revision upsert queued after it then read nothing back and threw ConcurrencyException for a write that had not conflicted. It did so even when the statement matched nothing.

DeleteWhere was never affected because it builds a Deletion. That is why the issue's table shows it working.

Fix

StatementOperation : Statement, IStorageOperation, NoDataReturnedCall, with a remark saying why the marker is load-bearing.

The projection UnDelete paths had the same misalignment. It was harmless there only because the upsert after them ignores revision conflicts (IgnoreConcurrencyViolation, or Revision == 0).

Tests

DocumentDbTests.Deleting.Bug_5743_statement_before_revisioned_upsert:

  • UndoDeleteWhere, then UpdateRevision(note, 2): red on master (ConcurrencyException), green here.
  • HardDeleteWhere, then UpdateRevision(note, 2): red on master, green here.
  • A genuinely stale UpdateRevision(note, 1) after the statement is still refused, so the fix does not switch the guard off.

Regression runs (net10.0, PG 17):

  • the whole DocumentDbTests.Deleting namespace: 83 passed, 1 skipped;
  • the EventSourcingTests soft-delete, ShouldDelete and archiving projection classes: 49 passed, 1 skipped.

#5744's UndeleteDocumentAsync queues the upsert before UndoDeleteWhere to step around this bug, and its comment says so. That ordering stays correct after this fix.

🤖 Generated with Claude Code

UndoDeleteWhere and HardDeleteWhere (and the projection UnDelete paths) queue a bare StatementOperation,
which was not marked NoDataReturnedCall, unlike Deletion and PatchOperation. The batch therefore called
NextResult after its UPDATE / DELETE, which produces no result set, and skipped the next operation's
result. A numeric-revision upsert queued after it read nothing back and threw a ConcurrencyException for a
write that had not conflicted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

UndoDeleteWhere / HardDeleteWhere queued before a numeric-revision upsert of the same type throws a false ConcurrencyException

1 participant