Skip to content

M4: row materialization (incremental source generator)#4

Merged
lgamorim merged 7 commits into
masterfrom
feature/M4-row-materialization
Jul 11, 2026
Merged

M4: row materialization (incremental source generator)#4
lgamorim merged 7 commits into
masterfrom
feature/M4-row-materialization

Conversation

@lgamorim

Copy link
Copy Markdown
Owner

Summary

Opens Phase 2 (Codegen) at 0.2.0-preview.1 and delivers M4: an incremental source generator that implements [SqlQuery]-annotated static partial methods with straight-line, reflection-free DbDataReader materialization.

  • ADR 0002 accepted — signature-driven codegen: the generator emits purely from the attribute and the declared method signature, never from .sqlbound/ snapshots. Forced by the roadmap (codegen precedes verification) and consistent with ADR 0001''s stance that missing snapshots never break the build; snapshot-informed optimization stays a possible future amendment.
  • SqlQueryAttribute lives in the runtime package so user code never references the generator assembly.
  • SqlBound.Generators (netstandard2.0, Roslyn 4.8) packs into analyzers/dotnet/cs with no lib/ folder and no runtime dependency. Tests drive CSharpGeneratorDriver directly (the Microsoft.CodeAnalysis.Testing verifiers lag on xunit.v3).
  • Pipeline: ForAttributeWithMetadataName with a fully value-equatable model for incremental caching; usage errors SQLB001SQLB008 (non-partial/non-static methods, missing DbConnection, unsupported return/row/parameter types, empty SQL, generics) plus analyzer release tracking.
  • Emission: ordinals resolved once via GetOrdinal, typed getters per row, DBNullnull for nullable members and a descriptive exception naming the column otherwise, @name scalar binding (coalescing to DBNull.Value only when the argument can be null), optional DbTransaction, CancellationToken.None fallback, deterministic FNV-1a hint names. Canonical output locked byte-for-byte in a golden test; all variants proven to compile warning-free.
  • End-to-end proof: the integration project consumes the generator as an analyzer-only reference (the exact shape NuGet consumers get) and runs a generated query against real in-memory SQLite — including the NULL→decimal? path — while reading Dapper''s writes on a shared connection, extending the M3 coexistence proof to the static surface.

M4 scope is one canonical shape (Task<IReadOnlyList<T>> over positional records); the remaining shapes land in M5, AOT + benchmarks in M6.

Test plan

  • 59 tests green in Release (35 unit, 19 generator snapshot/compile tests, 5 SQLite integration).
  • dotnet format --verify-no-changes, zero-warning build, and dotnet pack verified locally (package layout inspected: analyzer DLL only).

🤖 Generated with Claude Code

lgamorim and others added 7 commits July 11, 2026 11:56
Phase 2 (Codegen) gets its own minor version line per the semver
convention: each phase owns a minor, carrying a prerelease suffix while
the phase is in active development. The suffix drops to a clean 0.2.0
in the commit that closes the phase and gets tagged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The generator emits materialization code purely from the [SqlQuery]
attribute and the declared partial method signature, never from
.sqlbound/ snapshots. The roadmap forces this (codegen precedes
verification, so no snapshot infrastructure exists when M4 is built),
and it preserves ADR 0001''s DX stance that missing or stale snapshots
degrade verification but never break the build. Snapshot-informed
optimization remains a possible future amendment, not a commitment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
[SqlQuery] marks the static partial methods whose implementations the
source generator (M4) emits. It lives in the runtime package, not the
generator package, so user code compiles against SqlBound alone and the
generator remains a pure build-time dependency.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The generator targets netstandard2.0 (Roslyn requirement) and packs
into analyzers/dotnet/cs with no lib/ folder and no symbol package, so
it can never become a runtime dependency of a consumer. Tests drive
CSharpGeneratorDriver directly rather than the Microsoft.CodeAnalysis
.Testing verifiers, which lag on xunit.v3 support; the harness exposes
generated sources, generator diagnostics, and post-generation
compilation diagnostics for snapshot assertions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ForAttributeWithMetadataName drives a transform that parses each
[SqlQuery] method into a fully value-equatable model (EquatableArray,
DiagnosticInfo instead of Diagnostic) so incremental caching works.
Invalid usage reports SQLB001-008 as errors: non-partial or already
implemented methods, non-static methods, missing DbConnection first
parameter, unsupported return/row/parameter types, empty command text,
and generic declarations. A valid method yields the emission model;
code emission follows in the next commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The generated implementation resolves column ordinals once by
constructor-parameter name before the read loop, then materializes each
row with typed getter calls: nullable members map DBNull to null,
non-nullable members throw a descriptive InvalidOperationException
naming the column. Scalars bind as @name parameters (coalescing to
DBNull only when the argument can be null), an optional DbTransaction
is honored, and a missing CancellationToken parameter falls back to
CancellationToken.None. Hint names carry an FNV-1a signature hash so
output stays deterministic across processes, unlike string.GetHashCode.
Snapshot tests lock the canonical output byte for byte and prove every
variant compiles warning-free in a nullable-enabled compilation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The integration test project consumes SqlBound.Generators as an
analyzer-only project reference (the same shape a NuGet consumer gets
from analyzers/dotnet/cs) and executes a generated [SqlQuery] method
against a real in-memory SQLite database: rows materialize including
the DBNull-to-nullable path, and the generated method reads writes made
by Dapper on the same shared connection, extending the M3 coexistence
proof to the static query surface.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@lgamorim lgamorim added this to the M4 — Row materialization milestone Jul 11, 2026
@lgamorim lgamorim merged commit 5f738da into master Jul 11, 2026
2 checks passed
@lgamorim lgamorim deleted the feature/M4-row-materialization branch July 11, 2026 13:55
@lgamorim lgamorim modified the milestones: M4 — Row materialization, 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