Skip to content

Commit 351b349

Browse files
committed
SERVERPROPERTY, SESSIONPROPERTY, CONNECTIONPROPERTY, COLLATIONPROPERTY, LOGINPROPERTY, SESSION_CONTEXT, SQL_VARIANT_PROPERTY and sys.configurations' value columns project true sql_variant instead of inner-type substitution, carrying per-property inner base types (COLLATIONPROPERTY Version is tinyint, sys.configurations values are int) so mixed-inner UNION/ISNULL/COALESCE/CASE stay variant, SELECT INTO creates a sql_variant column, SESSION_CONTEXT preserves the stored value's type, comparisons unwrap, the wire reports 0x62 COLMETADATA, and arithmetic rejects with Msg 257 (variant with a convertible non-variant) split enforced identically at parse and run time.
1 parent f271a0e commit 351b349

25 files changed

Lines changed: 444 additions & 308 deletions

SqlServerSimulator.Tests.SqlClient/SqlVariantWireTests.cs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,46 @@ LEFT JOIN [sys].[database_scoped_configurations] AS [dbscl] WITH (NOLOCK)
7878
IsFalse((bool)reader["LegacyCardinalityEstimation"]);
7979
}
8080

81+
// SERVERPROPERTY projects sql_variant over the wire (0x62 COLMETADATA) like
82+
// real; each cell carries its probed inner base type — EngineEdition an int,
83+
// Edition an nvarchar string.
84+
[TestMethod]
85+
public async Task ServerProperty_ReadOverWire_ReportsVariantWithInnerTypes()
86+
{
87+
var simulation = new Simulation();
88+
await using var listener = await simulation.ListenAsync(0, TestContext.CancellationToken);
89+
await using var connection = await Wire.OpenAsync(listener, TestContext.CancellationToken);
90+
await using var command = new SqlCommand(
91+
"SELECT SERVERPROPERTY('EngineEdition') AS engine, SERVERPROPERTY('Edition') AS edition", connection);
92+
await using var reader = await command.ExecuteReaderAsync(TestContext.CancellationToken);
93+
94+
AreEqual("sql_variant", reader.GetDataTypeName(0));
95+
AreEqual("sql_variant", reader.GetDataTypeName(1));
96+
IsTrue(await reader.ReadAsync(TestContext.CancellationToken));
97+
_ = IsInstanceOfType<int>(reader.GetValue(0));
98+
AreEqual(3, reader.GetValue(0));
99+
AreEqual("Developer Edition (64-bit)", reader.GetValue(1));
100+
}
101+
102+
// SESSION_CONTEXT round-trips the stored value's base type through the
103+
// sql_variant wire form: an int stored reads back as a boxed int.
104+
[TestMethod]
105+
public async Task SessionContext_ReadOverWire_PreservesIntInner()
106+
{
107+
var simulation = new Simulation();
108+
await using var listener = await simulation.ListenAsync(0, TestContext.CancellationToken);
109+
await using var connection = await Wire.OpenAsync(listener, TestContext.CancellationToken);
110+
await using (var set = new SqlCommand("exec sp_set_session_context N'tenant', 42", connection))
111+
_ = await set.ExecuteNonQueryAsync(TestContext.CancellationToken);
112+
await using var command = new SqlCommand("SELECT SESSION_CONTEXT(N'tenant')", connection);
113+
await using var reader = await command.ExecuteReaderAsync(TestContext.CancellationToken);
114+
115+
AreEqual("sql_variant", reader.GetDataTypeName(0));
116+
IsTrue(await reader.ReadAsync(TestContext.CancellationToken));
117+
_ = IsInstanceOfType<int>(reader.GetValue(0));
118+
AreEqual(42, reader.GetValue(0));
119+
}
120+
81121
[TestMethod]
82122
public async Task CastWrappedInnerTypes_ReadOverWire()
83123
{

SqlServerSimulator.Tests/CatalogViewTests.cs

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -360,16 +360,18 @@ public void DmOsHostInfo_HostSkuNullOffWindows()
360360
}
361361

362362
[TestMethod]
363-
public void SysConfigurations_AgentXps_ValueInUseIsBigintZero()
363+
public void SysConfigurations_AgentXps_ValueInUseIsVariantIntZero()
364364
{
365365
// The exact query SSMS's SMO issues during its Object-Explorer
366366
// database-node preamble. Msg 208 here aborts the request before the
367-
// database enumeration, so the row must resolve and surface bigint.
367+
// database enumeration, so the row must resolve. value_in_use is
368+
// sql_variant carrying an int inner (like real).
368369
using var reader = new Simulation().ExecuteReader(
369370
"select value_in_use from sys.configurations where configuration_id = 16384");
371+
AreEqual("sql_variant", reader.GetDataTypeName(0));
370372
IsTrue(reader.Read());
371-
AreEqual(typeof(long), reader.GetFieldType(0));
372-
AreEqual(0L, reader.GetInt64(0));
373+
_ = Assert.IsInstanceOfType<int>(reader.GetValue(0));
374+
AreEqual(0, reader.GetValue(0));
373375
IsFalse(reader.Read());
374376
}
375377

@@ -389,10 +391,12 @@ public void SysConfigurations_ColumnShape()
389391
IsTrue(reader.Read());
390392
AreEqual(typeof(int), reader.GetFieldType(0));
391393
AreEqual(typeof(string), reader.GetFieldType(1));
392-
AreEqual(typeof(long), reader.GetFieldType(2));
393-
AreEqual(typeof(long), reader.GetFieldType(3));
394-
AreEqual(typeof(long), reader.GetFieldType(4));
395-
AreEqual(typeof(long), reader.GetFieldType(5));
394+
// value / minimum / maximum / value_in_use are sql_variant (object
395+
// field type), matching real — each wraps an int inner.
396+
AreEqual(typeof(object), reader.GetFieldType(2));
397+
AreEqual(typeof(object), reader.GetFieldType(3));
398+
AreEqual(typeof(object), reader.GetFieldType(4));
399+
AreEqual(typeof(object), reader.GetFieldType(5));
396400
AreEqual(typeof(string), reader.GetFieldType(6));
397401
AreEqual(typeof(bool), reader.GetFieldType(7));
398402
AreEqual(typeof(bool), reader.GetFieldType(8));
@@ -409,10 +413,11 @@ public void SysConfigurations_AgentXpsRow_MatchesReference()
409413
IsTrue(reader.Read());
410414
AreEqual(16384, reader.GetInt32(0));
411415
AreEqual("Agent XPs", reader.GetString(1));
412-
AreEqual(0L, reader.GetInt64(2));
413-
AreEqual(0L, reader.GetInt64(3));
414-
AreEqual(1L, reader.GetInt64(4));
415-
AreEqual(0L, reader.GetInt64(5));
416+
// The four sql_variant columns wrap an int inner (probe-confirmed).
417+
AreEqual(0, reader.GetValue(2));
418+
AreEqual(0, reader.GetValue(3));
419+
AreEqual(1, reader.GetValue(4));
420+
AreEqual(0, reader.GetValue(5));
416421
AreEqual("Enable or disable Agent XPs", reader.GetString(6));
417422
IsTrue(reader.GetBoolean(7));
418423
IsTrue(reader.GetBoolean(8));

SqlServerSimulator.Tests/CollationMetadataTests.cs

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -306,9 +306,9 @@ public void DatabasePropertyEx_MissingCloseParen_RaisesSyntaxError()
306306
"SELECT DATABASEPROPERTYEX('simulated', 'Status' extra"));
307307

308308
// === COLLATIONPROPERTY(collation_name, property) ===
309-
// Probe-confirmed against SQL Server 2025 (2026-07-14). Real SQL Server
310-
// projects sql_variant; the simulator surfaces the bare base type
311-
// (int for CodePage/LCID/ComparisonStyle/Version, nvarchar for Name).
309+
// Probe-confirmed against SQL Server 2025 (2026-07-14). Like real, the
310+
// result is sql_variant carrying a per-property inner base type: int for
311+
// CodePage/LCID/ComparisonStyle, tinyint for Version, nvarchar for Name.
312312

313313
[TestMethod]
314314
public void CollationProperty_CodePage_ReturnsAnsiCodePage()
@@ -327,19 +327,32 @@ public void CollationProperty_ComparisonStyle_ReturnsBitmask()
327327

328328
[TestMethod]
329329
public void CollationProperty_Version_SqlCollation_IsZero()
330-
=> AreEqual(0, new Simulation().ExecuteScalar(
330+
=> AreEqual((byte)0, new Simulation().ExecuteScalar(
331331
"SELECT COLLATIONPROPERTY('SQL_Latin1_General_CP1_CI_AS', 'Version')"));
332332

333333
[TestMethod]
334334
public void CollationProperty_Version_HundredSeriesCollation_IsTwo()
335-
=> AreEqual(2, new Simulation().ExecuteScalar(
335+
=> AreEqual((byte)2, new Simulation().ExecuteScalar(
336336
"SELECT COLLATIONPROPERTY('Latin1_General_100_CI_AS', 'Version')"));
337337

338338
[TestMethod]
339339
public void CollationProperty_Name_ReturnsCollationName()
340340
=> AreEqual("SQL_Latin1_General_CP1_CI_AS", new Simulation().ExecuteScalar(
341341
"SELECT COLLATIONPROPERTY('SQL_Latin1_General_CP1_CI_AS', 'Name')"));
342342

343+
// The result projects as sql_variant; CodePage carries an int inner, Version
344+
// a tinyint inner (probe-confirmed against SQL Server 2025).
345+
[TestMethod]
346+
public void CollationProperty_ReportsSqlVariantMetadataWithInnerType()
347+
{
348+
using var reader = new Simulation().ExecuteReader(
349+
"SELECT COLLATIONPROPERTY('SQL_Latin1_General_CP1_CI_AS', 'CodePage')");
350+
AreEqual("sql_variant", reader.GetDataTypeName(0));
351+
IsTrue(reader.Read());
352+
_ = Assert.IsInstanceOfType<int>(reader.GetValue(0));
353+
AreEqual(1252, reader.GetValue(0));
354+
}
355+
343356
// ComparisonStyle tracks the suffix flags — accent-insensitive adds bit 2,
344357
// case-sensitive clears bit 1, KS/WS clear the ignore-kana/width bits, and
345358
// binary collations report 0. Probe-confirmed values.

SqlServerSimulator.Tests/FormatMessageAndLoginScalarTests.cs

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -280,22 +280,25 @@ public void PwdCompare_VerifiesRealServerGeneratedHash()
280280

281281
// ---- LOGINPROPERTY ----
282282

283+
// Each property surfaces its probed inner base type through the
284+
// sql_variant result: datetime for the time properties, int for the
285+
// counters / flags, nvarchar for the name properties.
283286
[TestMethod]
284287
public void LoginProperty_KnownLogin_TimeAndCountProperties()
285288
{
286-
AreEqual("2020-01-01 00:00:00.000", Scalar("select loginproperty('dbo', 'PasswordLastSetTime')"));
287-
AreEqual("0", Scalar("select loginproperty('dbo', 'BadPasswordCount')"));
288-
AreEqual("1900-01-01 00:00:00.000", Scalar("select loginproperty('dbo', 'BadPasswordTime')"));
289-
AreEqual("1900-01-01 00:00:00.000", Scalar("select loginproperty('dbo', 'LockoutTime')"));
290-
AreEqual("0", Scalar("select loginproperty('dbo', 'HistoryLength')"));
289+
AreEqual(new DateTime(2020, 1, 1), Scalar("select loginproperty('dbo', 'PasswordLastSetTime')"));
290+
AreEqual(0, Scalar("select loginproperty('dbo', 'BadPasswordCount')"));
291+
AreEqual(new DateTime(1900, 1, 1), Scalar("select loginproperty('dbo', 'BadPasswordTime')"));
292+
AreEqual(new DateTime(1900, 1, 1), Scalar("select loginproperty('dbo', 'LockoutTime')"));
293+
AreEqual(0, Scalar("select loginproperty('dbo', 'HistoryLength')"));
291294
}
292295

293296
[TestMethod]
294297
public void LoginProperty_KnownLogin_BooleanFlags()
295298
{
296-
AreEqual("0", Scalar("select loginproperty('dbo', 'IsExpired')"));
297-
AreEqual("0", Scalar("select loginproperty('dbo', 'IsLocked')"));
298-
AreEqual("0", Scalar("select loginproperty('dbo', 'IsMustChange')"));
299+
AreEqual(0, Scalar("select loginproperty('dbo', 'IsExpired')"));
300+
AreEqual(0, Scalar("select loginproperty('dbo', 'IsLocked')"));
301+
AreEqual(0, Scalar("select loginproperty('dbo', 'IsMustChange')"));
299302
}
300303

301304
[TestMethod]
@@ -313,9 +316,22 @@ public void LoginProperty_KnownLogin_NameProperties()
313316
AreEqual("us_english", Scalar("select loginproperty('dbo', 'DefaultLanguage')"));
314317
}
315318

319+
// The result projects as sql_variant carrying the property's inner base
320+
// type — an int for a counter, a datetime for a time property.
321+
[TestMethod]
322+
public void LoginProperty_ReportsSqlVariantMetadataWithInnerType()
323+
{
324+
using var reader = new Simulation().ExecuteReader(
325+
"select loginproperty('dbo', 'IsExpired') as flag, loginproperty('dbo', 'PasswordLastSetTime') as pwset");
326+
AreEqual("sql_variant", reader.GetDataTypeName(0));
327+
IsTrue(reader.Read());
328+
_ = Assert.IsInstanceOfType<int>(reader.GetValue(0));
329+
_ = Assert.IsInstanceOfType<DateTime>(reader.GetValue(1));
330+
}
331+
316332
[TestMethod]
317333
public void LoginProperty_CaseInsensitiveLoginAndProperty()
318-
=> AreEqual("0", Scalar("select loginproperty('DBO', 'isexpired')"));
334+
=> AreEqual(0, Scalar("select loginproperty('DBO', 'isexpired')"));
319335

320336
[TestMethod]
321337
public void LoginProperty_UnknownLogin_ReturnsNull()

SqlServerSimulator.Tests/LoginDdlTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ public void LoginProperty_IsLocked_CreatedLogin_ReturnsZero()
127127
{
128128
var sim = new Simulation();
129129
_ = sim.ExecuteNonQuery("create login t with password = 'Xy!12345'");
130-
AreEqual("0", sim.ExecuteScalar("select loginproperty('t', 'IsLocked')"));
130+
AreEqual(0, sim.ExecuteScalar("select loginproperty('t', 'IsLocked')"));
131131
}
132132

133133
[TestMethod]

SqlServerSimulator.Tests/ServerPropertyTests.cs

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,14 @@ public void ProductLevel_ReturnsRTM()
2222
public void EngineEdition_Returns3AsInt()
2323
=> AreEqual(3, new Simulation().ExecuteScalar("select serverproperty('EngineEdition')"));
2424

25+
// The projection is sql_variant (like real); each cell surfaces its inner
26+
// CLR type — EngineEdition an int, SqlCharSet / SqlSortOrder a tinyint byte.
2527
[TestMethod]
26-
public void EngineEdition_SurfacesAsIntType()
28+
public void EngineEdition_SurfacesAsVariantWithIntInner()
2729
{
2830
using var reader = new Simulation().ExecuteReader("select serverproperty('EngineEdition')");
2931
IsTrue(reader.Read());
30-
AreEqual("int", reader.GetDataTypeName(0));
32+
AreEqual("sql_variant", reader.GetDataTypeName(0));
3133
_ = Assert.IsInstanceOfType<int>(reader.GetValue(0));
3234
AreEqual(3, reader.GetValue(0));
3335
}
@@ -37,7 +39,7 @@ public void SqlCharSet_ReturnsTinyIntOne()
3739
{
3840
using var reader = new Simulation().ExecuteReader("select serverproperty('SqlCharSet')");
3941
IsTrue(reader.Read());
40-
AreEqual("tinyint", reader.GetDataTypeName(0));
42+
AreEqual("sql_variant", reader.GetDataTypeName(0));
4143
AreEqual((byte)1, reader.GetValue(0));
4244
}
4345

@@ -46,7 +48,7 @@ public void SqlSortOrder_ReturnsTinyInt52OnDefaultCollation()
4648
{
4749
using var reader = new Simulation().ExecuteReader("select serverproperty('SqlSortOrder')");
4850
IsTrue(reader.Read());
49-
AreEqual("tinyint", reader.GetDataTypeName(0));
51+
AreEqual("sql_variant", reader.GetDataTypeName(0));
5052
AreEqual((byte)52, reader.GetValue(0));
5153
}
5254

@@ -74,20 +76,30 @@ public void ProductUpdateReference_ReturnsKb()
7476
public void ResourceVersion_ReturnsReferenceBuild()
7577
=> AreEqual("17.00.4065", new Simulation().ExecuteScalar("select serverproperty('ResourceVersion')"));
7678

79+
// The result is always sql_variant, whether or not the name argument is a
80+
// compile-time constant (a runtime @variable resolves the same value).
7781
[TestMethod]
78-
public void NonConstantName_FallsBackToNVarchar()
79-
=> AreEqual("3", new Simulation().ExecuteScalar(
82+
public void NonConstantName_StillReturnsVariantInner()
83+
=> AreEqual(3, new Simulation().ExecuteScalar(
8084
"declare @p nvarchar(30) = 'EngineEdition'; select serverproperty(@p)"));
8185

82-
// Typed values must survive set-op schema unification. int (EngineEdition)
83-
// and tinyint (SqlCharSet) promote to int per data-type precedence. Mixing
84-
// a numeric property with a string one instead raises a conversion error
85-
// (int outranks nvarchar) — a deliberate divergence from real SQL Server,
86-
// where every SERVERPROPERTY is sql_variant so no promotion occurs.
86+
// sql_variant UNION ALL keeps each row's own inner base type — no schema
87+
// unification / promotion (matching real, where SERVERPROPERTY is
88+
// sql_variant). Mixing a numeric property with a string one now succeeds
89+
// where the old bare-type substitution raised a conversion error.
8790
[TestMethod]
88-
public void UnionOfNumericProperties_PromotesToInt()
89-
=> AreEqual(3, new Simulation().ExecuteScalar(
90-
"SELECT SERVERPROPERTY('EngineEdition') UNION ALL SELECT SERVERPROPERTY('SqlCharSet')"));
91+
public void UnionOfMixedInnerProperties_KeepsPerRowInnerTypes()
92+
{
93+
using var reader = new Simulation().ExecuteReader(
94+
"SELECT SERVERPROPERTY('EngineEdition') AS v UNION ALL SELECT SERVERPROPERTY('Edition')");
95+
AreEqual("sql_variant", reader.GetDataTypeName(0));
96+
IsTrue(reader.Read());
97+
_ = Assert.IsInstanceOfType<int>(reader.GetValue(0));
98+
AreEqual(3, reader.GetValue(0));
99+
IsTrue(reader.Read());
100+
_ = Assert.IsInstanceOfType<string>(reader.GetValue(0));
101+
AreEqual("Developer Edition (64-bit)", reader.GetValue(0));
102+
}
91103

92104
[TestMethod]
93105
public void Collation_ReturnsServerCollation()

SqlServerSimulator.Tests/SessionContextTests.cs

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ namespace SqlServerSimulator;
66
[TestClass]
77
public sealed class SessionContextTests
88
{
9+
// SESSION_CONTEXT preserves the stored value's base type through the
10+
// sql_variant result: an int round-trips as int, an nvarchar as nvarchar.
911
[TestMethod]
10-
public void SetAndRead_Named()
11-
=> AreEqual("42", ExecuteScalar(
12+
public void SetAndRead_Named_PreservesIntType()
13+
=> AreEqual(42, ExecuteScalar(
1214
"exec sp_set_session_context @key = N'TenantId', @value = 42; select session_context(N'TenantId')"));
1315

1416
[TestMethod]
@@ -20,6 +22,29 @@ public void SetAndRead_Positional()
2022
public void MissingKey_ReturnsNull()
2123
=> IsInstanceOfType<DBNull>(ExecuteScalar("select session_context(N'nope')"));
2224

25+
// SESSION_CONTEXT projects sql_variant (like real); a stored int surfaces
26+
// its int inner and compares against an int column by unwrapping.
27+
[TestMethod]
28+
public void ReportsSqlVariantMetadata()
29+
{
30+
using var reader = new Simulation().ExecuteReader(
31+
"exec sp_set_session_context N'k', 7; select session_context(N'k')");
32+
AreEqual("sql_variant", reader.GetDataTypeName(0));
33+
AreEqual(typeof(object), reader.GetFieldType(0));
34+
IsTrue(reader.Read());
35+
_ = IsInstanceOfType<int>(reader.GetValue(0));
36+
AreEqual(7, reader.GetValue(0));
37+
}
38+
39+
[TestMethod]
40+
public void ConnectionProperty_ReportsSqlVariantMetadata()
41+
{
42+
using var reader = new Simulation().ExecuteReader("select connectionproperty('net_transport')");
43+
AreEqual("sql_variant", reader.GetDataTypeName(0));
44+
IsTrue(reader.Read());
45+
AreEqual("TCP", reader.GetValue(0));
46+
}
47+
2348
[TestMethod]
2449
public void Key_IsCaseSensitive()
2550
=> IsInstanceOfType<DBNull>(ExecuteScalar(

0 commit comments

Comments
 (0)