|
| 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 | +} |
0 commit comments