Skip to content

StreamOne/WriteSingle with a Select() projection throws "Field not found in row: data" when emitETag is on (regression in 9.18.0) #5158

Description

@jeremydmiller

Summary

WriteSingle / StreamOne<T> throws at runtime when the queryable carries a Select() projection, because the ETag support appends an mt_version column to a select that has no data column.

Regression introduced by #5027 / PR #5030 (44f9b44a2), first shipped in 9.18.0. Still present on master at 0e8daed37.

Repro

await using var query = store.QuerySession();
var context = new DefaultHttpContext { Response = { Body = new MemoryStream() } };

await query.Query<Issue>().Where(x => x.Id == issue.Id)
    .Select(x => new { x.Description })      // <-- any Select() projection
    .WriteSingle(context, emitETag: true);   // emitETag: true is the default
System.IndexOutOfRangeException : Field not found in row: data
   at Npgsql.BackendMessages.RowDescriptionMessage.GetFieldIndex(String name)
   at Npgsql.NpgsqlDataReader.GetOrdinal(String name)
   at Marten.Services.JsonStreamingExtensions.StreamOneWithVersion(DbDataReader reader, ...)
   at Marten.Internal.Sessions.QuerySession.StreamOneWithVersion(DbCommand command, ...)
   at Marten.Linq.MartenLinqQueryProvider.StreamOneWithVersion[T](Expression expression, ...)
   at Marten.AspNetCore.QueryableExtensions.WriteSingle[T](IQueryable`1 queryable, ...)

Passing emitETag: false works, which is the current workaround — it routes to the plain StreamJsonFirstOrDefault path that never appends the version column.

Root cause

In MartenLinqQueryProvider.StreamOneWithVersion<T>, T is the projected type once a Select() is in play — an anonymous type, or any DTO — not the mapped document type:

var mapping = _session.Options.Storage.FindMapping(typeof(T)) as DocumentMapping;

if (numericRevision || mapping is { Metadata.Version.Enabled: true })
{
    main.SelectClause = new VersionSelectClause<T>(main.SelectClause);
    ...
}

FindMapping on an unmapped type returns a freshly-built DocumentMapping whose Metadata.Version.Enabled defaults to true, so the guard passes. VersionSelectClause then appends d.mt_version as mt_etag_version to a select clause that projects scalars rather than the data column, and the reader's GetOrdinal("data") blows up.

The check needs to key off whether the select clause actually produces the document payload (or off the source document type behind the projection), not off a mapping looked up by the projected type.

Scope

  • Affects WriteSingle and therefore StreamOne<T> whenever a Select() projection is combined with the default emitETag: true.
  • The revision flavor added in Derive the StreamOne ETag from the numeric revision for revisioned documents #5121 has the same shape, but is unreachable for projected types: an unmapped type gets the Guid default, so it always takes the Version branch.
  • The StreamOne<TDoc, TOut> compiled-query overload does not participate in ETag handling and is unaffected.
  • Pre-9.18 (before the inline mt_version read) this combination worked.

Verification

Reproduced on a clean checkout of master at de63fc5ed — i.e. before #5121 — so this is not related to that change. Also reproduces on 0e8daed37.

Noted while reviewing #5121; deliberately left out of the follow-up (#5157) since it is a separate pre-existing defect.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions