Skip to content

Commit 5265804

Browse files
committed
Live SSMS shakedown fixes spanning Table Designer, Activity Monitor, and standard reports: dynamic SQL executed via EXEC()/sp_executesql brackets its statements with real's nested-proc token discipline (DONEINPROC per statement, RETURNSTATUS 0 + DONEPROC on scope exit), the simulator's version identity becomes the reference build 17.0.4065.4 across SERVERPROPERTY/@@VERSION/@@MICROSOFTVERSION/LOGINACK/prelogin with xp_msver rows pinned to the probed reference output, parenthesized join groups in FROM parse and execute as SysP partition catalog views already shipped, the legacy permissions() bitmap, sys.assembly_types and sys.columns gain their missing real columns, COLLATE database_default/catalog_default resolve as bind-time keywords, Msg 207 prints only the leaf column name like real, and SERVERPROPERTY ProcessID/ComputerNamePhysicalNetBIOS return the host process id and machine placeholder instead of the NULLs Activity Monitor crashes casting.
1 parent e341378 commit 5265804

38 files changed

Lines changed: 1449 additions & 156 deletions
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
using static Microsoft.VisualStudio.TestTools.UnitTesting.Assert;
2+
3+
namespace SqlServerSimulator;
4+
5+
/// <summary>
6+
/// Pins the outcome-stream markers that drive the TDS endpoint's DONEINPROC /
7+
/// RETURNSTATUS / DONEPROC discipline for an <c>EXEC('…')</c> / sp_executesql
8+
/// dynamic-SQL scope. Real SQL Server runs such a body as a nested procedure
9+
/// scope — its statements report DONEINPROC (0xFF) and the scope closes with
10+
/// RETURNSTATUS + DONEPROC — where the simulator previously emitted a plain
11+
/// batch DONE (0xFD). The engine now brackets the dynamic body's outcomes with
12+
/// <see cref="SimulatedProcScopeBoundary"/> markers (Enter / Exit) that
13+
/// <c>StreamOutcomesAsync</c> consumes; every in-process consumer ignores them.
14+
/// Token-level shape captured cleartext against SQL Server 2025 (2026-07-19).
15+
/// </summary>
16+
[TestClass]
17+
public sealed class ProcScopeBoundaryTests
18+
{
19+
private static List<SimulatedStatementOutcome> Outcomes(string sql)
20+
{
21+
var simulation = new Simulation();
22+
using var connection = simulation.CreateDbConnection();
23+
connection.Open();
24+
using var command = connection.CreateCommand();
25+
command.CommandText = sql;
26+
return [.. simulation.CreateResultSetsForCommand(command)];
27+
}
28+
29+
private static string Shape(SimulatedStatementOutcome outcome) => outcome switch
30+
{
31+
SimulatedProcScopeBoundary { IsEnter: true } => "ENTER",
32+
SimulatedProcScopeBoundary => "EXIT",
33+
SimulatedQueryResult => "QUERY",
34+
_ => "OTHER",
35+
};
36+
37+
[TestMethod]
38+
public void ExecString_BracketsInnerOutcomesWithEnterAndExitMarkers()
39+
{
40+
var shapes = Outcomes("select 1 as a; exec('select 2 as b'); select 3 as c").Select(Shape).ToList();
41+
42+
CollectionAssert.AreEqual(new[] { "QUERY", "ENTER", "QUERY", "EXIT", "QUERY" }, shapes);
43+
}
44+
45+
[TestMethod]
46+
public void ExecString_WithNoResultSet_StillBracketsScope()
47+
{
48+
// A body that produces no result set must still open and close the scope
49+
// (real emits RETURNSTATUS + DONEPROC even with no DONEINPROC in between).
50+
var boundaries = Outcomes("exec('declare @x int; set @x = 1')")
51+
.OfType<SimulatedProcScopeBoundary>()
52+
.Select(b => b.IsEnter)
53+
.ToList();
54+
55+
CollectionAssert.AreEqual(new[] { true, false }, boundaries);
56+
}
57+
58+
[TestMethod]
59+
public void SpExecuteSql_BracketsScope()
60+
{
61+
var shapes = Outcomes("exec sp_executesql N'select 42 as answer'").Select(Shape).ToList();
62+
63+
AreEqual("ENTER", shapes[0]);
64+
AreEqual("QUERY", shapes[1]);
65+
AreEqual("EXIT", shapes[^1]);
66+
}
67+
68+
[TestMethod]
69+
public void BatchWithoutExec_HasNoBoundaryMarkers()
70+
{
71+
var boundaries = Outcomes("select 1; select 2").OfType<SimulatedProcScopeBoundary>().Count();
72+
73+
AreEqual(0, boundaries);
74+
}
75+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
using Microsoft.Data.SqlClient;
2+
using static Microsoft.VisualStudio.TestTools.UnitTesting.Assert;
3+
4+
namespace SqlServerSimulator;
5+
6+
/// <summary>
7+
/// End-to-end wire coverage for <c>EXEC('…')</c> dynamic SQL inside a SQLBatch.
8+
/// Real SQL Server runs the dynamic body as a nested procedure scope: its
9+
/// statements report DONEINPROC (0xFF) and the scope closes with RETURNSTATUS +
10+
/// DONEPROC, where the simulator previously emitted a plain batch DONE (0xFD).
11+
/// The SSMS report viewer's environment-probe batch (whose final result set
12+
/// comes from an <c>EXEC('…')</c>) froze .NET Framework's stricter native TDS
13+
/// parser on the old shape; these tests drive the same shape over real SqlClient
14+
/// so the corrected token stream stays well-formed. Real-server token shape
15+
/// captured cleartext 2026-07-19.
16+
/// </summary>
17+
[TestClass]
18+
public sealed class DynamicSqlExecWireTests
19+
{
20+
public TestContext TestContext { get; set; } = null!;
21+
22+
[TestMethod]
23+
public async Task ExecInBatch_AfterBatchLevelSelects_AllResultSetsRead()
24+
{
25+
var simulation = new Simulation();
26+
await using var listener = await simulation.ListenAsync(0, TestContext.CancellationToken);
27+
await using var connection = await Wire.OpenAsync(listener, TestContext.CancellationToken);
28+
29+
// Batch-level SELECTs (plain DONE) followed by an EXEC('…') result set
30+
// (DONEINPROC + RETURNSTATUS + DONEPROC) — the report-viewer shape.
31+
const string batch = "select 1 as a; select 2 as b; exec('select 3 as c')";
32+
await using var command = new SqlCommand(batch, connection);
33+
await using var reader = await command.ExecuteReaderAsync(TestContext.CancellationToken);
34+
35+
var seen = new List<(string Column, int Value)>();
36+
do
37+
{
38+
while (await reader.ReadAsync(TestContext.CancellationToken))
39+
seen.Add((reader.GetName(0), reader.GetInt32(0)));
40+
}
41+
while (await reader.NextResultAsync(TestContext.CancellationToken));
42+
43+
CollectionAssert.AreEqual(
44+
new[] { ("a", 1), ("b", 2), ("c", 3) },
45+
seen);
46+
}
47+
48+
[TestMethod]
49+
public async Task ReportViewerEnvironmentProbe_ThreeResultSets()
50+
{
51+
var simulation = new Simulation();
52+
await using var listener = await simulation.ListenAsync(0, TestContext.CancellationToken);
53+
await using var connection = await Wire.OpenAsync(listener, TestContext.CancellationToken);
54+
55+
await using (var lockTimeout = new SqlCommand("SET LOCK_TIMEOUT 10000", connection))
56+
_ = await lockTimeout.ExecuteNonQueryAsync(TestContext.CancellationToken);
57+
58+
const string probe = """
59+
DECLARE @edition sysname;
60+
SET @edition = cast(SERVERPROPERTY(N'EDITION') as sysname);
61+
SELECT case when @edition = N'SQL Azure' then 2 else 1 end as 'DatabaseEngineType', SERVERPROPERTY('EngineEdition') AS DatabaseEngineEdition, SERVERPROPERTY('ProductVersion') AS ProductVersion, @@MICROSOFTVERSION AS MicrosoftVersion, 0 as IsFabricServer, convert(sysname, SERVERPROPERTY(N'Collation')) AS Collation;
62+
select host_platform from sys.dm_os_host_info
63+
if @edition = N'SQL Azure'
64+
select 'TCP' as ConnectionProtocol
65+
else
66+
exec ('select CONVERT(nvarchar(40),CONNECTIONPROPERTY(''net_transport'')) as ConnectionProtocol')
67+
""";
68+
69+
await using var command = new SqlCommand(probe, connection);
70+
await using var reader = await command.ExecuteReaderAsync(TestContext.CancellationToken);
71+
72+
var resultSets = 0;
73+
var lastColumn = "";
74+
do
75+
{
76+
var rows = 0;
77+
while (await reader.ReadAsync(TestContext.CancellationToken))
78+
rows++;
79+
AreEqual(1, rows);
80+
lastColumn = reader.GetName(reader.FieldCount - 1);
81+
resultSets++;
82+
}
83+
while (await reader.NextResultAsync(TestContext.CancellationToken));
84+
85+
AreEqual(3, resultSets);
86+
AreEqual("ConnectionProtocol", lastColumn);
87+
}
88+
}

SqlServerSimulator.Tests.SqlClient/NetworkListenerTests.cs

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,18 +37,16 @@ public async Task ListenAsync_PortZero_AssignsEphemeralPort()
3737
}
3838

3939
// SqlConnection.ServerVersion is parsed from the LOGINACK token's
40-
// ProgVersion (major.minor.build). The simulator deliberately reports a
41-
// 0 build ("17.00.0000") as an honest "not a real SQL Server build"
42-
// marker — probed harmless to SMO's Object Explorer (the Databases node
43-
// populates regardless; the enumeration gate was ntext RPC-param support,
44-
// not the reported version).
40+
// ProgVersion (major.minor.build). The simulator reports the SQL Server
41+
// 2025 reference build 17.0.4065.4, so SqlClient reads "17.00.4065" — a
42+
// real build number is what lets SSMS's per-build feature gates proceed.
4543
[TestMethod]
46-
public async Task LoginAck_ReportsSimulatedZeroBuild()
44+
public async Task LoginAck_ReportsReferenceBuild()
4745
{
4846
var simulation = new Simulation();
4947
await using var listener = await simulation.ListenAsync(0, TestContext.CancellationToken);
5048
await using var connection = new SqlConnection(ConnectionString(listener));
5149
await connection.OpenAsync(TestContext.CancellationToken);
52-
AreEqual("17.00.0000", connection.ServerVersion);
50+
AreEqual("17.00.4065", connection.ServerVersion);
5351
}
5452
}

SqlServerSimulator.Tests/AtAtKeywordExpansionTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public void AtAt_MaxConnections_Returns32767()
2323

2424
[TestMethod]
2525
public void AtAt_MicrosoftVersion_ReturnsProductVersionEncoding()
26-
=> AreEqual(285212672, new Simulation().ExecuteScalar("select @@microsoftversion"));
26+
=> AreEqual(285216737, new Simulation().ExecuteScalar("select @@microsoftversion"));
2727

2828
[TestMethod]
2929
public void AtAt_MicrosoftVersion_ComposesInExpression()

SqlServerSimulator.Tests/CollationDeclaredColumnTests.cs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,77 @@ public void TempTable_InheritsActiveDatabaseDefaultCollation()
421421
AreEqual(1, n);
422422
}
423423

424+
/// <summary>
425+
/// <c>COLLATE database_default</c> in a table-variable column definition
426+
/// resolves to the active database's collation at bind time. SSMS's Disk
427+
/// Usage report declares <c>nvarchar(...) COLLATE database_default</c>
428+
/// table variables; before this the keyword was rejected as an
429+
/// unrecognized collation. Probe-confirmed against SQL Server 2025:
430+
/// <c>SQL_VARIANT_PROPERTY(..., 'Collation')</c> reports the active
431+
/// database's collation.
432+
/// </summary>
433+
[TestMethod]
434+
public void TableVariable_CollateDatabaseDefault_ResolvesToActiveDatabaseCollation()
435+
{
436+
var sim = new Simulation();
437+
using var conn = sim.CreateOpenConnection();
438+
_ = conn.CreateCommand("alter database simulated collate Latin1_General_CS_AS").ExecuteNonQuery();
439+
var coll = conn.CreateCommand(
440+
"declare @tv table (c nvarchar(10) collate database_default); insert @tv values (N'a'); " +
441+
"select convert(sysname, sql_variant_property(cast(c as sql_variant), 'Collation')) from @tv").ExecuteScalar();
442+
AreEqual("Latin1_General_CS_AS", coll);
443+
}
444+
445+
/// <summary>
446+
/// <c>COLLATE database_default</c> on a <c>#temp</c> column resolves to the
447+
/// *session* database's collation — probe-confirmed the real server uses
448+
/// the connection's active database, not tempdb's, so the resolved value
449+
/// matches an ordinary column's.
450+
/// </summary>
451+
[TestMethod]
452+
public void TempTable_CollateDatabaseDefault_ResolvesToSessionDatabaseCollation()
453+
{
454+
var sim = new Simulation();
455+
using var conn = sim.CreateOpenConnection();
456+
_ = conn.CreateCommand("alter database simulated collate Latin1_General_CS_AS").ExecuteNonQuery();
457+
_ = conn.CreateCommand("create table #t (c nvarchar(10) collate database_default); insert #t values (N'a')").ExecuteNonQuery();
458+
var coll = conn.CreateCommand(
459+
"select convert(sysname, sql_variant_property(cast(c as sql_variant), 'Collation')) from #t").ExecuteScalar();
460+
AreEqual("Latin1_General_CS_AS", coll);
461+
}
462+
463+
/// <summary>
464+
/// <c>COLLATE database_default</c> in a regular <c>CREATE TABLE</c> column
465+
/// records the resolved database collation in
466+
/// <c>sys.columns.collation_name</c> (matching real), and the keyword is
467+
/// case-insensitive.
468+
/// </summary>
469+
[TestMethod]
470+
public void CreateTable_CollateDatabaseDefault_RecordsResolvedCollationCaseInsensitive()
471+
{
472+
var sim = new Simulation();
473+
using var conn = sim.CreateOpenConnection();
474+
_ = conn.CreateCommand("alter database simulated collate Latin1_General_CS_AS").ExecuteNonQuery();
475+
_ = conn.CreateCommand("create table t (a nvarchar(10) collate DATABASE_DEFAULT)").ExecuteNonQuery();
476+
AreEqual("Latin1_General_CS_AS", conn.CreateCommand(
477+
"select collation_name from sys.columns where object_id = object_id('t') and name = 'a'").ExecuteScalar());
478+
}
479+
480+
/// <summary>
481+
/// Expression-level <c>expr COLLATE database_default</c> resolves to the
482+
/// active database's collation (probe-confirmed), sharing the same
483+
/// keyword-resolution seam as the column-definition sites.
484+
/// </summary>
485+
[TestMethod]
486+
public void Expression_CollateDatabaseDefault_ResolvesToActiveDatabaseCollation()
487+
{
488+
var sim = new Simulation();
489+
using var conn = sim.CreateOpenConnection();
490+
_ = conn.CreateCommand("alter database simulated collate Latin1_General_CS_AS").ExecuteNonQuery();
491+
AreEqual("Latin1_General_CS_AS", conn.CreateCommand(
492+
"select convert(sysname, sql_variant_property(N'x' collate database_default, 'Collation'))").ExecuteScalar());
493+
}
494+
424495
/// <summary>
425496
/// LIKE consults the column's collation's <c>CaseSensitive</c> flag —
426497
/// covers the <c>CultureCollation.CaseSensitive</c> property path the

SqlServerSimulator.Tests/JoinTests.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -407,7 +407,7 @@ select 1 from a right join (select id from b where b.ref_id = o.id) bx
407407
public void RightJoin_DerivedTableRight_LateralCorrelationToLeft_Rejected()
408408
{
409409
// Real SQL Server raises Msg 4104 ("multi-part identifier could not be bound")
410-
// at bind-time; the simulator raises Msg 207 ("Invalid column name 'a.id'") at
410+
// at bind-time; the simulator raises Msg 207 ("Invalid column name 'id'") at
411411
// runtime because Reference.Run is the resolution point — the derived-table
412412
// parse doesn't see the left-side snapshot resolver, so resolution falls through
413413
// to the (null at top-level) outer resolver and fails on the first inner row.
@@ -421,6 +421,19 @@ public void RightJoin_DerivedTableRight_LateralCorrelationToLeft_Rejected()
421421
""", 207);
422422
}
423423

424+
/// <summary>
425+
/// Msg 207 renders only the leaf identifier — a qualified reference to a
426+
/// nonexistent column drops the table / alias qualifier, matching real
427+
/// SQL Server verbatim (probe-confirmed: <c>col.is_replicated</c> surfaces
428+
/// as <c>"Invalid column name 'is_replicated'."</c>, not the qualified
429+
/// form). Surfaced by SSMS's Table-Designer column query.
430+
/// </summary>
431+
[TestMethod]
432+
public void InvalidColumnName_QualifiedReference_RendersLeafOnly()
433+
=> new Simulation().AssertSqlError(
434+
"create table t (id int); select t.nosuchcol from t",
435+
207, "Invalid column name 'nosuchcol'.");
436+
424437
[TestMethod]
425438
public void CommaFrom_TwoSources_FilterEqualsInnerJoin()
426439
{

0 commit comments

Comments
 (0)