Skip to content

Commit f11f6d8

Browse files
committed
TVP parameters work over the TDS wire: RPC TYPE_INFO 0xF3 decodes TVP_TYPENAME + client column metadata + row stream through new TdsValueReader/TdsColumnDecoder shared with the bulk-load reader, binds positionally for both proc RPC and sp_executesql text commands, and structured-parameter row insertion now enforces NOT NULL/CHECK/PK/UNIQUE (515/547/2627/2601) on the wire and in-process paths alike with IEnumerable<SqlDataRecord> sources working over the wire, identity-DBNull auto-generation retained as a documented divergence from real's Msg 1077, and the four built-in rowset dispatch blocks in FROM-source parsing (OPENJSON / STRING_SPLIT / GENERATE_SERIES / fn_listextendedproperty) collapsed into a shared BuiltInRowsetSource helper.
1 parent b10eb0b commit f11f6d8

18 files changed

Lines changed: 1290 additions & 618 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/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).
167+
- **TDS network endpoint** (`Simulation.ListenAsync``SimulatedNetworkListener`; real SqlClient over loopback TCP+TLS; SQLBatch/RPC/TM/BulkLoad (`SqlBulkCopy`); TVP (`0xF3` `SqlDbType.Structured`) RPC params via the shared `TdsColumnDecoder`; `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/RpcErrorTests.cs

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -32,35 +32,6 @@ public async Task ParameterizedDivideByZero_Number8134()
3232
AreEqual(8134, ex.Number);
3333
}
3434

35-
// A table-valued parameter (SqlDbType.Structured) is decoded to the 0xF3
36-
// wire token, which the RPC parser rejects. The unsupported-type error
37-
// surfaces as Msg 50000 naming the table-valued form. Asserts the observed
38-
// behavior; the message text documents the surface.
39-
[TestMethod]
40-
public async Task TableValuedParameter_SurfacesAsUnsupported()
41-
{
42-
var simulation = new Simulation();
43-
await using var listener = await simulation.ListenAsync(0, TestContext.CancellationToken);
44-
await using var connection = await Wire.OpenAsync(listener, TestContext.CancellationToken);
45-
46-
var table = new DataTable();
47-
_ = table.Columns.Add("id", typeof(int));
48-
_ = table.Rows.Add(1);
49-
_ = table.Rows.Add(2);
50-
51-
var ex = await Assert.ThrowsAsync<SqlException>(async () =>
52-
{
53-
await using var command = new SqlCommand("select count(*) from @t", connection);
54-
var parameter = command.Parameters.AddWithValue("@t", table);
55-
parameter.SqlDbType = SqlDbType.Structured;
56-
parameter.TypeName = "dbo.IntList";
57-
_ = await command.ExecuteScalarAsync(TestContext.CancellationToken);
58-
});
59-
60-
AreEqual(50000, ex.Number);
61-
Contains("table-valued", ex.Message);
62-
}
63-
6435
[TestMethod]
6536
public async Task OutputParameterBeforeErrorInBatch_ThrowsButWritesOutput()
6637
{

SqlServerSimulator.Tests.SqlClient/TvpRpcTests.cs

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

SqlServerSimulator.Tests/TableValuedParameterTests.cs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,78 @@ public void Structured_DataTable_IdentityValueSupplied_RaisesMsg1077()
417417
AreEqual("1077", ex.Data["HelpLink.EvtID"]);
418418
}
419419

420+
[TestMethod]
421+
public void Structured_DataTable_NullIntoNotNullColumn_RaisesMsg515()
422+
{
423+
var simulation = new Simulation();
424+
_ = simulation.ExecuteNonQuery("create type dbo.t1 as table (id int not null, v int)");
425+
426+
using var con = simulation.CreateDbConnection();
427+
con.Open();
428+
using var cmd = con.CreateCommand();
429+
cmd.CommandText = "select id from @rows";
430+
var dt = new DataTable();
431+
_ = dt.Columns.Add("id", typeof(int));
432+
_ = dt.Columns.Add("v", typeof(int));
433+
_ = dt.Rows.Add(DBNull.Value, 1);
434+
var p = cmd.CreateParameter();
435+
p.ParameterName = "@rows";
436+
p.Value = dt;
437+
p.TypeName = "dbo.t1";
438+
_ = cmd.Parameters.Add(p);
439+
440+
var ex = Throws<System.Data.Common.DbException>(() => cmd.ExecuteNonQuery());
441+
AreEqual("515", ex.Data["HelpLink.EvtID"]);
442+
}
443+
444+
[TestMethod]
445+
public void Structured_DataTable_DuplicatePrimaryKey_RaisesMsg2627()
446+
{
447+
var simulation = new Simulation();
448+
_ = simulation.ExecuteNonQuery("create type dbo.t1 as table (id int not null primary key, v int)");
449+
450+
using var con = simulation.CreateDbConnection();
451+
con.Open();
452+
using var cmd = con.CreateCommand();
453+
cmd.CommandText = "select id from @rows";
454+
var dt = new DataTable();
455+
_ = dt.Columns.Add("id", typeof(int));
456+
_ = dt.Columns.Add("v", typeof(int));
457+
_ = dt.Rows.Add(1, 10);
458+
_ = dt.Rows.Add(1, 20);
459+
var p = cmd.CreateParameter();
460+
p.ParameterName = "@rows";
461+
p.Value = dt;
462+
p.TypeName = "dbo.t1";
463+
_ = cmd.Parameters.Add(p);
464+
465+
var ex = Throws<System.Data.Common.DbException>(() => cmd.ExecuteNonQuery());
466+
AreEqual("2627", ex.Data["HelpLink.EvtID"]);
467+
}
468+
469+
[TestMethod]
470+
public void Structured_DataTable_CheckViolation_RaisesMsg547()
471+
{
472+
var simulation = new Simulation();
473+
_ = simulation.ExecuteNonQuery("create type dbo.t1 as table (id int not null check (id > 0))");
474+
475+
using var con = simulation.CreateDbConnection();
476+
con.Open();
477+
using var cmd = con.CreateCommand();
478+
cmd.CommandText = "select id from @rows";
479+
var dt = new DataTable();
480+
_ = dt.Columns.Add("id", typeof(int));
481+
_ = dt.Rows.Add(-5);
482+
var p = cmd.CreateParameter();
483+
p.ParameterName = "@rows";
484+
p.Value = dt;
485+
p.TypeName = "dbo.t1";
486+
_ = cmd.Parameters.Add(p);
487+
488+
var ex = Throws<System.Data.Common.DbException>(() => cmd.ExecuteNonQuery());
489+
AreEqual("547", ex.Data["HelpLink.EvtID"]);
490+
}
491+
420492
[TestMethod]
421493
public void Structured_IDataReader_BindsViaCommand()
422494
{

0 commit comments

Comments
 (0)