Enable Detach Database in database handler (#2110)

This commit is contained in:
Cory Rivera
2023-06-22 17:28:41 -07:00
committed by GitHub
parent 2052b597c9
commit 9d0d4b0cae
5 changed files with 332 additions and 32 deletions

View File

@@ -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
{
/// <summary>
/// SFC (SMO) URN identifying the object
/// </summary>
public string ObjectUrn { get; set; }
/// <summary>
/// URI of the underlying connection for this request
/// </summary>
public string ConnectionUri { get; set; }
/// <summary>
/// Whether to drop active connections to this database
/// </summary>
public bool DropConnections { get; set; }
/// <summary>
/// Whether to update the optimization statistics related to this database
/// </summary>
public bool UpdateStatistics { get; set; }
/// <summary>
/// Whether to generate a TSQL script for the operation instead of detaching the database
/// </summary>
public bool GenerateScript { get; set; }
}
public class DetachDatabaseRequest
{
public static readonly RequestType<DetachDatabaseRequestParams, string> Type = RequestType<DetachDatabaseRequestParams, string>.Create("objectManagement/detachDatabase");
}
}

View File

@@ -68,6 +68,7 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectManagement
this.serviceHost.SetRequestHandler(ScriptObjectRequest.Type, HandleScriptObjectRequest, true); this.serviceHost.SetRequestHandler(ScriptObjectRequest.Type, HandleScriptObjectRequest, true);
this.serviceHost.SetRequestHandler(DisposeViewRequest.Type, HandleDisposeViewRequest, true); this.serviceHost.SetRequestHandler(DisposeViewRequest.Type, HandleDisposeViewRequest, true);
this.serviceHost.SetRequestHandler(SearchRequest.Type, HandleSearchRequest, true); this.serviceHost.SetRequestHandler(SearchRequest.Type, HandleSearchRequest, true);
this.serviceHost.SetRequestHandler(DetachDatabaseRequest.Type, HandleDetachDatabaseRequest, true);
} }
internal async Task HandleRenameRequest(RenameRequestParams requestParams, RequestContext<RenameRequestResponse> requestContext) internal async Task HandleRenameRequest(RenameRequestParams requestParams, RequestContext<RenameRequestResponse> requestContext)
@@ -197,6 +198,13 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectManagement
await requestContext.SendResult(res.ToArray()); await requestContext.SendResult(res.ToArray());
} }
internal async Task HandleDetachDatabaseRequest(DetachDatabaseRequestParams requestParams, RequestContext<string> requestContext)
{
var handler = this.GetObjectTypeHandler(SqlObjectType.Database) as DatabaseHandler;
var sqlScript = handler.Detach(requestParams);
await requestContext.SendResult(sqlScript);
}
private IObjectTypeHandler GetObjectTypeHandler(SqlObjectType objectType) private IObjectTypeHandler GetObjectTypeHandler(SqlObjectType objectType)
{ {
foreach (var handler in objectTypeHandlers) foreach (var handler in objectTypeHandlers)

View File

@@ -9,7 +9,6 @@ using System.Data;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.SqlServer.Management.Common; using Microsoft.SqlServer.Management.Common;
using Microsoft.SqlServer.Management.Sdk.Sfc;
using Microsoft.SqlServer.Management.Smo; using Microsoft.SqlServer.Management.Smo;
using Microsoft.SqlTools.ServiceLayer.Admin; using Microsoft.SqlTools.ServiceLayer.Admin;
using static Microsoft.SqlTools.ServiceLayer.Admin.AzureSqlDbHelper; 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.ObjectManagement.Contracts;
using Microsoft.SqlTools.ServiceLayer.Utility; using Microsoft.SqlTools.ServiceLayer.Utility;
using Microsoft.SqlTools.Utility; using Microsoft.SqlTools.Utility;
using System.Text;
using System.IO;
using Microsoft.SqlTools.ServiceLayer.Utility.SqlScriptFormatters;
namespace Microsoft.SqlTools.ServiceLayer.ObjectManagement namespace Microsoft.SqlTools.ServiceLayer.ObjectManagement
{ {
@@ -98,9 +100,7 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectManagement
public override Task<InitializeViewResult> InitializeObjectView(InitializeViewRequestParams requestParams) public override Task<InitializeViewResult> InitializeObjectView(InitializeViewRequestParams requestParams)
{ {
// create a default data context and database object // create a default data context and database object
ConfigAction configAction = requestParams.IsNewObject ? ConfigAction.Create : ConfigAction.Update; using (var dataContainer = CreateDatabaseDataContainer(requestParams.ConnectionUri, requestParams.ObjectUrn, requestParams.IsNewObject))
var databaseInfo = !requestParams.IsNewObject ? new DatabaseInfo() { Name = requestParams.Database } : null;
using (var dataContainer = CreateDatabaseDataContainer(requestParams, configAction, databaseInfo))
{ {
if (dataContainer.Server == null) if (dataContainer.Server == null)
{ {
@@ -126,6 +126,8 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectManagement
if (!requestParams.IsNewObject) if (!requestParams.IsNewObject)
{ {
var smoDatabase = dataContainer.SqlDialogSubject as Database; var smoDatabase = dataContainer.SqlDialogSubject as Database;
if (smoDatabase != null)
{
databaseViewInfo.ObjectInfo = new DatabaseInfo() databaseViewInfo.ObjectInfo = new DatabaseInfo()
{ {
Name = smoDatabase.Name, Name = smoDatabase.Name,
@@ -142,6 +144,7 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectManagement
Status = smoDatabase.Status.ToString() Status = smoDatabase.Status.ToString()
}; };
} }
}
// azure sql db doesn't have a sysadmin fixed role // azure sql db doesn't have a sysadmin fixed role
var compatibilityLevelEnabled = !isDw && (dataContainer.LoggedInUserIsSysadmin || isAzureDB); var compatibilityLevelEnabled = !isDw && (dataContainer.LoggedInUserIsSysadmin || isAzureDB);
@@ -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 // 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.RecoveryModels = GetRecoveryModels(dataContainer.Server, prototype);
databaseViewInfo.ContainmentTypes = GetContainmentTypes(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 // Skip adding logins if running against an Azure SQL DB
@@ -240,30 +251,105 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectManagement
return Task.FromResult(script); return Task.FromResult(script);
} }
private CDataContainer CreateDatabaseDataContainer(InitializeViewRequestParams requestParams, ConfigAction configAction, DatabaseInfo? database = null) /// <summary>
/// Used to detach the specified database from a server.
/// </summary>
/// <param name="detachParams">The various parameters needed for the Detach operation</param>
public string Detach(DetachDatabaseRequestParams detachParams)
{ {
ConnectionInfo connectionInfo = this.GetConnectionInfo(requestParams.ConnectionUri); var sqlScript = string.Empty;
CDataContainer dataContainer = CDataContainer.CreateDataContainer(connectionInfo, databaseExists: configAction != ConfigAction.Create); 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) if (dataContainer.Server == null)
{ {
throw new InvalidOperationException(serverNotExistsError); throw new InvalidOperationException(serverNotExistsError);
} }
string objectUrn = configAction != ConfigAction.Create && database != null if (string.IsNullOrEmpty(objectURN))
? string.Format(System.Globalization.CultureInfo.InvariantCulture, "Server/Database[@Name='{0}']", Urn.EscapeString(database.Name)) {
: string.Format(System.Globalization.CultureInfo.InvariantCulture, "Server"); objectURN = string.Format(System.Globalization.CultureInfo.InvariantCulture, "Server");
}
dataContainer.SqlDialogSubject = dataContainer.Server.GetSmoObject(objectUrn); dataContainer.SqlDialogSubject = dataContainer.Server.GetSmoObject(objectURN);
return dataContainer; 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) if (database.Name == null)
{ {
throw new ArgumentException("Database name not provided."); 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) if (dataContainer.Server == null)
{ {
@@ -522,6 +608,35 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectManagement
return recoveryModels.ToArray(); return recoveryModels.ToArray();
} }
private DatabaseFile[] GetDatabaseFiles(Database database)
{
var filesList = new List<DatabaseFile>();
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();
}
/// <summary> /// <summary>
/// Get supported database compatibility levels for this Azure server. /// Get supported database compatibility levels for this Azure server.
/// </summary> /// </summary>

View File

@@ -14,6 +14,7 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectManagement
public string[] CompatibilityLevels { get; set; } public string[] CompatibilityLevels { get; set; }
public string[] ContainmentTypes { get; set; } public string[] ContainmentTypes { get; set; }
public string[] RecoveryModels { get; set; } public string[] RecoveryModels { get; set; }
public DatabaseFile[] Files { get; set; }
public bool IsAzureDB { get; set; } public bool IsAzureDB { get; set; }
public string[] AzureBackupRedundancyLevels { get; set; } public string[] AzureBackupRedundancyLevels { get; set; }
@@ -27,4 +28,12 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectManagement
public string EditionDisplayName { get; set; } public string EditionDisplayName { get; set; }
public string[] Details { 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; }
}
} }

View File

@@ -4,6 +4,7 @@
// //
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Data.SqlClient; using Microsoft.Data.SqlClient;
@@ -13,6 +14,7 @@ using Microsoft.SqlTools.ServiceLayer.Admin;
using Microsoft.SqlTools.ServiceLayer.Connection; using Microsoft.SqlTools.ServiceLayer.Connection;
using Microsoft.SqlTools.ServiceLayer.IntegrationTests.Utility; using Microsoft.SqlTools.ServiceLayer.IntegrationTests.Utility;
using Microsoft.SqlTools.ServiceLayer.ObjectManagement; using Microsoft.SqlTools.ServiceLayer.ObjectManagement;
using Microsoft.SqlTools.ServiceLayer.ObjectManagement.Contracts;
using Microsoft.SqlTools.ServiceLayer.Test.Common; using Microsoft.SqlTools.ServiceLayer.Test.Common;
using NUnit.Framework; using NUnit.Framework;
using static Microsoft.SqlTools.ServiceLayer.Admin.AzureSqlDbHelper; using static Microsoft.SqlTools.ServiceLayer.Admin.AzureSqlDbHelper;
@@ -201,7 +203,7 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.ObjectManagement
{ AzureEdition.Hyperscale, "HS_Gen5_2" } { 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"); 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)) if (AzureSqlDbHelper.TryGetServiceObjectiveInfo(edition, out var expectedLevelInfo))
{ {
@@ -240,7 +242,7 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.ObjectManagement
{ AzureEdition.Hyperscale, "0MB" } { AzureEdition.Hyperscale, "0MB" }
}; };
Assert.That(actualSizesMap.Count, Is.EqualTo(expectedDefaults.Count), "Did not get expected number of editions for DatabaseHandler's max sizes"); 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)) if (AzureSqlDbHelper.TryGetDatabaseSizeInfo(edition, out var expectedSizeInfo))
{ {
@@ -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) private bool DatabaseExists(string dbName, Server server)
{ {
server.Databases.Refresh(); server.Databases.Refresh();