Skip to content

Commit b10eb0b

Browse files
committed
SqlBulkCopy works end-to-end over the TDS endpoint for SqlClient 6.x and 7.x: the metadata pre-batch parses via four general engine fixes (SET FMTONLY as session state returning schema-only zero-row results, leading-empty-segment names like ..proc and .[sys].[all_columns], sp_tablecollations_100 as a modeled system proc, bare SELECT TOP n *), INSERT BULK and the BulkLoadBCP packet decode client COLMETADATA + rows into the normal storage path (FIXEDLENTYPE tokens raw, decimals as sign + fixed 16-byte mantissa whose length byte only flags NULL), and probed bulk semantics land exactly: CHECK/FK skipped-and-untrusted by default vs enforced-and-trusted with CHECK_CONSTRAINTS, PK/UNIQUE/NOT NULL always enforced, triggers gated on FIRE_TRIGGERS, KeepIdentity/KeepNulls defaults, computed/rowversion server-generated, external-transaction rollback undoing the load.
1 parent bea9a41 commit b10eb0b

19 files changed

Lines changed: 1594 additions & 15 deletions

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ Per-feature deep-dives live under `docs/claude/`. Each entry below is a trigger:
164164
- **New top-level statement parser or dispatch-loop separator rules, double-quoted identifiers / QUOTED_IDENTIFIER**[`grammar.md`](docs/claude/grammar.md) + [`control-flow.md`](docs/claude/control-flow.md).
165165
- **BACPAC import** (`Simulation.ImportBacpac` — multi-database via repeated calls, `BacpacImportOptions`, `ModelXmlReader` dispatcher, BCP wire format, `BacpacBuilder` test harness) → [`bacpac-loader.md`](docs/claude/bacpac-loader.md).
166166
- **Linked servers** (`Simulation.AddRemoteSimulation`, `sp_addlinkedserver` / `sp_dropserver`, four-part FROM routing through the remote's ADO.NET pipeline, `OPENQUERY(server,'query')` ad-hoc pass-through + compile-time schema discovery, `sys.servers`) → [`linked-servers.md`](docs/claude/linked-servers.md).
167-
- **TDS network endpoint** (`Simulation.ListenAsync``SimulatedNetworkListener`; real SqlClient over loopback TCP+TLS; SQLBatch/RPC/TM, no bulk; `sp_cursor*` API-server-cursor RPC family; credential enforcement via the `CREATE LOGIN` registry, Msg 18456 on mismatch; EF via plain `UseSqlServer`; oracles = `*.Tests.SqlClient` + `*.Tests.Smo`, the real-SMO consumer oracle) → [`tds-endpoint.md`](docs/claude/tds-endpoint.md).
167+
- **TDS network endpoint** (`Simulation.ListenAsync``SimulatedNetworkListener`; real SqlClient over loopback TCP+TLS; SQLBatch/RPC/TM/BulkLoad (`SqlBulkCopy`); `sp_cursor*` API-server-cursor RPC family; credential enforcement via the `CREATE LOGIN` registry, Msg 18456 on mismatch; EF via plain `UseSqlServer`; oracles = `*.Tests.SqlClient` + `*.Tests.Smo`, the real-SMO consumer oracle) → [`tds-endpoint.md`](docs/claude/tds-endpoint.md).
168168

169169
## Not modeled
170170

SqlServerSimulator.Tests.SqlClient/BulkCopyTests.cs

Lines changed: 451 additions & 0 deletions
Large diffs are not rendered by default.

SqlServerSimulator.Tests.SqlClient/Wire.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,10 @@ public static void ExecInProc(Simulation simulation, string sql)
3737
}
3838

3939
/// <summary>
40-
/// Inserts through the in-process surface with one bound parameter. The wire
41-
/// listener does not yet accept RPC / parameterized commands, so large or
42-
/// binary payloads seed via ADO here, then read back over the wire.
40+
/// Inserts through the in-process surface with one bound parameter — a
41+
/// convenience for seeding large or binary payloads in-proc before reading
42+
/// them back over the wire (the wire path accepts RPC / parameterized
43+
/// commands too; this just keeps seeding terse).
4344
/// </summary>
4445
public static void ExecInProcParam(Simulation simulation, string sql, string parameterName, object value)
4546
{

SqlServerSimulator/BuiltInResources.LegacyCompat.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ internal static partial class BuiltInResources
5050
"xp_msver",
5151
"xp_qv",
5252
"xp_instance_regread",
53+
// sp_tablecollations_100: SqlClient's SqlBulkCopy metadata batch calls
54+
// it (`exec ..sp_tablecollations_100 N'[schema].[table]'`) to read the
55+
// destination columns' TDS collation structures before streaming rows.
56+
"sp_tablecollations_100",
5357
];
5458

5559
/// <summary>

SqlServerSimulator/Network/CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
Auto-loaded when working in this directory. The behavior deep-dive is [`docs/claude/tds-endpoint.md`](../../docs/claude/tds-endpoint.md) — protocol scope, response shapes, divergences, deferred items. These notes are the local implementation contracts.
44

5-
- **Session layout**: `TdsSession` is a partial — main file (prelogin/TLS/login/batch loop + `StreamOutcomesAsync`), `.Rpc.cs` (RPC dispatch + prepared-handle map + `WellKnownProcId` name/id map), `.Cursors.cs` (the `sp_cursor*` API-server-cursor family), `.TransactionManager.cs` (TM requests + the session `transaction` field).
5+
- **Session layout**: `TdsSession` is a partial — main file (prelogin/TLS/login/batch loop + `StreamOutcomesAsync`), `.Rpc.cs` (RPC dispatch + prepared-handle map + `WellKnownProcId` name/id map), `.Cursors.cs` (the `sp_cursor*` API-server-cursor family), `.TransactionManager.cs` (TM requests + the session `transaction` field), `.BulkLoad.cs` (the `SqlBulkCopy` handshake: `INSERT BULK` detect/parse-ack + BulkLoadBCP packet decode via `TdsBulkLoadReader`, feeding `Simulation.ExecuteBulkInsert`).
66
- **API server cursors** (`.Cursors.cs`): `sp_cursoropen/fetch/close/prepexec/execute/prepare/unprepare/option` + `sp_cursor` positioned DML. Each handle in the per-session `apiCursors` map wraps an engine `Cursor` **registered under an opaque `sss_apicursor_<n>` name in `connection.Cursors`** (so `WHERE CURRENT OF` resolves it) — built by synthesizing `DECLARE … CURSOR … FOR <stmt>; OPEN` and reusing the engine's sensitivity resolution. Fetch drives `Cursor.Fetch` directly per row over a throwaway `BatchContext` (its command needs a non-empty `CommandText` — `" "` — or `ParserContext` throws); positioned DML sets `Cursor.CurrentRid` to a buffered RID then runs synthesized `UPDATE/DELETE … WHERE CURRENT OF`. **Prepared/parameterized cursors freeze their bound params on the ApiCursor and re-apply them to every fetch batch** — a keyset/dynamic cursor re-runs its SELECT per fetch, so the fresh batch would otherwise miss the variables. Result sets append a trailing `ROWSTAT` int column. Concurrency options (SCROLL_LOCKS/OPTIMISTIC) are echoed but not wired (probe-confirmed API-cursor optimistic conflicts don't surface). Contract + probed constants in [`docs/claude/tds-endpoint.md`](../../docs/claude/tds-endpoint.md). One task per socket; teardown is socket-close-driven; the catch list in `RunAsync` is the session's crash boundary — an exception type not listed there kills the session silently (client sees a transport error), so wire-format parse failures must throw `InvalidDataException`, unsupported features `NotSupportedException` (converted to ERROR tokens in handlers, never leaked).
77
- **Validate before emitting**: any per-column rejection (`TdsTypeCodec.ValidateSchema`) must fire *before* COLMETADATA bytes are written — a throw mid-token desyncs the stream, which surfaces client-side as a corrupted-frame/transport error, not a useful message.
88
- **Token writer contract**: `TdsTokenWriter` buffers synchronously; `FlushAsync(final: false)` after every row keeps memory bounded to max(row, packet). Only the final flush sets EOM. Tokens may legally split across packet boundaries.

0 commit comments

Comments
 (0)