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();