-
Notifications
You must be signed in to change notification settings - Fork 9.1k
Create SMO samples project #558
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| # SmoSamples | ||
|
|
||
| This unit test project is meant to demonstrate features of the Sql Management Objects framework and to help developers optimize performance of their SMO-based applications. | ||
|
|
||
|
|
||
| ### Contents | ||
|
|
||
| [About this sample](#about-this-sample)<br/> | ||
| [Before you begin](#before-you-begin)<br/> | ||
| [Run this sample](#run-this-sample)<br/> | ||
| [Sample details](#sample-details)<br/> | ||
| [Disclaimers](#disclaimers)<br/> | ||
| [Related links](#related-links)<br/> | ||
|
|
||
|
|
||
| <a name=about-this-sample></a> | ||
|
|
||
| ## About this sample | ||
|
|
||
| <!-- Delete the ones that don't apply --> | ||
| - **Applies to:** SQL Server 2016 (or higher), Azure SQL Database, Azure SQL Data Warehouse | ||
| - **Key features:** | ||
| - Unit tests and a docker file that demonstrate proper use of SMO features against a working SQL Server instance. | ||
| - **Programming Language:** | ||
| - C# | ||
|
|
||
| <a name=before-you-begin></a> | ||
|
|
||
| ## Before you begin | ||
|
|
||
| To run this sample, you need the following prerequisites. | ||
|
|
||
| **Software prerequisites:** | ||
|
|
||
| 1. SQL Server 2016 (or higher) or an Azure SQL Database with the full WideWorldImporters sample database, or | ||
| 2. Docker | ||
| 3. At minimum the dotnet 2.2 SDK, or Visual Studio 2017 | ||
|
|
||
| <a name=run-this-sample></a> | ||
|
|
||
| ## Run this sample | ||
| If using Docker, use runtests.sh or runtests.cmd as appropriate. If using a separate instance of SQL Server or Azure SQL Database, create a .runsettings file and run the unit tests using Visual Studio or "dotnet vstest". | ||
|
|
||
| <a name=sample-details></a> | ||
|
|
||
| ## Sample details | ||
|
|
||
| Each unit test demonstrates a specific aspect of SMO-based application development, either in isolation or in conjunction with other SMO components. <br/> | ||
| Feature areas tested include: | ||
| 1. Efficient use of collections | ||
| 2. Sql query capture | ||
| 3. Events | ||
| 4. URNs | ||
| 5. Script generation | ||
|
|
||
|
|
||
| <a name=related-links></a> | ||
|
|
||
| ## Related Links | ||
| The SMO NuGet package is at https://www.nuget.org/packages/Microsoft.SqlServer.SqlManagementObjects/ <br/> | ||
| Documentation for the APIs is at https://docs.microsoft.com/sql/relational-databases/server-management-objects-smo/overview-smo<br/> | ||
| The WideWorldImporters sample database can be found at https://github.com/Microsoft/sql-server-samples/releases/download/wide-world-importers-v1.0/WideWorldImporters-Full.bak <br/> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| FROM mcr.microsoft.com/mssql/server:2017-latest | ||
| WORKDIR /tmp/backup | ||
| RUN wget -q https://github.com/Microsoft/sql-server-samples/releases/download/wide-world-importers-v1.0/WideWorldImporters-Full.bak | ||
| COPY restore.sql . | ||
| COPY restore.sh . | ||
| COPY entrypoint.sh . | ||
| CMD ["/bin/bash", "/tmp/backup/entrypoint.sh"] | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| /opt/mssql/bin/sqlservr & /tmp/backup/restore.sh | ||
| tail -f /dev/null |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| # Wait for SQL Server to start and be ready to accept connections | ||
| sleep 35s | ||
| echo sa_password is $SA_PASSWORD | ||
| /opt/mssql-tools/bin/sqlcmd -S . -U sa -P $SA_PASSWORD -i /tmp/backup/restore.sql | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| RESTORE DATABASE WideWorldImporters FROM DISK = "/tmp/backup/WideWorldImporters-Full.bak" | ||
| WITH MOVE "WWI_Primary" TO "/var/opt/mssql/data/WideWorldImporters.mdf", | ||
| MOVE "WWI_Userdata" TO "/var/opt/mssql/data/WideWorldImporters_UserData.ndf", | ||
| MOVE "WWI_Log" TO "/var/opt/mssql/data/WideWorldImporters.ldf", MOVE "WWI_InMemory_Data_1" | ||
| TO "/var/opt/mssql/data/WideWorldImporters_InMemory_Data_1" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| @echo off | ||
| set pwd=Passwd__%random% | ||
| echo Building the SQL Linux Docker container | ||
| docker pull mcr.microsoft.com/mssql/server:2017-latest | ||
| docker build -t sqllinux prep | ||
| echo Running the SQL linux docker image | ||
| start cmd /k docker run -e "ACCEPT_EULA=Y" -e "SA_PASSWORD=%pwd%" -e "MSSQL_SA_PASSWORD=%pwd%" -h sqlserver --name sqlserver -p:1433:1433 --rm sqllinux | ||
| echo Waiting 90 seconds for SQL server to restore WideWorldImporters | ||
| timeout /t 90 | ||
| setlocal | ||
| echo running tests against SQL 2017 database WideWorldImporters | ||
| set TEST_PASSWORD=%pwd% | ||
| dotnet publish src -o out | ||
| dotnet vstest src\out\SmoSamples.dll /logger:console /settings:src\localhost.runsettings | ||
| endlocal | ||
| echo Terminating docker container | ||
| docker kill sqlserver |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| pwd=Pwd$RANDOM | ||
| echo Building the SQL Linux Docker container | ||
| docker pull mcr.microsoft.com/mssql/server:2017-latest | ||
| docker build -t sqllinux prep | ||
| echo Running the SQL linux docker image | ||
| docker run -e ACCEPT_EULA=Y -e SA_PASSWORD=$pwd -e MSSQL_SA_PASSWORD=$pwd -h sqlserver --name sqlserver -p:1433:1433 -d --rm sqllinux | ||
| echo Waiting 2 minutes for SQL server to restore WideWorldImporters | ||
| sleep 120 | ||
| echo running tests against SQL 2017 database WideWorldImporters | ||
| export TEST_PASSWORD=$pwd | ||
| dotnet publish src | ||
| dotnet vstest src/bin/Debug/netcoreapp2.1/SmoSamples.dll --logger:console --Settings:src/localhost.runsettings | ||
| echo Terminating docker container | ||
| docker kill sqlserver |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| namespace Microsoft.SqlServer.SmoSamples | ||
| { | ||
| using System; | ||
|
shueybubbles marked this conversation as resolved.
|
||
| using System.Collections.Generic; | ||
| using System.Diagnostics; | ||
| using System.Text; | ||
| using Microsoft.SqlServer.Management.Smo; | ||
| using Microsoft.VisualStudio.TestTools.UnitTesting; | ||
| using NUnit.Framework; | ||
| using Assert = NUnit.Framework.Assert; | ||
|
|
||
| [TestClass] | ||
| public class CollectionSamples | ||
| { | ||
| public VisualStudio.TestTools.UnitTesting.TestContext TestContext { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// SetDefaultInitFields tells the Server object which properties to include in the initial query | ||
| /// to populate of a given object type when initialized a collection of that type. | ||
| /// The test demonstrates the effect of using this call to enumerate Tables when accessing the FileGroup | ||
| /// property of each Table object | ||
| /// </summary> | ||
| [TestMethod] | ||
| public void Collection_iteration_is_faster_with_SetDefaultInitFields() | ||
|
shueybubbles marked this conversation as resolved.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So overall I like the idea of having actual examples users can run to see the benefits. But it'd definitely be great to also explain how this is actionable for them - ideally every test would have an explanation of what it's demonstrating and how users could use it in their own code. |
||
| { | ||
| using (var connectionMetrics = ConnectionMetrics.SetupMeasuredConnection(TestContext, 50)) | ||
| { | ||
| var server = new Management.Smo.Server(connectionMetrics.ServerConnection); | ||
| var database = server.Databases[TestContext.GetTestDatabaseName()]; | ||
| connectionMetrics.Reset(); | ||
| foreach (Table table in database.Tables) | ||
| { | ||
| // Accessing FileGroup triggers a query to fetch it | ||
| Trace.TraceInformation( | ||
| $"Unoptimized table Name: {table.Name}\tSchema:{table.Schema}\tFileGroup:{table.FileGroup}"); | ||
| } | ||
|
|
||
| var unoptimizedMetrics = (QueryCount: connectionMetrics.QueryCount, | ||
| BytesSent: connectionMetrics.BytesSent, BytesRead: connectionMetrics.BytesRead, | ||
| ConnectionCount: connectionMetrics.ConnectionCount); | ||
| Trace.TraceInformation(string.Join($"{Environment.NewLine}\t", new[] | ||
| { | ||
| "Unoptimized metrics:", | ||
| $"QueryCount:{unoptimizedMetrics.QueryCount}", $"ConnectionCount:{unoptimizedMetrics.ConnectionCount}", | ||
| $"BytesSent:{unoptimizedMetrics.BytesSent}", $"BytesRead:{unoptimizedMetrics.BytesRead}" | ||
| })); | ||
|
|
||
| connectionMetrics.Reset(); | ||
| server.SetDefaultInitFields(typeof(Table), "Name", "Schema", "FileGroup"); | ||
| database.Tables.Refresh(); | ||
| foreach (Table table in database.Tables) | ||
| { | ||
| // The FileGroup property is already populated, so no extra query is needed | ||
| Trace.TraceInformation( | ||
| $"Optimized table Name: {table.Name}\tSchema:{table.Schema}\tFileGroup:{table.FileGroup}"); | ||
| } | ||
|
|
||
| var optimizedMetrics = (QueryCount: connectionMetrics.QueryCount, | ||
| BytesSent: connectionMetrics.BytesSent, BytesRead: connectionMetrics.BytesRead, | ||
| ConnectionCount: connectionMetrics.ConnectionCount); | ||
| Trace.TraceInformation(string.Join($"{Environment.NewLine}\t", new[] | ||
| { | ||
| "Optimized Metrics:", | ||
| $"QueryCount:{optimizedMetrics.QueryCount}", $"ConnectionCount:{optimizedMetrics.ConnectionCount}", | ||
| $"BytesSent:{optimizedMetrics.BytesSent}", $"BytesRead:{optimizedMetrics.BytesRead}" | ||
| })); | ||
| Assert.That(optimizedMetrics.BytesRead, Is.LessThan(unoptimizedMetrics.BytesRead), "BytesRead"); | ||
| Assert.That(optimizedMetrics.BytesSent, Is.LessThan(unoptimizedMetrics.BytesSent), "BytesSent"); | ||
| Assert.That(optimizedMetrics.ConnectionCount, Is.AtMost(unoptimizedMetrics.ConnectionCount), "ConnectionCount"); | ||
| Assert.That(optimizedMetrics.QueryCount, Is.LessThan(unoptimizedMetrics.QueryCount), "QueryCount"); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,150 @@ | ||
| using Microsoft.SqlServer.Management.Common; | ||
| using Microsoft.SqlServer.Management.Smo; | ||
| using Microsoft.VisualStudio.TestTools.UnitTesting; | ||
| using NUnit.Framework; | ||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.Data.SqlClient; | ||
| using System.Diagnostics; | ||
| using System.Reflection; | ||
| using System.Text; | ||
| using Assert = NUnit.Framework.Assert; | ||
| namespace Microsoft.SqlServer.SmoSamples | ||
| { | ||
| /// <summary> | ||
| /// Used by test classes to initialize and retrieve a ServerConnection for use in the tests themselves | ||
| /// </summary> | ||
| static class ConnectionHelpers | ||
| { | ||
|
|
||
| /// <summary> | ||
| /// Returns a ServerConnection based on the connectionString parameter defined in the runsettings file | ||
| /// </summary> | ||
| public static ServerConnection GetTestConnection(this VisualStudio.TestTools.UnitTesting.TestContext context, ConnectionType connectionType = ConnectionType.Default) | ||
| { | ||
| var connectionString = context.GetConnectionString(); | ||
| var connectionStrBuilder = new SqlConnectionStringBuilder(connectionString); | ||
| var instanceName = connectionStrBuilder.DataSource; | ||
| var sqlServerLogin = connectionStrBuilder.UserID; | ||
| var password = connectionStrBuilder.Password; | ||
| if (connectionType == ConnectionType.SqlConnection) | ||
| { | ||
| return new ServerConnection(new SqlConnection(connectionString)); | ||
| } | ||
| if (connectionType == ConnectionType.Integrated) | ||
| { | ||
| return new ServerConnection(instanceName); | ||
| } | ||
| if (connectionType == ConnectionType.SqlAuth ) | ||
| { | ||
| if (string.IsNullOrWhiteSpace(sqlServerLogin) || string.IsNullOrWhiteSpace(password)) | ||
| { | ||
| throw new ArgumentException("username and password values are missing from test connection string"); | ||
| } | ||
| return new ServerConnection(instanceName, sqlServerLogin, password); | ||
| } | ||
| if (string.IsNullOrEmpty(sqlServerLogin)) | ||
| { | ||
| return new ServerConnection(instanceName); | ||
| } | ||
| return new ServerConnection(instanceName, sqlServerLogin, password); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Returns a connection string based on the connectionString parameter defined in the runsettings file | ||
| /// Placeholders may be included in the connection string if the caller has set corresponding environment variables. | ||
| /// [hostname] -> TEST_HOSTNAME environment variable | ||
| /// [username] -> TEST_USERNAME | ||
| /// [password] -> TEST_PASSWORD | ||
| /// [database] -> TEST_DATABASE | ||
| /// </summary> | ||
| public static string GetConnectionString(this VisualStudio.TestTools.UnitTesting.TestContext context) | ||
| { | ||
| var connectionString = context.Properties["connectionString"].ToString(); | ||
| Assert.That(connectionString, Is.Not.Empty, "connectionString must be set"); | ||
| connectionString = connectionString.Replace("[hostname]", Environment.GetEnvironmentVariable("TEST_HOSTNAME")). | ||
| Replace("[username]", Environment.GetEnvironmentVariable("TEST_USERNAME")). | ||
| Replace("[password]", Environment.GetEnvironmentVariable("TEST_PASSWORD")). | ||
| Replace("[database]", Environment.GetEnvironmentVariable("TEST_DATABASE")); | ||
| Console.WriteLine("Connection string: {0}", connectionString); | ||
| return connectionString; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Returns the name of the database to use for the tests | ||
| /// If TEST_DATABASE environment variable is set, that value is used, otherwise the | ||
| /// testDatabase parameter from runsettings is used. | ||
| /// </summary> | ||
| /// <returns></returns> | ||
| public static string GetTestDatabaseName(this VisualStudio.TestTools.UnitTesting.TestContext context) | ||
| { | ||
| var databaseName = Environment.GetEnvironmentVariable("TEST_DATABASE"); | ||
| if (string.IsNullOrEmpty(databaseName)) | ||
| { | ||
| databaseName = context.Properties["testDatabase"].ToString(); | ||
| } | ||
| Assert.That(databaseName, Is.Not.Empty, "testDatabase must be set"); | ||
| Console.WriteLine("Test database: {0}", databaseName); | ||
| return databaseName; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Returns the folder where result files should be written | ||
| /// </summary> | ||
| /// <param name="context"></param> | ||
| /// <returns></returns> | ||
| public static string GetResultsFolder(this VisualStudio.TestTools.UnitTesting.TestContext context) | ||
| { | ||
| var path = Environment.GetEnvironmentVariable("RESULTS_FOLDER"); | ||
| if (string.IsNullOrEmpty(path)) | ||
| { | ||
| path = context.Properties.ContainsKey("resultsFolder") ? context.Properties["resultsFolder"].ToString() : null; | ||
| } | ||
| if (string.IsNullOrEmpty(path)) | ||
| { | ||
| path = PathWrapper.GetDirectoryName(Assembly.GetExecutingAssembly().Location); | ||
| path = PathWrapper.Combine(path, "results"); | ||
| } | ||
| return path; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// creates a new database with a random name, runs the action, and drops the database | ||
| /// </summary> | ||
| /// <param name="context"></param> | ||
| /// <param name="action"></param> | ||
| /// <param name="preCreateAction"></param> | ||
| public static void ExecuteWithDbDrop(this VisualStudio.TestTools.UnitTesting.TestContext context, Action<Database> action, Action<Database> preCreateAction = null) | ||
| { | ||
| var dbName = string.Format("{0}{1}", context.TestName, new Random().Next()); | ||
| var serverConnection = context.GetTestConnection(); | ||
| var server = new Management.Smo.Server(serverConnection); | ||
| var database = new Database(server, dbName); | ||
| preCreateAction?.Invoke(database); | ||
| database.Create(); | ||
| try | ||
| { | ||
| action(database); | ||
| } | ||
| finally | ||
| { | ||
| try | ||
| { | ||
| database.Drop(); | ||
| } | ||
| catch (Exception e) | ||
| { | ||
| Trace.TraceError("Unable to drop database {0}: {1}", dbName, e); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| enum ConnectionType | ||
| { | ||
| Default, // whatever is specified in the config | ||
| Integrated, // integrated auth | ||
| SqlAuth, // SQL auth | ||
| SqlConnection // Create a SqlConnection first from the connection string | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.