Skip to content

Commit 9f71784

Browse files
committed
Add SqlServerSimulator.Tests.Smo for Microsoft.SqlServer.SqlManagementObjects test coverage using an in-code WideWorldImporters-shaped schema, fixed issues it revealed: named inline column DEFAULT constraints, sys.views carries type/type_desc with new all-object mirrors sys.all_views / sys.all_sql_modules, and sys.assemblies (empty) + sys.database_scoped_configurations (fresh-database defaults).
1 parent 80ff56d commit 9f71784

16 files changed

Lines changed: 521 additions & 4 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; credential enforcement via the `CREATE LOGIN` registry, Msg 18456 on mismatch; EF via plain `UseSqlServer`; oracle = `*.Tests.SqlClient`) → [`tds-endpoint.md`](docs/claude/tds-endpoint.md).
167+
- **TDS network endpoint** (`Simulation.ListenAsync``SimulatedNetworkListener`; real SqlClient over loopback TCP+TLS; SQLBatch/RPC/TM, no bulk; 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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
namespace SqlServerSimulator;
2+
3+
/// <summary>
4+
/// Builds the shared SMO fixture once before the parallel run (a simulation +
5+
/// WWI-shaped schema + login + TDS listener) and tears the listener down after.
6+
/// Warming it here keeps first-touch JIT / TLS-handshake cost out of the test
7+
/// timings, the same rationale as the sibling oracles' assembly-init warm-up.
8+
/// </summary>
9+
[TestClass]
10+
public static class AssemblyHooks
11+
{
12+
[AssemblyInitialize]
13+
public static void Initialize(TestContext _) => SmoFixture.Initialize();
14+
15+
[AssemblyCleanup]
16+
public static void Cleanup() => SmoFixture.Cleanup();
17+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
using Microsoft.SqlServer.Management.Smo;
2+
using static Microsoft.VisualStudio.TestTools.UnitTesting.Assert;
3+
4+
namespace SqlServerSimulator;
5+
6+
/// <summary>
7+
/// The Object-Explorer surface: SMO enumerating databases, tables, and a rich
8+
/// table's child collections (columns / indexes / FKs / triggers), plus the
9+
/// views and stored-procedures folders. This is the shape SSMS populates its
10+
/// tree from.
11+
/// </summary>
12+
[TestClass]
13+
public sealed class ObjectExplorerTests
14+
{
15+
private static Server server = null!;
16+
17+
[ClassInitialize]
18+
public static void ClassInitialize(TestContext _) => server = SmoFixture.NewServer();
19+
20+
[ClassCleanup]
21+
public static void ClassCleanup() => server.ConnectionContext.Disconnect();
22+
23+
private static Database FixtureDatabase => server.Databases[SmoFixture.DatabaseName];
24+
25+
[TestMethod]
26+
public void Databases_ContainsFixtureDatabase()
27+
{
28+
IsTrue(server.Databases.Contains(SmoFixture.DatabaseName));
29+
}
30+
31+
[TestMethod]
32+
public void Tables_CountMatchesFixture()
33+
{
34+
// Five declared tables plus the auto-created temporal history sibling
35+
// (EmployeeRolesHistory), which SMO lists as its own table.
36+
AreEqual(6, FixtureDatabase.Tables.Count);
37+
}
38+
39+
[TestMethod]
40+
public void RichTable_ChildCollectionCountsMatchFixture()
41+
{
42+
var customers = FixtureDatabase.Tables["Customers", "Sales"];
43+
IsNotNull(customers);
44+
45+
AreEqual(8, customers.Columns.Count);
46+
// Clustered PK + IX_Customers_Name (INCLUDE) + IX_Customers_Active (filtered).
47+
AreEqual(3, customers.Indexes.Count);
48+
AreEqual(1, customers.ForeignKeys.Count);
49+
AreEqual(1, customers.Triggers.Count);
50+
}
51+
52+
[TestMethod]
53+
public void Views_EnumerateAndContainFixtureView()
54+
{
55+
// Enumerating .Count must not throw (exercises the sys.all_views path).
56+
IsGreaterThanOrEqualTo(1, FixtureDatabase.Views.Count);
57+
IsNotNull(FixtureDatabase.Views["CustomerSummary", "Sales"]);
58+
}
59+
60+
[TestMethod]
61+
public void StoredProcedures_EnumerateAndContainFixtureProc()
62+
{
63+
IsGreaterThanOrEqualTo(1, FixtureDatabase.StoredProcedures.Count);
64+
IsNotNull(FixtureDatabase.StoredProcedures["GetCustomerCount", "Sales"]);
65+
}
66+
67+
[TestMethod]
68+
public void ExtendedProperties_SurfaceOnTableAndColumn()
69+
{
70+
var people = FixtureDatabase.Tables["People", "Application"];
71+
IsNotNull(people);
72+
IsGreaterThanOrEqualTo(1, people.ExtendedProperties.Count);
73+
IsNotNull(people.ExtendedProperties["MS_Description"]);
74+
75+
var fullName = people.Columns["FullName"];
76+
IsNotNull(fullName);
77+
IsGreaterThanOrEqualTo(1, fullName.ExtendedProperties.Count);
78+
}
79+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
using Microsoft.SqlServer.Management.Smo;
2+
using static Microsoft.VisualStudio.TestTools.UnitTesting.Assert;
3+
4+
namespace SqlServerSimulator;
5+
6+
/// <summary>
7+
/// SMO's Script-As → CREATE: the definition text SSMS generates from the
8+
/// simulator's catalog surface. Assertions target the load-bearing lines
9+
/// (StringAssert.Contains), not a brittle full-text match — the exact
10+
/// whitespace / index-option boilerplate is SMO's, not the simulator's.
11+
/// </summary>
12+
[TestClass]
13+
public sealed class ScriptAsTests
14+
{
15+
private static Server server = null!;
16+
17+
[ClassInitialize]
18+
public static void ClassInitialize(TestContext _) => server = SmoFixture.NewServer();
19+
20+
[ClassCleanup]
21+
public static void ClassCleanup() => server.ConnectionContext.Disconnect();
22+
23+
private static Database FixtureDatabase => server.Databases[SmoFixture.DatabaseName];
24+
25+
private static string Script(Table table, ScriptingOptions options) =>
26+
string.Join('\n', table.Script(options).Cast<string>());
27+
28+
[TestMethod]
29+
public void RichTable_CreateScript_ContainsLoadBearingLines()
30+
{
31+
var customers = FixtureDatabase.Tables["Customers", "Sales"];
32+
IsNotNull(customers);
33+
34+
var script = Script(customers, new ScriptingOptions
35+
{
36+
DriAll = true,
37+
Indexes = true,
38+
Triggers = true,
39+
ExtendedProperties = true,
40+
});
41+
42+
// Schema-qualified CREATE TABLE header.
43+
Assert.Contains("CREATE TABLE [Sales].[Customers]", script);
44+
// A representative column with type + nullability.
45+
Assert.Contains("[CustomerName] [nvarchar](100)", script);
46+
Assert.Contains("NOT NULL", script);
47+
// Identity clause.
48+
Assert.Contains("IDENTITY(1,1)", script);
49+
// Primary key.
50+
Assert.Contains("CONSTRAINT [PK_Customers] PRIMARY KEY CLUSTERED", script);
51+
// Foreign key with its references clause (cross-schema).
52+
Assert.Contains("CONSTRAINT [FK_Customers_People] FOREIGN KEY", script);
53+
Assert.Contains("REFERENCES [Application].[People] ([PersonID])", script);
54+
// Nonclustered index including its INCLUDE list.
55+
Assert.Contains("CREATE NONCLUSTERED INDEX [IX_Customers_Name]", script);
56+
Assert.Contains("INCLUDE([CreditLimit])", script);
57+
// Named default constraint.
58+
Assert.Contains("CONSTRAINT [DF_Customers_Discount]", script);
59+
Assert.Contains("DEFAULT", script);
60+
// Check constraint.
61+
Assert.Contains("CONSTRAINT [CK_Customers_CreditLimit]", script);
62+
Assert.Contains("CHECK", script);
63+
}
64+
65+
[TestMethod]
66+
public void TemporalTable_CreateScript_EmitsSystemVersioning()
67+
{
68+
var employeeRoles = FixtureDatabase.Tables["EmployeeRoles", "Application"];
69+
IsNotNull(employeeRoles);
70+
71+
var script = Script(employeeRoles, new ScriptingOptions { DriAll = true });
72+
73+
Assert.Contains("GENERATED ALWAYS AS ROW START", script);
74+
Assert.Contains("GENERATED ALWAYS AS ROW END", script);
75+
Assert.Contains("PERIOD FOR SYSTEM_TIME ([ValidFrom], [ValidTo])", script);
76+
Assert.Contains("SYSTEM_VERSIONING = ON", script);
77+
Assert.Contains("HISTORY_TABLE = [Application].[EmployeeRolesHistory]", script);
78+
}
79+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using Microsoft.SqlServer.Management.Smo;
2+
using static Microsoft.VisualStudio.TestTools.UnitTesting.Assert;
3+
4+
namespace SqlServerSimulator;
5+
6+
/// <summary>
7+
/// A plain query through the SMO connection: SMO's own session must appear in
8+
/// <c>sys.dm_exec_sessions</c>, proving the endpoint tracks the live connection
9+
/// SMO opened.
10+
/// </summary>
11+
[TestClass]
12+
public sealed class SessionsTests
13+
{
14+
private static Server server = null!;
15+
16+
[ClassInitialize]
17+
public static void ClassInitialize(TestContext _) => server = SmoFixture.NewServer();
18+
19+
[ClassCleanup]
20+
public static void ClassCleanup() => server.ConnectionContext.Disconnect();
21+
22+
[TestMethod]
23+
public void DmExecSessions_ContainsOwnConnection()
24+
{
25+
var count = Convert.ToInt32(server.ConnectionContext.ExecuteScalar(
26+
"SELECT COUNT(*) FROM sys.dm_exec_sessions WHERE session_id = @@SPID"));
27+
AreEqual(1, count);
28+
}
29+
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
using Microsoft.Data.SqlClient;
2+
using Microsoft.SqlServer.Management.Common;
3+
using Microsoft.SqlServer.Management.Smo;
4+
5+
namespace SqlServerSimulator;
6+
7+
/// <summary>
8+
/// The one shared fixture the SMO oracle drives: a <see cref="Simulation"/>
9+
/// seeded with a compact WWI-shaped schema, a <c>smo</c> login, and a TDS
10+
/// listener on an OS-assigned port. Real <c>Microsoft.SqlServer.Management.Smo</c>
11+
/// (the library behind SSMS Object Explorer + Script-As) connects over loopback
12+
/// against this, exactly as SSMS would.
13+
///
14+
/// The schema doubles as documentation of what the SMO oracle exercises: two
15+
/// schemas (<c>Sales</c> / <c>Application</c>); an identity clustered PK; an FK
16+
/// web spanning both schemas plus a multi-column FK; nonclustered indexes (one
17+
/// with <c>INCLUDE</c>, one filtered); named + auto-named DEFAULT constraints;
18+
/// a CHECK constraint; a computed column; a <c>rowversion</c> column; a
19+
/// system-versioned temporal pair; extended properties on a table and a column;
20+
/// an AFTER INSERT trigger; a view and a stored procedure; and seed rows so
21+
/// <c>sys.partitions.rows</c> is non-zero.
22+
/// </summary>
23+
internal static class SmoFixture
24+
{
25+
private static SimulatedNetworkListener? listener;
26+
27+
/// <summary>The database every SMO test targets — the simulation's default user database.</summary>
28+
public const string DatabaseName = "simulated";
29+
30+
/// <summary>Connection string SMO connects with (loopback TDS + TLS, the <c>smo</c> login).</summary>
31+
public static string ConnectionString { get; private set; } = "";
32+
33+
public static void Initialize()
34+
{
35+
var sim = new Simulation();
36+
SeedSchema(sim);
37+
// The listener roots the simulation for the run; no separate field needed.
38+
listener = sim.ListenAsync(0).GetAwaiter().GetResult();
39+
ConnectionString =
40+
$"Server=127.0.0.1,{listener.Port};Database={DatabaseName};User ID=smo;Password=smo;" +
41+
"TrustServerCertificate=True;Encrypt=True;Connect Timeout=30";
42+
}
43+
44+
public static void Cleanup()
45+
{
46+
listener?.Dispose();
47+
listener = null;
48+
}
49+
50+
/// <summary>
51+
/// Builds a fresh SMO <see cref="Server"/> over its own loopback connection.
52+
/// SMO caches metadata per <see cref="Server"/>, so each test takes its own —
53+
/// the queries run against the in-process simulator in milliseconds.
54+
/// </summary>
55+
public static Server NewServer() =>
56+
new(new ServerConnection(new SqlConnection(ConnectionString)));
57+
58+
private static void SeedSchema(Simulation sim)
59+
{
60+
// Batches run one at a time: CREATE VIEW / PROCEDURE / TRIGGER must each
61+
// be the sole statement in their batch (SQL Server's grammar rule), and
62+
// the in-process command doesn't split on GO.
63+
string[] batches =
64+
[
65+
"CREATE SCHEMA Sales",
66+
"CREATE SCHEMA Application",
67+
"""
68+
CREATE TABLE Application.People (
69+
PersonID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_People PRIMARY KEY CLUSTERED,
70+
FullName nvarchar(100) NOT NULL,
71+
IsEmployee bit NOT NULL CONSTRAINT DF_People_IsEmployee DEFAULT (0))
72+
""",
73+
"""
74+
CREATE TABLE Sales.Customers (
75+
CustomerID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_Customers PRIMARY KEY CLUSTERED,
76+
CustomerName nvarchar(100) NOT NULL,
77+
PrimaryContactPersonID int NOT NULL CONSTRAINT FK_Customers_People REFERENCES Application.People (PersonID),
78+
CreditLimit decimal(18, 2) NULL CONSTRAINT CK_Customers_CreditLimit CHECK (CreditLimit >= 0),
79+
DiscountPercent decimal(5, 2) NOT NULL CONSTRAINT DF_Customers_Discount DEFAULT (0),
80+
IsOnCreditHold bit NOT NULL DEFAULT (0),
81+
CreditLimitWithTax AS (CreditLimit * 1.1),
82+
ConcurrencyToken rowversion)
83+
""",
84+
"""
85+
CREATE TABLE Sales.Orders (
86+
OrderID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_Orders PRIMARY KEY CLUSTERED,
87+
CustomerID int NOT NULL CONSTRAINT FK_Orders_Customers REFERENCES Sales.Customers (CustomerID),
88+
OrderReference nvarchar(20) NOT NULL,
89+
CONSTRAINT UQ_Orders UNIQUE (OrderID, CustomerID))
90+
""",
91+
"""
92+
CREATE TABLE Sales.OrderLines (
93+
OrderLineID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_OrderLines PRIMARY KEY CLUSTERED,
94+
OrderID int NOT NULL,
95+
CustomerID int NOT NULL,
96+
CONSTRAINT FK_OrderLines_Orders FOREIGN KEY (OrderID, CustomerID)
97+
REFERENCES Sales.Orders (OrderID, CustomerID))
98+
""",
99+
"CREATE NONCLUSTERED INDEX IX_Customers_Name ON Sales.Customers (CustomerName) INCLUDE (CreditLimit)",
100+
"CREATE NONCLUSTERED INDEX IX_Customers_Active ON Sales.Customers (CustomerName) WHERE CreditLimit > 0",
101+
"""
102+
CREATE TABLE Application.EmployeeRoles (
103+
RoleID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_EmployeeRoles PRIMARY KEY CLUSTERED,
104+
PersonID int NOT NULL,
105+
RoleName nvarchar(50) NOT NULL,
106+
ValidFrom datetime2(7) GENERATED ALWAYS AS ROW START NOT NULL,
107+
ValidTo datetime2(7) GENERATED ALWAYS AS ROW END NOT NULL,
108+
PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo))
109+
WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = Application.EmployeeRolesHistory))
110+
""",
111+
"CREATE VIEW Sales.CustomerSummary AS SELECT CustomerID, CustomerName FROM Sales.Customers",
112+
"CREATE PROCEDURE Sales.GetCustomerCount AS SELECT COUNT(*) AS Cnt FROM Sales.Customers",
113+
"CREATE TRIGGER Sales.trg_Customers_Insert ON Sales.Customers AFTER INSERT AS BEGIN SET NOCOUNT ON; END",
114+
"EXEC sys.sp_addextendedproperty @name = N'MS_Description', @value = N'People master table', " +
115+
"@level0type = N'SCHEMA', @level0name = N'Application', @level1type = N'TABLE', @level1name = N'People'",
116+
"EXEC sys.sp_addextendedproperty @name = N'MS_Description', @value = N'Person full name', " +
117+
"@level0type = N'SCHEMA', @level0name = N'Application', @level1type = N'TABLE', @level1name = N'People', " +
118+
"@level2type = N'COLUMN', @level2name = N'FullName'",
119+
"INSERT Application.People (FullName, IsEmployee) VALUES ('Alice', 1), ('Bob', 0)",
120+
"INSERT Sales.Customers (CustomerName, PrimaryContactPersonID, CreditLimit) VALUES ('Acme', 1, 100), ('Globex', 2, 200)",
121+
"INSERT Sales.Orders (CustomerID, OrderReference) VALUES (1, 'PO-1'), (2, 'PO-2')",
122+
"INSERT Sales.OrderLines (OrderID, CustomerID) VALUES (1, 1), (2, 2)",
123+
"INSERT Application.EmployeeRoles (PersonID, RoleName) VALUES (1, 'Manager')",
124+
"CREATE LOGIN smo WITH PASSWORD = 'smo'",
125+
];
126+
127+
using var connection = sim.CreateDbConnection();
128+
connection.Open();
129+
foreach (var batch in batches)
130+
{
131+
using var command = connection.CreateCommand();
132+
command.CommandText = batch;
133+
_ = command.ExecuteNonQuery();
134+
}
135+
}
136+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net10.0</TargetFramework>
5+
<Nullable>enable</Nullable>
6+
<GenerateDocumentationFile>True</GenerateDocumentationFile>
7+
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
8+
<DebugType>embedded</DebugType>
9+
<DebugSymbols>true</DebugSymbols>
10+
<NoWarn>1591</NoWarn>
11+
<IsPackable>false</IsPackable>
12+
<RootNamespace>SqlServerSimulator</RootNamespace>
13+
<ImplicitUsings>enable</ImplicitUsings>
14+
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
15+
</PropertyGroup>
16+
17+
<ItemGroup>
18+
<PackageReference Include="coverlet.collector" Version="10.0.1">
19+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
20+
<PrivateAssets>all</PrivateAssets>
21+
</PackageReference>
22+
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.1.6" />
23+
<PackageReference Include="Microsoft.SqlServer.SqlManagementObjects" Version="172.76.0" />
24+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
25+
<PackageReference Include="MSTest.TestAdapter" Version="4.1.0" />
26+
<PackageReference Include="MSTest.TestFramework" Version="4.1.0" />
27+
</ItemGroup>
28+
29+
<ItemGroup>
30+
<ProjectReference Include="..\SqlServerSimulator\SqlServerSimulator.csproj" />
31+
</ItemGroup>
32+
33+
<ItemGroup>
34+
<None Include="..\.editorconfig" Link=".editorconfig" />
35+
</ItemGroup>
36+
37+
</Project>

0 commit comments

Comments
 (0)