Skip to content

M5: query shapes (single, optional, scalar, execute, streaming)#5

Merged
lgamorim merged 7 commits into
masterfrom
feature/M5-query-shapes
Jul 11, 2026
Merged

M5: query shapes (single, optional, scalar, execute, streaming)#5
lgamorim merged 7 commits into
masterfrom
feature/M5-query-shapes

Conversation

@lgamorim

Copy link
Copy Markdown
Owner

Summary

Delivers M5 at 0.2.0-preview.2: the full return-shape matrix for generated queries, completing the codegen surface ahead of M6 (AOT + benchmarks).

  • Single & optional row: Task<T> with strict-single semantics (zero or multiple rows throw, surfacing missing-WHERE bugs); Task<T?> returns null on zero rows and still throws on more than one. Optionality comes from Nullable<T> for value-type rows and NRT annotations for reference rows.
  • Scalar shapes: element resolution is orthogonal to the outer shape — a supported scalar type reads the first column directly across Task<T>, Task<T?>, and Task<IReadOnlyList<T>>, so scalar lists (including nullable elements) fall out for free. Non-nullable scalars match ExecuteScalar expectations (first row, throw on no rows/DBNull).
  • [SqlExecute]: non-query statements get their own attribute since Task<int> cannot mean both a scalar SELECT and rows affected; named to avoid CS1614 ambiguity with Microsoft.Data.SqlClient.SqlCommand. Returns Task<int> (rows affected) or Task. New diagnostics: SQLB009 (unsupported execute return type), SQLB010 (both attributes on one method, reported exactly once).
  • Streaming: IAsyncEnumerable<T> methods emit as async iterators yielding rows as read — no buffering; [EnumeratorCancellation] emitted on the implementation-part parameter merges cleanly across partial declarations (no CS8425), confirmed by warning-free compile tests and the TreatWarningsAsErrors integration build.
  • Property-mapped rows: parameterless-constructible types with public set/init properties (inherited included) materialize via object initializers; a single parameterized public constructor still takes precedence, so positional records keep constructor mapping. Unmappable settable properties are an SQLB005 error rather than being silently skipped.
  • The emitter now dispatches on a shape discriminator in the model; the M4 row-list golden stayed byte-identical through the refactor, and a new golden locks the single-row output.

Test plan

  • 93 tests green in Release (39 unit, 41 generator snapshot/compile tests, 13 SQLite integration).
  • Integration tests exercise every shape against real in-memory SQLite: single hit/miss, optional miss, scalar count, scalar list, await foreach streaming, rows affected, and property-mapped mutable rows including the NULL→null path.
  • dotnet format --verify-no-changes, zero-warning build, and dotnet pack verified locally.

🤖 Generated with Claude Code

lgamorim and others added 7 commits July 11, 2026 15:05
Incrementing the preview suffix per milestone keeps packages built from
different milestones of the same phase distinguishable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The model gains a ResultShape discriminator and records the declared
return type verbatim, so the emitter dispatches per shape instead of
assuming a row list. Task<T> reads with strict-single semantics (zero
or multiple rows throw, surfacing missing-WHERE bugs), Task<T?> returns
null on zero rows and still throws on more than one. Optionality comes
from Nullable<T> for value-type rows and the nullable annotation for
reference-type rows. The M4 row-list golden stays byte-identical
through the refactor; a new golden locks the single-row output.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Element resolution is now orthogonal to the outer shape: a supported
scalar type reads the first column directly (ordinal 0, no GetOrdinal),
anything else stays a constructor-mapped row. Non-nullable scalars
throw on no rows or DBNull, matching ExecuteScalar expectations without
the strict-single check row shapes get, since scalar queries are
typically aggregates; nullable scalars return null in both cases.
Scalar lists fall out of the same resolution, including nullable
elements.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Task<int> cannot mean both a scalar SELECT and rows affected, so
non-query statements get their own attribute instead of overloading
[SqlQuery]. The name avoids [SqlCommand], which would hit CS1614
ambiguity against Microsoft.Data.SqlClient.SqlCommand in consumer code.
[SqlExecute] methods return Task<int> (rows affected) or Task
(discarded), sharing the parser, parameter binding, and transaction
plumbing; SQLB009 rejects other return types and SQLB010 rejects
carrying both attributes, reported exactly once even though both
pipelines see the method. Usage diagnostics now name the offending
attribute in their messages.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
IAsyncEnumerable<T> methods emit as async iterators that yield each row
(or first-column scalar) as it is read, so consumers can process large
result sets without buffering; command and reader disposal ride the
iterator finally blocks. The generated implementation part decorates
the CancellationToken parameter with [EnumeratorCancellation] so
WithCancellation flows the token; the compiler merges the attribute
across partial parts, which the warning-free compile tests confirm.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Row types without a parameterized constructor can now map columns
through public settable (set/init) properties, including inherited
ones, on a parameterless-constructible type, emitted as an object
initializer. A single parameterized public constructor still takes
precedence, so positional records keep constructor mapping. All
settable properties must be supported column types: silently skipping
an unmappable property would hide bugs, so SQLB005 rejects it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Integration tests exercise the full M5 matrix against a real in-memory
SQLite database: strict single row (hit and miss), optional row miss,
scalar count, scalar list, await-foreach streaming, [SqlExecute] rows
affected, and property-mapped mutable rows including the NULL-to-null
path. The streaming method also validates in a real consumer-shaped
build (TreatWarningsAsErrors) that [EnumeratorCancellation] emitted on
the implementation part merges cleanly across partial declarations.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@lgamorim lgamorim added this to the M5 — Query shapes milestone Jul 11, 2026
@lgamorim lgamorim merged commit adf00f6 into master Jul 11, 2026
2 checks passed
@lgamorim lgamorim deleted the feature/M5-query-shapes branch July 11, 2026 16:50
@lgamorim lgamorim modified the milestones: M5 — Query shapes, Phase 2 — Codegen (0.2.x) Jul 11, 2026
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.

1 participant