Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 44 additions & 7 deletions notes/parity.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,31 @@ Status legend: ✅ supported (verified) · ⚠️ partial/constrained · ❌ not
🚫 deliberate non-goal (decision recorded) · ❓ unverified. See [README.md](README.md) for
where the evidence comes from.

## Where the API-surface statuses now come from

**`test/Dapper.AOT.Test/ApiSurface.expected.txt` is the source of truth for "what does
Dapper.AOT do with this overload?"** It is generated by `ApiSurfaceCoverageTests`, which
classifies every public `SqlMapper` extension method using the same code the generator uses, and
is compared on every test run — so an overload added upstream fails the build rather than
quietly aging this file. Do not restate it here; cite it.

What it deliberately does **not** answer, so these stay judgement calls or corpus work:

- **behaviour.** "Generated" is not "matches Dapper" — round 5 found 84 tests that compiled
clean and failed at runtime. Only the Dapper suite settles this, and most remaining ❓ rows
below are of this kind;
- **per-call-site refusals.** A supported overload can still be refused for reasons belonging to
the call-site (unconstructable result, generic-by-containment, …). Those are diagnosed, and
the interceptor fixtures cover them;
- **non-extension statics.** `Format`, `PackListParameters`, `LookupDbType`, `HasTypeHandler`,
`TypeHandlerCache<T>` and friends are called directly rather than intercepted, so the
generator has no opinion on them. The question there is "is this AOT-safe when called?", which
no instrument answers today.

So ❓ below now means **"needs a corpus run to settle"** unless the row says otherwise; the few
rows nothing measures at all are marked *uninstrumented* in place. No ❓ remains for a question
the surface report already answers — where one did, the row now cites it.

**Impact** = usefulness toward the goal (corpus density + real-world usage), zero/low/med/high.
Zero means the *concept does not exist* under AOT — e.g. pruning the ref-emit plan cache is a
zero on any scale, because there is no ref-emit plan cache in AOT. **Complexity** = initial
Expand All @@ -26,12 +51,23 @@ Two levers change several complexity scores and are worth naming up front:

## The delta, in one place (2026-08-26)

Measured against the acceptance corpus: **677 of 793** Dapper tests pass through generated code,
with **533 of 725** call-sites intercepted (73.5%). What stands between that and "all green",
largest first:
Two independent measurements, because they answer different questions.

**API surface** (`ApiSurface.expected.txt`, generated): of Dapper's 110 public extension
overloads — 40 candidates, 16 unsupported-and-diagnosed, 13 unsupported-and-undiagnosed, 27
skipped silently, 9 never inspected (helpers, correctly). So **40 of 110 tell the consumer
nothing**, almost all `CommandDefinition`-shaped. That is a defect class of its own, separate
from any missing feature: it is not that these fail, it is that they fail *quietly*.

**Behaviour** (Dapper suite, local SQL Server): **677 of 793** pass through generated code, with
**533 of 725** call-sites intercepted (73.5%). Note the denominator counts what the generator
examines, so the silently-skipped overloads above are inside it and the diagnosed ones are too.

What stands between that and "all green", largest first:

| # | what | where it shows up | size |
| --- | --- | --- | --- |
| 0 | **say something at the 40 mute overloads** | 27 skipped silently + 13 unsupported-undiagnosed | small, and it is the cheapest safety win on the list: it turns a runtime AOT failure into a build warning without supporting anything new |
| 1 | **multi-map** (`Query<T1..T7,TReturn>` + `splitOn`) | unsupported API - outside the 725 | large |
| 2 | **`QueryMultiple` / `GridReader`** | unsupported API | large; needs a Dapper-side extension point first |
| 3 | **corpus adoption of `[TypeHandler]`** | TypeHandlerTests x16/provider | a harness edit, not product work - but not all of it converts, see below |
Expand Down Expand Up @@ -68,15 +104,15 @@ non-public members, and the "has no meaning" APIs warning - all in §7.
| `ExecuteReader` / `ExecuteReaderAsync` | ❌ | med | low-med | command setup already generated; return the (wrapped) reader; `WrappedReader`/`IWrappedDataReader` disposal semantics |
| `GetRowParser<T>(reader)` | ✅ | — | — | |
| `GetRowParser(reader, Type concreteType, ...)` | ❌ | med | low* | discriminator/polymorphism pattern; dictionary lookup once types are announced |
| `Parse<T>` / `Parse(Type)` / `Parse` (dynamic) | ❌ | low | low | same reader machinery, different entry point |
| `Parse<T>` / `Parse(Type)` / `Parse` (dynamic) | ❌ | low | low | same reader machinery, different entry point. Report: *not inspected* — all three sit outside the generator's name filter, so nothing is emitted and nothing is said |
| `AsTableValuedParameter` (`DataTable` / `SqlDataRecord`) | ⚠️ | low | low | the result *is* an `ICustomQueryParameter`, so covered above. A **bare** `DataTable` member needs a handler declared for `DataTable`; vanilla registers one by default, so this is the same "do we ship built-in declarations" question as the XML row |
| `AsList<T>` | n/a | — | — | trivial helper; confirm it doesn't count as a candidate site |
| `GetTypeDeserializer(Type, reader, startBound, length, ...)` | ❌ | low-med | low* | a valid raw-materializer API, not mere plumbing: with announced types it's the same dispatch map, returning a boxed `Func<DbDataReader, object>`. Its generic strengthening **already exists**: `GetRowParser<T>` (same slicing knobs), which AOT supports |
| `CreateParamInfoGenerator(Identity, ...)` | ❌ | low | med | the raw parameter-binder factory; **no generic counterpart exists in Dapper** — see "Strengthened APIs" in [type-vs-generic.md](type-vs-generic.md) for the proposed `<T>` form |
| `ReadChar` / `ReadNullableChar` / `SanitizeParameterValue` | ✅ | — | — | plain static helpers, AOT-safe as-is; nothing to intercept |
| `PurgeQueryCache` / `GetCachedSQL*` / `GetHashCollissions` / `QueryCachePurged` | 🚫 | **zero** | — | there is no ref-emit plan cache in AOT — but usage should *warn*, see §7 |
| `Format` / `ReplaceLiterals` | | low | low | falls out of the literal-injection work (see [tokens.md](tokens.md)) |
| public infrastructure statics: `PackListParameters`, `FindOrAddParameter`, `LookupDbType`, `HasTypeHandler`, `GetTypeName`/`SetTypeName`, `SetDbType`, `TypeHandlerCache<T>.Parse/SetValue`, `ThrowDataException`, `ThrowNullCustomQueryParameter` | ❓ | low | low | in scope because they are public (Contrib-style extenders call them), even though they exist to serve Dapper's generated IL. Mostly plain AOT-safe statics; the `Type`-keyed ones (`LookupDbType`, `TypeHandlerCache`) fold into announced types / the type-handler story |
| `Format` / `ReplaceLiterals` | | low | low | falls out of the literal-injection work (see [tokens.md](tokens.md)). Report: `ReplaceLiterals` is *not inspected*; `Format` is not an extension method, so it is called directly and the generator never sees it |
| public infrastructure statics: `PackListParameters`, `FindOrAddParameter`, `LookupDbType`, `HasTypeHandler`, `GetTypeName`/`SetTypeName`, `SetDbType`, `TypeHandlerCache<T>.Parse/SetValue`, `ThrowDataException`, `ThrowNullCustomQueryParameter` | ❓ | low | low | in scope because they are public (Contrib-style extenders call them), even though they exist to serve Dapper's generated IL. Mostly plain AOT-safe statics; the `Type`-keyed ones (`LookupDbType`, `TypeHandlerCache`) fold into announced types / the type-handler story. **Uninstrumented on purpose**: these are not extension methods, so they are called directly rather than intercepted and the surface report has no opinion on them; the open question is whether each is AOT-safe *when called*, and nothing measures that |

## 2. Parameters (input side)

Expand All @@ -97,7 +133,8 @@ non-public members, and the "has no meaning" APIs warning - all in §7.
| param filtering (only bind members named in SQL) + `SupportLegacyParameterTokens` | ❓ | med | low | AOT currently *includes* + warns (DAP236); on strict providers that's an error, so may need parity not preference |
| UDTs (`UdtTypeHandler`, geo types) | ⚠️ | low | low | provider-specific, and now expressible: declare a handler for the type. No built-in, so a consumer supplies it |
| XML types (`XmlDocument`/`XDocument`/`XElement`) | ⚠️ | low-med | low | expressible today by declaring a handler; vanilla registers these by default, so the open question is whether we ship built-in declarations rather than whether it *can* work |
| `CommandDefinition` incl. `CommandFlags.Pipelined` | ❓ | med | low-med | `NoCache` is **zero** (no cache to bypass); `Buffered` covered; `Pipelined` is a perf feature to verify |
| `CommandDefinition` overloads | ❌ | **high** | med | **27 overloads, every one skipped silently** — the analyzer only inspects call-sites carrying SQL as a string argument, and these hide it inside the struct, so nothing is emitted *and nothing is reported*. Consumers get vanilla Dapper under JIT and a runtime failure under native AOT with no build-time signal (issues #112, #158, #165). External PR #153 proposes support; a diagnostic is worth having either way, and is cheaper |
| `CommandFlags` (`Buffered`, `Pipelined`, `NoCache`) | ❓ (behaviour) | med | low-med | `NoCache` is **zero** (no cache to bypass); `Buffered` covered; `Pipelined` is a perf feature to verify — and all of it is moot at a call-site until the row above is fixed |
| `commandTimeout` / `transaction` / `commandType` args | ✅ ❓ | — | — | verify `TableDirect` |
| `CancellationToken` | ✅ | — | — | AOT extends Dapper here (DAP044/045) |

Expand Down
Loading