Fix #5062: mt_quick_append_events returned {NULL} for an empty event array - #5088
Merged
Merged
Conversation
…array
`array_length('{}', 1)` is NULL in PostgreSQL rather than 0, so
`event_version + array_length(event_ids, 1)` evaluated to NULL and the
function returned a single-element array holding NULL whenever it was
called with no events. Npgsql then failed the read into `long[]`:
System.InvalidCastException: Cannot read a non-nullable collection of
elements because the returned array contains nulls.
That cast failure surfaced from QuickAppendEventsOperationBase.Postprocess
Async -- inside the batch's callback loop -- so it propagated out of the
loop and discarded whatever exception had already been collected, leaving
callers with an unrelated, non-retryable InvalidCastException instead of
the real error. On the reporter's system that dead-lettered Wolverine
messages that would otherwise have been retried.
Three coordinated changes:
* COALESCE the array_length so the empty case means what it says: zero
events appended, final version = the stream's current version.
* Skip the field-0 read in PostprocessAsync when the stream carries no
events. Nothing needs post-processing there anyway, and this keeps a
database whose function has not been migrated yet from throwing the
cast exception (the row is still consumed, so the reader stays aligned
for the rest of the page).
* Don't issue the call at all for a zero-event Append in
ProjectionUpdateBatch.WaitForCompletion -- the one caller in Marten
that could reach the function with empty arrays. A StartStream still
gets its mt_streams row.
Regression tests pin both the function (built from the deployed signature
read out of pg_get_function_arguments, so it holds across metadata /
timestamp / tag / bigint configurations) and Marten's own generated call
site. Both fail on master with the reported InvalidCastException.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Jul 30, 2026
Open
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #5062.
The defect
array_length('{}', 1)is NULL in PostgreSQL, not 0. Soevaluated to NULL whenever
mt_quick_append_eventswas called with no events, and the function returned a single-element array holding NULL. Npgsql then failed the read intolong[]:Why it masked the real error
The cast failure surfaced from
QuickAppendEventsOperationBase.PostprocessAsync— i.e. from insideOperationPage.ApplyCallbacksAsync, the loop that collects per-operation exceptions into the batch'sexceptionslist. A throw out of that loop lands inExecuteBatchPagesAsync'scatch, which rethrows the thrown exception and discards everything already collected. Callers therefore got an unrelated, non-retryableInvalidCastExceptioninstead of whatever actually made the append fail. On the reporter's system that dead-lettered Wolverine messages that would otherwise have been retried.The fix
Three coordinated changes:
QuickAppendEventFunction—COALESCE(array_length(event_ids, 1), 0), so the empty case means what it says: zero events appended, final version = the stream's current version. This is the reporter's suggested fix and the actual correctness fix.QuickAppendEventsOperationBase.PostprocessAsync— skip the field-0 read when the stream carries no events. Nothing needs post-processing there anyway, and this matters for databases whose function has not been migrated yet (AutoCreate.None+ explicit migrations): they stop throwing the cast exception on the old function. The row is still consumed, so the reader stays aligned for the rest of the page.ProjectionUpdateBatch.WaitForCompletion— the one caller in Marten that could reach the function with empty arrays (anAppendside effect that ended up with no events; every other path already filters onEvents.Any()). AStartStreamstill gets itsmt_streamsrow.Tests
TenantPartitionedEventsTests/Regressions/Bug_5062_empty_quick_append.cs, on the reported configuration (Conjoined+UseTenantPartitionedEvents+ Quick):function_returns_a_non_null_version_for_an_empty_event_array— the issue's SQL repro. The function's parameter list varies with configuration (metadata columns, server timestamps, tag tables, bigint events), so the test reads the deployed signature back out ofpg_get_function_argumentsand feeds every array parameter an empty array. That pins the function itself rather than one config's hand-written call.empty_quick_append_operation_is_a_clean_no_op— the same empty append driven through Marten's own generated call site and result read, which is where theInvalidCastExceptionactually landed.Both fail on master with the exact reported
InvalidCastException; both pass with the fix. FullTenantPartitionedEventsTestssuite green on net9.0 (238 passed, 2 pre-existing skips).Upgrade note
The function body changed, so existing databases will show one
mt_quick_append_eventsupdate in the next schema migration. It is idempotent afterwards.🤖 Generated with Claude Code