Skip to content

Commit 7950d2d

Browse files
committed
Seed all four SQL Server system databases in every Simulation so SSMS's msdb-dependent features work: has_dbaccess is now accessibility-aware, and msdb.dbo.syspolicy_system_health_state ships as a real empty view so server-level Policy Health displays clean instead of erroring on no-access.
1 parent 5375dda commit 7950d2d

10 files changed

Lines changed: 419 additions & 159 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ Readonly struct, up to 4 inline slots (SQL Server's grammar limit). API: `Leaf`,
6464
Six scopes, one home each. **Add new state to whichever class matches its true scope** — when in doubt, ask who outlives whom. Field rosters live in the source XML docs; this captures only identity + load-bearing contracts.
6565

6666
- **`Simulation`** = server / instance. Holds `SystemHeapTables`, the `Databases` dict, and `ServerCollationName` (`init`-only; defaults `SQL_Latin1_General_CP1_CI_AS`; mirrors `model.collation` — install-time seed for every new `Database`, both the lazy `"simulated"` seed and collation-less bacpac imports; `init` reflects real immutability). Public surface = `Simulation` ctor + `CreateDbConnection()` + `ImportBacpac()` + `AddRemoteSimulation()` + `ServerCollationName` + `ListenAsync()``SimulatedNetworkListener`.
67-
- **`Database`** (internal) = one database. Holds `Schemas`, `CompatibilityLevel`, `CollationName`, the rowversion counter (`@@DBTS`), the MVCC version store, and the principal/permission/extended-property/full-text/DDL-trigger surfaces. `Databases` is seeded at construction with the `master` system database (`database_id` 1, so `USE master` / `master.sys.*` / `master.dbo.<proc>` resolve without an import); the first `CreateDbConnection()` lazily seeds `DefaultDatabaseName` (`"simulated"`) when no *user* database is present, and master is excluded from the initial-database fallback so a fresh connection still lands on `simulated`. Database ids: master = 1, user databases from 5 in name order (real reserves 2-4 for tempdb/model/msdb, unmodeled) — single source of truth is `DbId.DatabasesWithIds`, consumed by `DB_ID`/`DB_NAME`, `sys.databases`, `OBJECT_NAME`, and `DBCC SHRINKDATABASE`. `USE <db>` switches session (Msg 911 on miss); 3-part names route cross-DB reads (`SELECT * FROM other.dbo.t`), but cross-DB writes raise `NotSupportedException` via `BatchContext.RejectCrossDatabaseMutation` — `USE` first.
67+
- **`Database`** (internal) = one database. Holds `Schemas`, `CompatibilityLevel`, `CollationName`, the rowversion counter (`@@DBTS`), the MVCC version store, and the principal/permission/extended-property/full-text/DDL-trigger surfaces. `Databases` is seeded at construction with all four system databases — `master` / `tempdb` / `model` / `msdb` (so `USE <systemdb>` / `master.sys.*` / `master.dbo.<proc>` / SSMS's `msdb.dbo.syspolicy_system_health_state` all resolve without an import); the first `CreateDbConnection()` lazily seeds `DefaultDatabaseName` (`"simulated"`) when no *user* database is present, and all four system databases are excluded from the initial-database fallback (`Simulation.SystemDatabaseNames`) so a fresh connection still lands on `simulated`. Database ids are a fixed map — master = 1, tempdb = 2, model = 3, msdb = 4; user databases from 5 in name order — single source of truth is `Simulation.SystemDatabaseIds` + `DbId.DatabasesWithIds`, consumed by `DB_ID`/`DB_NAME`, `sys.databases`, `OBJECT_NAME`, and `DBCC SHRINKDATABASE`. `has_dbaccess` is accessibility-aware: 1 for master/tempdb/msdb/user dbs, 0 for `model` (restricted template), NULL for unknown. `#temp` still routes through the connection's `TempTables`, not the seeded `tempdb`. `USE <db>` switches session (Msg 911 on miss); 3-part names route cross-DB reads (`SELECT * FROM other.dbo.t`), but cross-DB writes raise `NotSupportedException` via `BatchContext.RejectCrossDatabaseMutation` — `USE` first.
6868
- **`Schema`** (internal) = one namespace in a database. Holds the object dicts (`HeapTables` / `Functions` / `Views` / `Procedures` / `Sequences` / `Triggers` — DML triggers share the object namespace) + the type namespace (`TableTypes` / `AliasTypes` / `XmlSchemaCollections`). Schema-qualified refs route through `Database.Schemas[<schema>]`; unqualified falls back to `DefaultSchemaName` (`"dbo"`).
6969
- **`SimulatedDbConnection`** = session. Holds `@@`-state (`LastIdentity` = `SCOPE_IDENTITY`/`@@IDENTITY`, `LastStatementRowCount` = `@@ROWCOUNT`, `LastErrorNumber` = `@@ERROR`), `CurrentDatabase` / `CurrentTransaction`, per-session `TempTables` (`#foo`, cleared on Dispose), `NestingLevel` (cap 32), `Spid` (≥51), `SessionIsolationLevel`, `LockTimeoutMillis`, `CurrentExecutingThreadId` (same-thread-deadlock detection). Full roster in the source XML docs.
7070
- **`BatchContext`** (internal, `Parser/`) = one command execution. Owns the `ParserContext` (parse-time scratch) + batch-lifetime runtime state: `Variables`, `TableVariables` (`@t`), `CurrentUndoLog`, `CurrentTableVarUndoLog` (statement-only, disjoint from the tx-scoped log so `ROLLBACK TRAN` skips `@t`), `UdfFrame` / `ProcFrame` (non-null in a UDF/proc body — gates value-form `RETURN`). Exposes the **resolver contract** the parser depends on:
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
using Microsoft.Data.SqlClient;
2+
using static Microsoft.VisualStudio.TestTools.UnitTesting.Assert;
3+
4+
namespace SqlServerSimulator;
5+
6+
/// <summary>
7+
/// Loopback-wire oracle for the SSMS server-level Policy Health connect path:
8+
/// real SqlClient reads <c>has_dbaccess('msdb')</c> = 1 and then the empty
9+
/// <c>msdb.dbo.syspolicy_system_health_state</c> view. This is the exact
10+
/// sequence SSMS issues at connect; before the msdb seed it popped a
11+
/// permission error. The wire oracle catches TDS-level regressions the
12+
/// in-process tests can't.
13+
/// </summary>
14+
[TestClass]
15+
public sealed class SystemDatabaseTests
16+
{
17+
public TestContext TestContext { get; set; } = null!;
18+
19+
[TestMethod]
20+
public async Task HasDbAccessMsdb_OverWire_ReturnsOne()
21+
{
22+
var simulation = new Simulation();
23+
await using var listener = await simulation.ListenAsync(0, TestContext.CancellationToken);
24+
await using var connection = await Wire.OpenAsync(listener, TestContext.CancellationToken);
25+
26+
await using var command = new SqlCommand("select has_dbaccess('msdb')", connection);
27+
AreEqual(1, await command.ExecuteScalarAsync(TestContext.CancellationToken));
28+
}
29+
30+
[TestMethod]
31+
public async Task SyspolicyHealthState_OverWire_ReturnsSixColumnsNoRows()
32+
{
33+
var simulation = new Simulation();
34+
await using var listener = await simulation.ListenAsync(0, TestContext.CancellationToken);
35+
await using var connection = await Wire.OpenAsync(listener, TestContext.CancellationToken);
36+
37+
await using var command = new SqlCommand(
38+
"select * from msdb.dbo.syspolicy_system_health_state", connection);
39+
await using var reader = await command.ExecuteReaderAsync(TestContext.CancellationToken);
40+
41+
AreEqual(6, reader.FieldCount);
42+
AreEqual("health_state_id", reader.GetName(0));
43+
AreEqual("policy_id", reader.GetName(1));
44+
AreEqual("last_run_date", reader.GetName(2));
45+
AreEqual("target_query_expression_with_id", reader.GetName(3));
46+
AreEqual("target_query_expression", reader.GetName(4));
47+
AreEqual("result", reader.GetName(5));
48+
IsFalse(await reader.ReadAsync(TestContext.CancellationToken));
49+
}
50+
}

SqlServerSimulator.Tests/Bacpac/BacpacImportTests.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,9 @@ public void ImportBacpac_MultipleBacpacs_AllLandAsSeparateDatabases()
8282
IsTrue(SimulationHasDatabase(sim, "sales"));
8383
IsTrue(SimulationHasDatabase(sim, "ops"));
8484
IsFalse(SimulationHasDatabase(sim, "simulated"));
85-
// Three rows: the always-present master system database plus the two
86-
// imported user databases.
87-
AreEqual(3, sim.ExecuteScalar("SELECT COUNT(*) FROM sys.databases"));
85+
// Six rows: the four always-present system databases (master / tempdb
86+
// / model / msdb) plus the two imported user databases.
87+
AreEqual(6, sim.ExecuteScalar("SELECT COUNT(*) FROM sys.databases"));
8888
// Both imported databases are queryable via cross-database 3-part
8989
// names (CrossDatabaseTests covers USE + joins + DML rejection in
9090
// depth; here we just confirm the per-database row count surfaces).

SqlServerSimulator.Tests/DbIdNameTests.cs

Lines changed: 5 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ namespace SqlServerSimulator;
55
/// <summary>
66
/// Tests for <c>DB_ID([name])</c> and <c>DB_NAME([id])</c>: round-trip the
77
/// simulator's database list through the id allocation that
8-
/// <c>sys.databases.database_id</c> uses — <c>master</c> is always 1, user
9-
/// databases take 5, 6, … in case-insensitive name order (real SQL Server
10-
/// reserves 2-4 for tempdb / model / msdb, unmodeled here). NULL argument /
11-
/// unknown name / unknown id all return NULL.
8+
/// <c>sys.databases.database_id</c> uses — the four system databases carry
9+
/// their fixed reserved ids (master = 1, tempdb = 2, model = 3, msdb = 4) and
10+
/// user databases take 5, 6, … in case-insensitive name order (system-database
11+
/// coverage lives in <c>SystemDatabaseTests</c>). NULL argument / unknown name
12+
/// / unknown id all return NULL.
1213
/// </summary>
1314
[TestClass]
1415
public sealed class DbIdNameTests
@@ -60,38 +61,4 @@ public void DbName_NullArg_ReturnsNull()
6061
[TestMethod]
6162
public void DbId_DbName_RoundTrip()
6263
=> AreEqual("simulated", new Simulation().ExecuteScalar("select db_name(db_id())"));
63-
64-
[TestMethod]
65-
public void HasDbAccess_HostedDatabase_Returns1()
66-
=> AreEqual(1, new Simulation().ExecuteScalar("select has_dbaccess('master')"));
67-
68-
[TestMethod]
69-
public void HasDbAccess_CaseInsensitive()
70-
=> AreEqual(1, new Simulation().ExecuteScalar("select has_dbaccess('SiMuLaTeD')"));
71-
72-
// SSMS probes msdb at connect to decide whether to surface Agent
73-
// features; the simulator doesn't model msdb, so NULL — same as a real
74-
// server where the login can't see it.
75-
[TestMethod]
76-
public void HasDbAccess_UnknownDatabase_ReturnsNull()
77-
=> AreEqual(DBNull.Value, new Simulation().ExecuteScalar("select has_dbaccess('msdb')"));
78-
79-
[TestMethod]
80-
public void HasDbAccess_EmptyOrNullName_ReturnsNull()
81-
{
82-
var simulation = new Simulation();
83-
AreEqual(DBNull.Value, simulation.ExecuteScalar("select has_dbaccess('')"));
84-
AreEqual(DBNull.Value, simulation.ExecuteScalar("select has_dbaccess(null)"));
85-
}
86-
87-
[TestMethod]
88-
public void HasDbAccess_NoArgument_RaisesMsg174()
89-
{
90-
var ex = new Simulation().AssertSqlError("select has_dbaccess()", 174);
91-
AreEqual("The has_dbaccess function requires 1 argument(s).", ex.Message);
92-
}
93-
94-
[TestMethod]
95-
public void HasDbAccess_VariableArgument_Resolves()
96-
=> AreEqual(1, new Simulation().ExecuteScalar("declare @n sysname = 'master' select has_dbaccess(@n)"));
9764
}

SqlServerSimulator.Tests/MasterDatabaseTests.cs

Lines changed: 0 additions & 55 deletions
This file was deleted.

0 commit comments

Comments
 (0)