You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Output-direction sql_variant and CLR-UDT RPC parameters write back over TDS: end-of-batch write-back stamps the engine-typed SqlValue on an internal SimulatedDbParameter side-channel and the RETURNVALUE writer reuses the column TYPE_INFO/value writers.
Copy file name to clipboardExpand all lines: docs/claude/backlog.md
+2-1Lines changed: 2 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -42,7 +42,8 @@ Remaining phases, roughly in value order:
42
42
**`text`/`ntext`/`image` ship fully** (2026-07-19) — result columns, `image` (0x22) input parameters, and **`SqlBulkCopy` into a legacy-LOB column** (2026-07-19: the BCP LONGLEN COLMETADATA + in-band text-pointer ROW value now decode); see [`tds-endpoint.md`](tds-endpoint.md#legacy-text--ntext--image-wire-forms).
43
43
**`sql_variant` and UDT (`geography`/`geometry`/`hierarchyid`) ship as RPC *input* parameters AND as *columns inside a TVP*** (2026-07-19 — the shared decoder made the TVP columns near-free; see [`tds-endpoint.md`](tds-endpoint.md#tvp-parameters-sqldbtypestructured)).
44
44
**TVP parameters (0xF3, `SqlDbType.Structured`) ship** — `DataTable` / `IEnumerable<SqlDataRecord>` / `DbDataReader` sources over proc-RPC + sp_executesql, error parity for arity / type / NOT NULL / CHECK / PK / UNIQUE / unknown-type.
45
-
Residuals: **output-direction** UDT / `sql_variant` params; **`SET TEXTSIZE`** doesn't truncate returned LOB data (parse-and-discard).
45
+
Residuals: **`SET TEXTSIZE`** doesn't truncate returned LOB data (parse-and-discard).
46
+
(Output-direction UDT / `sql_variant` params shipped 2026-07-19 — probed RETURNVALUE wire forms in [`tds-endpoint.md`](tds-endpoint.md#clr-udt--sql_variant-parameters).)
46
47
(The LOB-backed TVP column → stored-proc READONLY parameter dangling-pointer bug was fixed 2026-07-19: the proc-parameter copy re-homes off-row values into the parameter's heap; see [`table-valued-parameters.md`](table-valued-parameters.md).)
47
48
-**Smaller fidelity items**: TDS 8.0 / `Encrypt=Strict` (ALPN via `SslApplicationProtocol`); user-supplied `X509Certificate2` — an add-on-demand public-surface expansion.
48
49
(Off-loopback binding shipped 2026-07-19 as `ListenNetworkAsync`, gated on a registered login; per-interface bind-address selection remains add-on-demand.)
Copy file name to clipboardExpand all lines: docs/claude/tds-endpoint.md
+7-5Lines changed: 7 additions & 5 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -276,10 +276,13 @@ Probed against SQL Server 2025 + SqlClient 7.0.2 (2026-07-19).
276
276
-**`sql_variant` (`0x62`)** — TYPE_INFO is a 4-byte max length (ignored); the value is a 4-byte total length (**0 = NULL**) then the MS-TDS §2.2.5.5.3 body (base-type token + property-byte count + property bytes + inner value), the read mirror of `TdsTypeCodec.BuildVariantBody`, decoded into the inner `SqlValue` and wrapped by `SqlValue.FromVariant` so `SQL_VARIANT_PROPERTY(@p,'BaseType')` reports the sent type.
277
277
SqlClient's per-CLR-type base-type choices (probe-confirmed): `int`/`bigint`/`smallint`/`tinyint`/`bit` direct; `decimal` → `numeric(38, 2)` (base token `0x6C`); `float`/`real` direct; **both `string` and `SqlString` → `nvarchar`** (varchar is not sent as a variant); `DateTime` → `datetime`; `Guid` → `uniqueidentifier`; `SqlMoney` → `money`; `byte[]` → `varbinary`; `TimeSpan` → `time(7)`.
278
278
(SqlClient 7.0.2 mis-serializes a `DateOnly` variant — the stream truncates client-side before the server sees it; not a server concern.)
279
-
-**Residuals**: **output-direction** UDT / `sql_variant` parameters (real supports both) are unmodeled — the RETURNVALUE writeback for these types is rejected up front (`TdsTypeCodec.WriteReturnValue`, keyed off the `DbType.Object` marker) with a clear ERROR rather than a malformed RETURNVALUE token that desyncs the client.
280
-
(**TVP columns** of UDT / `sql_variant` type now decode — the RPC decoders and the column decoder share `TdsWireValue`; see [TVP parameters](#tvp-parameters-sqldbtypestructured).)
279
+
-**Output direction ships** (probe-captured RETURNVALUE shapes, SQL Server 2025 + SqlClient 7.0.2, 2026-07-19): a `sql_variant` output parameter's RETURNVALUE TYPE_INFO is `0x62` + ULONG max length (8009) with the same 4-byte-total-length + self-describing-body value a variant result column carries; a CLR-UDT output parameter's TYPE_INFO is the **COLMETADATA-shaped** UDT_INFO (USHORT max byte size — 892 for `hierarchyid`, `0xFFFF` for spatial — then db / schema / type B_VARCHARs and the US_VARCHAR assembly-qualified name; richer than the three-B_VARCHAR client request form) with a PLP value, PLP NULL for NULL.
280
+
Real fills the db segment with the current database; the simulator reuses the column writers verbatim (empty db segment — SqlClient reads both).
281
+
The engine value reaches the writer via `SimulatedDbParameter.OutputSqlValue`, an internal side-channel stamped at end-of-batch write-back alongside the CLR `Value` conversion, because the CLR object alone no longer carries the variant inner type / UDT kind; `TdsSession.Rpc.WriteOutputReturnValues` routes `DbType.Object`-marked outputs through the `SqlValue` overload of `TdsTypeCodec.WriteReturnValue`.
282
+
**Variant decimal bodies are always 1 sign byte + a 16-byte magnitude** regardless of precision (probe-captured in both a result column and a RETURNVALUE): SqlClient's row-value reader tolerates a narrower magnitude but its RETURNVALUE reader reads the fixed 17 data bytes and desyncs on anything shorter — `BuildVariantDecimal` writes the fixed form.
283
+
(**TVP columns** of UDT / `sql_variant` type also decode — the RPC decoders and the column decoder share `TdsWireValue`; see [TVP parameters](#tvp-parameters-sqldbtypestructured).)
281
284
A `sql_variant` carrying an inner value implicitly converted to a narrower sink (e.g. `insert … (int_col) values (@variant_int)`) is accepted by the simulator's engine-level variant coercion where **real raises Msg 257** — a pre-existing in-process `sql_variant` divergence surfaced (not introduced) by wire parameters.
282
-
Oracles: `UdtRpcParameterTests`, `SqlVariantRpcParameterTests` (Tests.SqlClient), using the real `Microsoft.SqlServer.Types` package for typed `SqlGeography` / `SqlHierarchyId` construction (works headless on Linux; `SqlGeometry.STGeomFromText` does **not** — geometry is built from raw WKB bytes).
285
+
Oracles: `UdtRpcParameterTests`, `SqlVariantRpcParameterTests`, `SqlVariantWireTests` (Tests.SqlClient), using the real `Microsoft.SqlServer.Types` package for typed `SqlGeography` / `SqlHierarchyId` construction (works headless on Linux; `SqlGeometry.STGeomFromText` does **not** — geometry is built from raw WKB bytes).
283
286
284
287
## Transaction Manager requests
285
288
@@ -385,8 +388,7 @@ A real build number is load-bearing for SSMS's per-build client feature gates (A
385
388
- Login INFO states are approximations.
386
389
- MARS ships (see [MARS](#mars-multiple-active-result-sets) below); no TDS 8.0 / `Encrypt=Strict`, no plaintext sessions, no integrated auth (an SSPI/FedAuth login presents an empty SQL username, which under a non-empty registry fails as Msg 18456 rather than negotiating).
387
390
- Credential-enforcement edges not modeled: `ALTER LOGIN … DISABLE` parses but doesn't block login; password policy (`CHECK_POLICY` / expiration / lockout) never enforced; no login auditing.
Every input-parameter TYPE_INFO is accepted — TVP / UDT / `sql_variant` / `text` / `ntext` / `image` — and every client value-stream column type (bulk / TVP) now decodes through the shared `TdsWireValue` / `TdsColumnDecoder`, including `sql_variant` / CLR-UDT columns and legacy-LOB bulk destinations.
391
+
- RPC parameters are gap-free in both directions: every input TYPE_INFO is accepted — TVP / UDT / `sql_variant` / `text` / `ntext` / `image` — every client value-stream column type (bulk / TVP) decodes through the shared `TdsWireValue` / `TdsColumnDecoder`, and output-direction UDT / `sql_variant` parameters write back as RETURNVALUE tokens (see [CLR-UDT / sql_variant parameters](#clr-udt--sql_variant-parameters)).
390
392
Non-cursor well-known ProcIDs beyond the sp_execute/sp_prepare family are rejected with ERROR 50000 naming the id.
391
393
- Mid-stream attention (cancel) ships — see below.
392
394
Residual: cancel reaction is bounded by the current statement's *materialization*, not just its streaming, because a statement's rows are materialized in one synchronous step before they stream (a cancel mid-way through a compute-heavy `SELECT`/sort waits for that materialization to finish, then discards the result at the outcome boundary; real streams-as-it-computes and stops sooner).
Copy file name to clipboardExpand all lines: src/SqlServerSimulator/Network/CLAUDE.md
+3-2Lines changed: 3 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -58,7 +58,8 @@ These notes are the local implementation contracts.
58
58
COLMETADATA = type byte + 4-byte max length (8009).
59
59
Each value = 4-byte total length then the MS-TDS 2.2.5.5.3 body (base-type token + cbProps + props + data).
60
60
**NULL is total-length 0, not 0xFFFFFFFF** — a non-NULL variant is always ≥2 bytes, so SqlClient reads 0 as NULL (the one non-obvious wire rule; got this wrong first and it desynced the stream).
61
-
`sql_variant`**input***parameters* decode (RPC read side, `TdsRpc.DecodeSqlVariant` → `TdsWireValue.ReadVariantBody` — the read mirror of `BuildVariantBody`, into `SqlValue.FromVariant`; **output** direction stays rejected), and **`sql_variant` columns inside a TVP** decode through the same body reader (`TdsColumnDecoder`: 0x62 + 4-byte max length, value = 4-byte total length `0`=NULL + body).
61
+
`sql_variant`**input***parameters* decode (RPC read side, `TdsRpc.DecodeSqlVariant` → `TdsWireValue.ReadVariantBody` — the read mirror of `BuildVariantBody`, into `SqlValue.FromVariant`), **output** direction writes back as a RETURNVALUE (`0x62` + ULONG maxlen 8009 + the column-form value; the engine value rides `SimulatedDbParameter.OutputSqlValue`, stamped at write-back, since the CLR `Value` loses the inner type), and **`sql_variant` columns inside a TVP** decode through the same body reader (`TdsColumnDecoder`: 0x62 + 4-byte max length, value = 4-byte total length `0`=NULL + body).
62
+
Variant decimal bodies are always sign + 16-byte magnitude regardless of precision (probed in column and RETURNVALUE positions; SqlClient's RETURNVALUE reader hard-reads 17 data bytes).
UDT **input***parameters* decode (`TdsRpc.DecodeClrUdt` → `TdsWireValue.BuildUdtValue`): the client UDT_INFO is three B_VARCHARs (db/schema/type, no max-size, no AQN — shorter than COLMETADATA) + PLP value; hierarchyid OrdPath bytes bind verbatim, spatial WKB decodes via `SpatialWkbDecoder.TryDecode`, resolved case-insensitively into a pre-built `SqlValue` (unknown type → Msg 8064, invalid spatial bytes → Msg 8023).
71
72
**UDT columns inside a TVP** decode through the same builder (`TdsColumnDecoder`: 0xF0 + three B_VARCHARs + PLP value).
72
-
**Output** direction stays rejected.
73
+
**Output** direction writes back as a RETURNVALUE carrying the **COLMETADATA-shaped** UDT_INFO (max byte size + db/schema/type + AQN — richer than the client request form) + PLP value, from `SimulatedDbParameter.OutputSqlValue`.
73
74
A LOB-backed UDT TVP column (`geography`/`geometry`) round-trips via both the `sp_executesql` text path and a stored-proc READONLY parameter (the proc-parameter copy re-homes off-row values — docs/claude/table-valued-parameters.md).
-**Length-prefix trap**: most strings are char-counted (B_VARCHAR / US_VARCHAR), but TM-request transaction names are byte-counted — misreading them as char-counted overruns the payload on every `SqlTransaction.Save`.
Copy file name to clipboardExpand all lines: src/SqlServerSimulator/SimulatedDbParameter.cs
+11Lines changed: 11 additions & 0 deletions
Original file line number
Diff line number
Diff line change
@@ -1,6 +1,7 @@
1
1
usingSystem.Data;
2
2
usingSystem.Data.Common;
3
3
usingSystem.Diagnostics.CodeAnalysis;
4
+
usingSqlServerSimulator.Storage;
4
5
5
6
[module:SuppressMessage("Microsoft.Design","CA1065:DoNotRaiseExceptionsInUnexpectedLocations",Scope="member",Target="~P:SqlServerSimulator.SimulatedDbParameter.DbType",Justification="Mirrors SqlParameter inference: a CLR Value with no DbType mapping is surfaced as ArgumentException at property read, not later at command execution.")]
6
7
@@ -37,6 +38,16 @@ public sealed class SimulatedDbParameter : DbParameter
37
38
{
38
39
privateDbType?dbType;
39
40
41
+
/// <summary>
42
+
/// The typed engine value written back to an output-direction parameter
43
+
/// at end of batch, alongside the CLR conversion stored in
44
+
/// <see cref="Value"/>. The TDS listener's RETURNVALUE writer reads it
45
+
/// for <c>sql_variant</c> / CLR-UDT parameters, whose wire form needs
46
+
/// the inner type identity (variant base type, UDT kind) that the CLR
0 commit comments