From 9d0d4b0caed359d5168906345fc15c0e2f385ca5 Mon Sep 17 00:00:00 2001 From: Cory Rivera Date: Thu, 22 Jun 2023 17:28:41 -0700 Subject: [PATCH] Enable Detach Database in database handler (#2110) --- .../Contracts/DetachDatabase.cs | 40 ++++ .../ObjectManagementService.cs | 8 + .../ObjectTypes/Database/DatabaseHandler.cs | 171 +++++++++++++++--- .../ObjectTypes/Database/DatabaseViewInfo.cs | 9 + .../ObjectManagement/DatabaseHandlerTests.cs | 136 +++++++++++++- 5 files changed, 332 insertions(+), 32 deletions(-) create mode 100644 src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/Contracts/DetachDatabase.cs diff --git a/src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/Contracts/DetachDatabase.cs b/src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/Contracts/DetachDatabase.cs new file mode 100644 index 00000000..0e063253 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/Contracts/DetachDatabase.cs @@ -0,0 +1,40 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +#nullable disable +using Microsoft.SqlTools.Hosting.Protocol.Contracts; +using Microsoft.SqlTools.Utility; + +namespace Microsoft.SqlTools.ServiceLayer.ObjectManagement.Contracts +{ + public class DetachDatabaseRequestParams : GeneralRequestDetails + { + /// + /// SFC (SMO) URN identifying the object + /// + public string ObjectUrn { get; set; } + /// + /// URI of the underlying connection for this request + /// + public string ConnectionUri { get; set; } + /// + /// Whether to drop active connections to this database + /// + public bool DropConnections { get; set; } + /// + /// Whether to update the optimization statistics related to this database + /// + public bool UpdateStatistics { get; set; } + /// + /// Whether to generate a TSQL script for the operation instead of detaching the database + /// + public bool GenerateScript { get; set; } + } + + public class DetachDatabaseRequest + { + public static readonly RequestType Type = RequestType.Create("objectManagement/detachDatabase"); + } +} \ No newline at end of file diff --git a/src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/ObjectManagementService.cs b/src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/ObjectManagementService.cs index 71043b7e..979b9457 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/ObjectManagementService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/ObjectManagementService.cs @@ -68,6 +68,7 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectManagement this.serviceHost.SetRequestHandler(ScriptObjectRequest.Type, HandleScriptObjectRequest, true); this.serviceHost.SetRequestHandler(DisposeViewRequest.Type, HandleDisposeViewRequest, true); this.serviceHost.SetRequestHandler(SearchRequest.Type, HandleSearchRequest, true); + this.serviceHost.SetRequestHandler(DetachDatabaseRequest.Type, HandleDetachDatabaseRequest, true); } internal async Task HandleRenameRequest(RenameRequestParams requestParams, RequestContext requestContext) @@ -197,6 +198,13 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectManagement await requestContext.SendResult(res.ToArray()); } + internal async Task HandleDetachDatabaseRequest(DetachDatabaseRequestParams requestParams, RequestContext requestContext) + { + var handler = this.GetObjectTypeHandler(SqlObjectType.Database) as DatabaseHandler; + var sqlScript = handler.Detach(requestParams); + await requestContext.SendResult(sqlScript); + } + private IObjectTypeHandler GetObjectTypeHandler(SqlObjectType objectType) { foreach (var handler in objectTypeHandlers) diff --git a/src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/ObjectTypes/Database/DatabaseHandler.cs b/src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/ObjectTypes/Database/DatabaseHandler.cs index ef4b2293..ffd27eb4 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/ObjectTypes/Database/DatabaseHandler.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/ObjectTypes/Database/DatabaseHandler.cs @@ -9,7 +9,6 @@ using System.Data; using System.Linq; using System.Threading.Tasks; using Microsoft.SqlServer.Management.Common; -using Microsoft.SqlServer.Management.Sdk.Sfc; using Microsoft.SqlServer.Management.Smo; using Microsoft.SqlTools.ServiceLayer.Admin; using static Microsoft.SqlTools.ServiceLayer.Admin.AzureSqlDbHelper; @@ -18,6 +17,9 @@ using Microsoft.SqlTools.ServiceLayer.Management; using Microsoft.SqlTools.ServiceLayer.ObjectManagement.Contracts; using Microsoft.SqlTools.ServiceLayer.Utility; using Microsoft.SqlTools.Utility; +using System.Text; +using System.IO; +using Microsoft.SqlTools.ServiceLayer.Utility.SqlScriptFormatters; namespace Microsoft.SqlTools.ServiceLayer.ObjectManagement { @@ -98,9 +100,7 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectManagement public override Task InitializeObjectView(InitializeViewRequestParams requestParams) { // create a default data context and database object - ConfigAction configAction = requestParams.IsNewObject ? ConfigAction.Create : ConfigAction.Update; - var databaseInfo = !requestParams.IsNewObject ? new DatabaseInfo() { Name = requestParams.Database } : null; - using (var dataContainer = CreateDatabaseDataContainer(requestParams, configAction, databaseInfo)) + using (var dataContainer = CreateDatabaseDataContainer(requestParams.ConnectionUri, requestParams.ObjectUrn, requestParams.IsNewObject)) { if (dataContainer.Server == null) { @@ -126,21 +126,24 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectManagement if (!requestParams.IsNewObject) { var smoDatabase = dataContainer.SqlDialogSubject as Database; - databaseViewInfo.ObjectInfo = new DatabaseInfo() + if (smoDatabase != null) { - Name = smoDatabase.Name, - CollationName = smoDatabase.Collation, - DateCreated = smoDatabase.CreateDate.ToString(), - LastDatabaseBackup = smoDatabase.LastBackupDate == DateTime.MinValue ? SR.databaseBackupDate_None : smoDatabase.LastBackupDate.ToString(), - LastDatabaseLogBackup = smoDatabase.LastLogBackupDate == DateTime.MinValue ? SR.databaseBackupDate_None : smoDatabase.LastLogBackupDate.ToString(), - MemoryAllocatedToMemoryOptimizedObjectsInMb = DatabaseUtils.ConvertKbtoMb(smoDatabase.MemoryAllocatedToMemoryOptimizedObjectsInKB), - MemoryUsedByMemoryOptimizedObjectsInMb = DatabaseUtils.ConvertKbtoMb(smoDatabase.MemoryUsedByMemoryOptimizedObjectsInKB), - NumberOfUsers = smoDatabase.Users.Count, - Owner = smoDatabase.Owner, - SizeInMb = smoDatabase.Size, - SpaceAvailableInMb = DatabaseUtils.ConvertKbtoMb(smoDatabase.SpaceAvailable), - Status = smoDatabase.Status.ToString() - }; + databaseViewInfo.ObjectInfo = new DatabaseInfo() + { + Name = smoDatabase.Name, + CollationName = smoDatabase.Collation, + DateCreated = smoDatabase.CreateDate.ToString(), + LastDatabaseBackup = smoDatabase.LastBackupDate == DateTime.MinValue ? SR.databaseBackupDate_None : smoDatabase.LastBackupDate.ToString(), + LastDatabaseLogBackup = smoDatabase.LastLogBackupDate == DateTime.MinValue ? SR.databaseBackupDate_None : smoDatabase.LastLogBackupDate.ToString(), + MemoryAllocatedToMemoryOptimizedObjectsInMb = DatabaseUtils.ConvertKbtoMb(smoDatabase.MemoryAllocatedToMemoryOptimizedObjectsInKB), + MemoryUsedByMemoryOptimizedObjectsInMb = DatabaseUtils.ConvertKbtoMb(smoDatabase.MemoryUsedByMemoryOptimizedObjectsInKB), + NumberOfUsers = smoDatabase.Users.Count, + Owner = smoDatabase.Owner, + SizeInMb = smoDatabase.Size, + SpaceAvailableInMb = DatabaseUtils.ConvertKbtoMb(smoDatabase.SpaceAvailable), + Status = smoDatabase.Status.ToString() + }; + } } // azure sql db doesn't have a sysadmin fixed role @@ -184,6 +187,14 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectManagement // These aren't included when the target DB is on Azure so only populate if it's not an Azure DB databaseViewInfo.RecoveryModels = GetRecoveryModels(dataContainer.Server, prototype); databaseViewInfo.ContainmentTypes = GetContainmentTypes(dataContainer.Server, prototype); + if (!requestParams.IsNewObject) + { + var smoDatabase = dataContainer.SqlDialogSubject as Database; + if (smoDatabase != null) + { + databaseViewInfo.Files = GetDatabaseFiles(smoDatabase); + } + } } // Skip adding logins if running against an Azure SQL DB @@ -240,30 +251,105 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectManagement return Task.FromResult(script); } - private CDataContainer CreateDatabaseDataContainer(InitializeViewRequestParams requestParams, ConfigAction configAction, DatabaseInfo? database = null) + /// + /// Used to detach the specified database from a server. + /// + /// The various parameters needed for the Detach operation + public string Detach(DetachDatabaseRequestParams detachParams) { - ConnectionInfo connectionInfo = this.GetConnectionInfo(requestParams.ConnectionUri); - CDataContainer dataContainer = CDataContainer.CreateDataContainer(connectionInfo, databaseExists: configAction != ConfigAction.Create); + var sqlScript = string.Empty; + ConnectionInfo connectionInfo = this.GetConnectionInfo(detachParams.ConnectionUri); + using (var dataContainer = CreateDatabaseDataContainer(detachParams.ConnectionUri, detachParams.ObjectUrn, false)) + { + var smoDatabase = dataContainer.SqlDialogSubject as Database; + if (smoDatabase != null) + { + if (detachParams.GenerateScript) + { + sqlScript = CreateDetachScript(detachParams, smoDatabase.Name); + } + else + { + DatabaseUserAccess originalAccess = smoDatabase.DatabaseOptions.UserAccess; + try + { + // In order to drop all connections to the database, we switch it to single + // user access mode so that only our current connection to the database stays open. + // Any pending operations are terminated and rolled back. + if (detachParams.DropConnections) + { + smoDatabase.Parent.KillAllProcesses(smoDatabase.Name); + smoDatabase.DatabaseOptions.UserAccess = SqlServer.Management.Smo.DatabaseUserAccess.Single; + smoDatabase.Alter(TerminationClause.RollbackTransactionsImmediately); + } + smoDatabase.Parent.DetachDatabase(smoDatabase.Name, detachParams.UpdateStatistics); + } + catch (SmoException) + { + // Revert to database's previous user access level if we changed it as part of dropping connections + // before hitting this exception. + if (originalAccess != smoDatabase.DatabaseOptions.UserAccess) + { + smoDatabase.DatabaseOptions.UserAccess = originalAccess; + smoDatabase.Alter(TerminationClause.RollbackTransactionsImmediately); + } + throw; + } + } + } + else + { + throw new InvalidOperationException($"Provided URN '{detachParams.ObjectUrn}' did not correspond to an existing database."); + } + } + return sqlScript; + } + + private string CreateDetachScript(DetachDatabaseRequestParams detachParams, string databaseName) + { + var escapedName = ToSqlScript.FormatIdentifier(databaseName); + var builder = new StringBuilder(); + builder.AppendLine("USE [master]"); + builder.AppendLine("GO"); + if (detachParams.DropConnections) + { + builder.AppendLine($"ALTER DATABASE {escapedName} SET SINGLE_USER WITH ROLLBACK IMMEDIATE"); + builder.AppendLine("GO"); + } + builder.Append($"EXEC master.dbo.sp_detach_db @dbname = N'{databaseName}'"); + if (detachParams.UpdateStatistics) + { + builder.Append($", @skipchecks = 'false'"); + } + builder.AppendLine(); + builder.AppendLine("GO"); + return builder.ToString(); + } + + private CDataContainer CreateDatabaseDataContainer(string connectionUri, string? objectURN, bool isNewDatabase) + { + ConnectionInfo connectionInfo = this.GetConnectionInfo(connectionUri); + CDataContainer dataContainer = CDataContainer.CreateDataContainer(connectionInfo, databaseExists: !isNewDatabase); if (dataContainer.Server == null) { throw new InvalidOperationException(serverNotExistsError); } - string objectUrn = configAction != ConfigAction.Create && database != null - ? string.Format(System.Globalization.CultureInfo.InvariantCulture, "Server/Database[@Name='{0}']", Urn.EscapeString(database.Name)) - : string.Format(System.Globalization.CultureInfo.InvariantCulture, "Server"); - - dataContainer.SqlDialogSubject = dataContainer.Server.GetSmoObject(objectUrn); + if (string.IsNullOrEmpty(objectURN)) + { + objectURN = string.Format(System.Globalization.CultureInfo.InvariantCulture, "Server"); + } + dataContainer.SqlDialogSubject = dataContainer.Server.GetSmoObject(objectURN); return dataContainer; } - private string ConfigureDatabase(InitializeViewRequestParams requestParams, DatabaseInfo database, ConfigAction configAction, RunType runType) + private string ConfigureDatabase(InitializeViewRequestParams viewParams, DatabaseInfo database, ConfigAction configAction, RunType runType) { if (database.Name == null) { throw new ArgumentException("Database name not provided."); } - using (var dataContainer = CreateDatabaseDataContainer(requestParams, configAction, database)) + using (var dataContainer = CreateDatabaseDataContainer(viewParams.ConnectionUri, viewParams.ObjectUrn, viewParams.IsNewObject)) { if (dataContainer.Server == null) { @@ -522,6 +608,35 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectManagement return recoveryModels.ToArray(); } + private DatabaseFile[] GetDatabaseFiles(Database database) + { + var filesList = new List(); + foreach (FileGroup fileGroup in database.FileGroups) + { + foreach (DataFile file in fileGroup.Files) + { + filesList.Add(new DatabaseFile() + { + Name = file.Name, + Type = FileType.Data.ToString(), + Path = Path.GetDirectoryName(file.FileName), + FileGroup = fileGroup.Name + }); + } + } + foreach (LogFile file in database.LogFiles) + { + filesList.Add(new DatabaseFile() + { + Name = file.Name, + Type = FileType.Log.ToString(), + Path = Path.GetDirectoryName(file.FileName), + FileGroup = string.Empty + }); + } + return filesList.ToArray(); + } + /// /// Get supported database compatibility levels for this Azure server. /// diff --git a/src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/ObjectTypes/Database/DatabaseViewInfo.cs b/src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/ObjectTypes/Database/DatabaseViewInfo.cs index 089d1ca7..4a1d95be 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/ObjectTypes/Database/DatabaseViewInfo.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/ObjectTypes/Database/DatabaseViewInfo.cs @@ -14,6 +14,7 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectManagement public string[] CompatibilityLevels { get; set; } public string[] ContainmentTypes { get; set; } public string[] RecoveryModels { get; set; } + public DatabaseFile[] Files { get; set; } public bool IsAzureDB { get; set; } public string[] AzureBackupRedundancyLevels { get; set; } @@ -27,4 +28,12 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectManagement public string EditionDisplayName { get; set; } public string[] Details { get; set; } } + + public class DatabaseFile + { + public string Name { get; set; } + public string Type { get; set; } + public string Path { get; set; } + public string FileGroup { get; set; } + } } \ No newline at end of file diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/ObjectManagement/DatabaseHandlerTests.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/ObjectManagement/DatabaseHandlerTests.cs index 7728e529..4ae785b8 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/ObjectManagement/DatabaseHandlerTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/ObjectManagement/DatabaseHandlerTests.cs @@ -4,6 +4,7 @@ // using System.Collections.Generic; +using System.Collections.Specialized; using System.Linq; using System.Threading.Tasks; using Microsoft.Data.SqlClient; @@ -13,6 +14,7 @@ using Microsoft.SqlTools.ServiceLayer.Admin; using Microsoft.SqlTools.ServiceLayer.Connection; using Microsoft.SqlTools.ServiceLayer.IntegrationTests.Utility; using Microsoft.SqlTools.ServiceLayer.ObjectManagement; +using Microsoft.SqlTools.ServiceLayer.ObjectManagement.Contracts; using Microsoft.SqlTools.ServiceLayer.Test.Common; using NUnit.Framework; using static Microsoft.SqlTools.ServiceLayer.Admin.AzureSqlDbHelper; @@ -201,7 +203,7 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.ObjectManagement { AzureEdition.Hyperscale, "HS_Gen5_2" } }; Assert.That(actualLevelsMap.Count, Is.EqualTo(expectedDefaults.Count), "Did not get expected number of editions for DatabaseHandler's service levels"); - foreach(AzureEdition edition in expectedDefaults.Keys) + foreach (AzureEdition edition in expectedDefaults.Keys) { if (AzureSqlDbHelper.TryGetServiceObjectiveInfo(edition, out var expectedLevelInfo)) { @@ -211,7 +213,7 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.ObjectManagement var expectedDefaultIndex = expectedLevelInfo.Key; var expectedDefault = expectedServiceLevels[expectedDefaultIndex]; - var actualDefault = actualServiceLevels[0]; + var actualDefault = actualServiceLevels[0]; Assert.That(actualDefault, Is.EqualTo(expectedDefault), "Did not get expected default SLO for edition '{0}'", edition.DisplayName); } else @@ -240,7 +242,7 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.ObjectManagement { AzureEdition.Hyperscale, "0MB" } }; Assert.That(actualSizesMap.Count, Is.EqualTo(expectedDefaults.Count), "Did not get expected number of editions for DatabaseHandler's max sizes"); - foreach(AzureEdition edition in expectedDefaults.Keys) + foreach (AzureEdition edition in expectedDefaults.Keys) { if (AzureSqlDbHelper.TryGetDatabaseSizeInfo(edition, out var expectedSizeInfo)) { @@ -250,7 +252,7 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.ObjectManagement var expectedDefaultIndex = expectedSizeInfo.Key; var expectedDefault = expectedSizes[expectedDefaultIndex]; - var actualDefault = actualSizes[0]; + var actualDefault = actualSizes[0]; Assert.That(actualDefault, Is.EqualTo(expectedDefault.ToString()), "Did not get expected default size for edition '{0}'", edition.DisplayName); } else @@ -304,6 +306,132 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.ObjectManagement } } + [Test] + public async Task DetachDatabaseTest() + { + var connectionResult = await LiveConnectionHelper.InitLiveConnectionInfoAsync("master"); + using (SqlConnection sqlConn = ConnectionService.OpenSqlConnection(connectionResult.ConnectionInfo)) + { + var server = new Server(new ServerConnection(sqlConn)); + + var testDatabase = ObjectManagementTestUtils.GetTestDatabaseInfo(); + var objUrn = ObjectManagementTestUtils.GetDatabaseURN(testDatabase.Name); + await ObjectManagementTestUtils.DropObject(connectionResult.ConnectionInfo.OwnerUri, objUrn); + + try + { + // Create database to test with + var parametersForCreation = ObjectManagementTestUtils.GetInitializeViewRequestParams(connectionResult.ConnectionInfo.OwnerUri, "master", true, SqlObjectType.Database, "", ""); + await ObjectManagementTestUtils.SaveObject(parametersForCreation, testDatabase); + Assert.That(DatabaseExists(testDatabase.Name!, server), $"Expected database '{testDatabase.Name}' was not created succesfully"); + + var handler = new DatabaseHandler(ConnectionService.Instance); + var connectionUri = connectionResult.ConnectionInfo.OwnerUri; + + var detachParams = new DetachDatabaseRequestParams() + { + ConnectionUri = connectionUri, + ObjectUrn = objUrn, + DropConnections = true, + UpdateStatistics = true, + GenerateScript = false + }; + + // Get databases's files so we can reattach it later before dropping it + var fileCollection = new StringCollection(); + var smoDatabase = server.GetSmoObject(objUrn) as Database; + foreach (FileGroup fileGroup in smoDatabase!.FileGroups) + { + foreach (DataFile file in fileGroup.Files) + { + fileCollection.Add(file.FileName); + } + } + foreach (LogFile file in smoDatabase.LogFiles) + { + fileCollection.Add(file.FileName); + } + + var script = handler.Detach(detachParams); + Assert.That(script, Is.Empty, "Should only return an empty string if GenerateScript is false"); + + server.Databases.Refresh(); + Assert.That(DatabaseExists(testDatabase.Name!, server), Is.False, $"Expected database '{testDatabase.Name}' was not detached succesfully"); + + server.AttachDatabase(testDatabase.Name, fileCollection); + + server.Databases.Refresh(); + Assert.That(DatabaseExists(testDatabase.Name!, server), $"Expected database '{testDatabase.Name}' was not re-attached succesfully"); + } + finally + { + DropDatabase(server, testDatabase.Name!); + } + } + } + + [Test] + public async Task DetachDatabaseScriptTest() + { + var connectionResult = await LiveConnectionHelper.InitLiveConnectionInfoAsync("master"); + using (SqlConnection sqlConn = ConnectionService.OpenSqlConnection(connectionResult.ConnectionInfo)) + { + var server = new Server(new ServerConnection(sqlConn)); + + var testDatabase = ObjectManagementTestUtils.GetTestDatabaseInfo(); + var objUrn = ObjectManagementTestUtils.GetDatabaseURN(testDatabase.Name); + await ObjectManagementTestUtils.DropObject(connectionResult.ConnectionInfo.OwnerUri, objUrn); + + try + { + // Create database to test with + var parametersForCreation = ObjectManagementTestUtils.GetInitializeViewRequestParams(connectionResult.ConnectionInfo.OwnerUri, "master", true, SqlObjectType.Database, "", ""); + await ObjectManagementTestUtils.SaveObject(parametersForCreation, testDatabase); + + var handler = new DatabaseHandler(ConnectionService.Instance); + var connectionUri = connectionResult.ConnectionInfo.OwnerUri; + + // Default use case + var detachParams = new DetachDatabaseRequestParams() + { + ConnectionUri = connectionUri, + ObjectUrn = objUrn, + DropConnections = false, + UpdateStatistics = false, + GenerateScript = true + }; + var expectedDetachScript = $"EXEC master.dbo.sp_detach_db @dbname = N'{testDatabase.Name}'"; + var expectedAlterScript = $"ALTER DATABASE [{testDatabase.Name}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE"; + var expectedStatsScript = $"EXEC master.dbo.sp_detach_db @dbname = N'{testDatabase.Name}', @skipchecks = 'false'"; + + var actualScript = handler.Detach(detachParams); + Assert.That(actualScript, Does.Contain(expectedDetachScript).IgnoreCase); + + // Drop connections only + detachParams.DropConnections = true; + actualScript = handler.Detach(detachParams); + Assert.That(actualScript, Does.Contain(expectedDetachScript).IgnoreCase); + Assert.That(actualScript, Does.Contain(expectedAlterScript).IgnoreCase); + + // Update statistics only + detachParams.DropConnections = false; + detachParams.UpdateStatistics = true; + actualScript = handler.Detach(detachParams); + Assert.That(actualScript, Does.Contain(expectedStatsScript).IgnoreCase); + + // Both drop and update + detachParams.DropConnections = true; + actualScript = handler.Detach(detachParams); + Assert.That(actualScript, Does.Contain(expectedAlterScript).IgnoreCase); + Assert.That(actualScript, Does.Contain(expectedStatsScript).IgnoreCase); + } + finally + { + DropDatabase(server, testDatabase.Name!); + } + } + } + private bool DatabaseExists(string dbName, Server server) { server.Databases.Refresh();