Add extra delete database functionality to DatabaseHandler (#2168)

* Also fixed connections not being properly closed in Detach operations
* Fixed errors being thrown in InitializeObjectView because certain db property fields are not supported against Azure
This commit is contained in:
Cory Rivera
2023-08-11 13:06:35 -07:00
committed by GitHub
parent 0820d9796a
commit 7c0da6b6b1
6 changed files with 318 additions and 59 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 DropDatabaseRequestParams : 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 delete the backup and restore history for this database
/// </summary>
public bool DeleteBackupHistory { get; set; }
/// <summary>
/// Whether to generate a TSQL script for the operation instead of dropping the database
/// </summary>
public bool GenerateScript { get; set; }
}
public class DropDatabaseRequest
{
public static readonly RequestType<DropDatabaseRequestParams, string> Type = RequestType<DropDatabaseRequestParams, string>.Create("objectManagement/dropDatabase");
}
}

View File

@@ -70,6 +70,7 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectManagement
this.serviceHost.SetRequestHandler(DisposeViewRequest.Type, HandleDisposeViewRequest, true);
this.serviceHost.SetRequestHandler(SearchRequest.Type, HandleSearchRequest, true);
this.serviceHost.SetRequestHandler(DetachDatabaseRequest.Type, HandleDetachDatabaseRequest, true);
this.serviceHost.SetRequestHandler(DropDatabaseRequest.Type, HandleDropDatabaseRequest, true);
}
internal async Task HandleRenameRequest(RenameRequestParams requestParams, RequestContext<RenameRequestResponse> requestContext)
@@ -206,6 +207,13 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectManagement
await requestContext.SendResult(sqlScript);
}
internal async Task HandleDropDatabaseRequest(DropDatabaseRequestParams requestParams, RequestContext<string> requestContext)
{
var handler = this.GetObjectTypeHandler(SqlObjectType.Database) as DatabaseHandler;
var sqlScript = handler.Drop(requestParams);
await requestContext.SendResult(sqlScript);
}
private IObjectTypeHandler GetObjectTypeHandler(SqlObjectType objectType)
{
foreach (var handler in objectTypeHandlers)

View File

@@ -126,11 +126,14 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectManagement
bool isDw = azurePrototype != null && azurePrototype.AzureEdition == AzureEdition.DataWarehouse;
bool isAzureDB = dataContainer.Server.ServerType == DatabaseEngineType.SqlAzureDatabase;
bool isManagedInstance = dataContainer.Server.DatabaseEngineEdition == DatabaseEngineEdition.SqlManagedInstance;
bool isSqlOnDemand = dataContainer.Server.Information.DatabaseEngineEdition == DatabaseEngineEdition.SqlOnDemand;
var databaseViewInfo = new DatabaseViewInfo()
{
ObjectInfo = new DatabaseInfo(),
IsAzureDB = isAzureDB
IsAzureDB = isAzureDB,
IsManagedInstance = isManagedInstance,
IsSqlOnDemand = isSqlOnDemand
};
// Collect the Database properties information
@@ -144,11 +147,7 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectManagement
Name = smoDatabase.Name,
CollationName = smoDatabase.Collation,
CompatibilityLevel = displayCompatLevels[smoDatabase.CompatibilityLevel],
ContainmentType = displayContainmentTypes[smoDatabase.ContainmentType],
RecoveryModel = displayRecoveryModels[smoDatabase.RecoveryModel],
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 = ByteConverter.ConvertKbtoMb(smoDatabase.MemoryAllocatedToMemoryOptimizedObjectsInKB),
MemoryUsedByMemoryOptimizedObjectsInMb = ByteConverter.ConvertKbtoMb(smoDatabase.MemoryUsedByMemoryOptimizedObjectsInKB),
NumberOfUsers = smoDatabase.Users.Count,
@@ -164,14 +163,24 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectManagement
EncryptionEnabled = smoDatabase.EncryptionEnabled
};
if (!isAzureDB)
{
((DatabaseInfo)databaseViewInfo.ObjectInfo).ContainmentType = displayContainmentTypes[smoDatabase.ContainmentType];
((DatabaseInfo)databaseViewInfo.ObjectInfo).RecoveryModel = displayRecoveryModels[smoDatabase.RecoveryModel];
((DatabaseInfo)databaseViewInfo.ObjectInfo).LastDatabaseBackup = smoDatabase.LastBackupDate == DateTime.MinValue ? SR.databaseBackupDate_None : smoDatabase.LastBackupDate.ToString();
((DatabaseInfo)databaseViewInfo.ObjectInfo).LastDatabaseLogBackup = smoDatabase.LastLogBackupDate == DateTime.MinValue ? SR.databaseBackupDate_None : smoDatabase.LastLogBackupDate.ToString();
}
if (!isManagedInstance)
{
databaseViewInfo.PageVerifyOptions = displayPageVerifyOptions.Values.ToArray();
databaseViewInfo.RestrictAccessOptions = displayRestrictAccessOptions.Values.ToArray();
((DatabaseInfo)databaseViewInfo.ObjectInfo).DatabaseReadOnly = smoDatabase.ReadOnly;
((DatabaseInfo)databaseViewInfo.ObjectInfo).RestrictAccess = displayRestrictAccessOptions[smoDatabase.UserAccess];
((DatabaseInfo)databaseViewInfo.ObjectInfo).PageVerify = displayPageVerifyOptions[smoDatabase.PageVerify];
((DatabaseInfo)databaseViewInfo.ObjectInfo).TargetRecoveryTimeInSec = smoDatabase.TargetRecoveryTime;
if (!isAzureDB)
{
((DatabaseInfo)databaseViewInfo.ObjectInfo).PageVerify = displayPageVerifyOptions[smoDatabase.PageVerify];
((DatabaseInfo)databaseViewInfo.ObjectInfo).TargetRecoveryTimeInSec = smoDatabase.TargetRecoveryTime;
}
if (prototype is DatabasePrototype160)
{
@@ -251,11 +260,7 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectManagement
}
finally
{
ServerConnection serverConnection = dataContainer.Server.ConnectionContext;
if (serverConnection.IsOpen)
{
serverConnection.Disconnect();
}
dataContainer.ServerConnection.Disconnect();
}
}
}
@@ -290,45 +295,52 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectManagement
ConnectionInfo connectionInfo = this.GetConnectionInfo(detachParams.ConnectionUri);
using (var dataContainer = CreateDatabaseDataContainer(detachParams.ConnectionUri, detachParams.ObjectUrn, false, null))
{
var smoDatabase = dataContainer.SqlDialogSubject as Database;
if (smoDatabase != null)
try
{
if (detachParams.GenerateScript)
var smoDatabase = dataContainer.SqlDialogSubject as Database;
if (smoDatabase != null)
{
sqlScript = CreateDetachScript(detachParams, smoDatabase.Name);
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
{
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;
}
throw new InvalidOperationException($"Provided URN '{detachParams.ObjectUrn}' did not correspond to an existing database.");
}
}
else
finally
{
throw new InvalidOperationException($"Provided URN '{detachParams.ObjectUrn}' did not correspond to an existing database.");
dataContainer.ServerConnection.Disconnect();
}
}
return sqlScript;
@@ -355,24 +367,118 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectManagement
return builder.ToString();
}
/// <summary>
/// Used to drop the specified database
/// </summary>
/// <param name="dropParams">The various parameters needed for the Drop operation</param>
public string Drop(DropDatabaseRequestParams dropParams)
{
var sqlScript = string.Empty;
ConnectionInfo connectionInfo = this.GetConnectionInfo(dropParams.ConnectionUri);
using (var dataContainer = CreateDatabaseDataContainer(dropParams.ConnectionUri, dropParams.ObjectUrn, false, null))
{
try
{
var smoDatabase = dataContainer.SqlDialogSubject as Database;
if (smoDatabase != null)
{
var originalAccess = smoDatabase.DatabaseOptions.UserAccess;
var server = smoDatabase.Parent;
var originalExecuteMode = server.ConnectionContext.SqlExecutionModes;
if (dropParams.GenerateScript)
{
server.ConnectionContext.SqlExecutionModes = SqlExecutionModes.CaptureSql;
server.ConnectionContext.CapturedSql.Clear();
}
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 (dropParams.DropConnections)
{
smoDatabase.DatabaseOptions.UserAccess = SqlServer.Management.Smo.DatabaseUserAccess.Single;
smoDatabase.Alter(TerminationClause.RollbackTransactionsImmediately);
}
if (dropParams.DeleteBackupHistory)
{
server.DeleteBackupHistory(smoDatabase.Name);
}
smoDatabase.Drop();
if (dropParams.GenerateScript)
{
var builder = new StringBuilder();
foreach (var scriptEntry in server.ConnectionContext.CapturedSql.Text)
{
if (scriptEntry != null)
{
builder.AppendLine(scriptEntry);
builder.AppendLine("GO");
}
}
sqlScript = builder.ToString();
}
}
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;
}
finally
{
if (dropParams.GenerateScript)
{
server.ConnectionContext.SqlExecutionModes = originalExecuteMode;
}
}
}
else
{
throw new InvalidOperationException($"Provided URN '{dropParams.ObjectUrn}' did not correspond to an existing database.");
}
}
finally
{
dataContainer.ServerConnection.Disconnect();
}
}
return sqlScript;
}
private CDataContainer CreateDatabaseDataContainer(string connectionUri, string? objectURN, bool isNewDatabase, string? databaseName)
{
ConnectionInfo connectionInfo = this.GetConnectionInfo(connectionUri);
if (!isNewDatabase && !string.IsNullOrEmpty(databaseName))
var originalDatabaseName = connectionInfo.ConnectionDetails.DatabaseName;
try
{
connectionInfo.ConnectionDetails.DatabaseName = databaseName;
if (!isNewDatabase && !string.IsNullOrEmpty(databaseName))
{
connectionInfo.ConnectionDetails.DatabaseName = databaseName;
}
CDataContainer dataContainer = CDataContainer.CreateDataContainer(connectionInfo, databaseExists: !isNewDatabase);
if (dataContainer.Server == null)
{
throw new InvalidOperationException(serverNotExistsError);
}
if (string.IsNullOrEmpty(objectURN))
{
objectURN = string.Format(System.Globalization.CultureInfo.InvariantCulture, "Server");
}
dataContainer.SqlDialogSubject = dataContainer.Server.GetSmoObject(objectURN);
return dataContainer;
}
CDataContainer dataContainer = CDataContainer.CreateDataContainer(connectionInfo, databaseExists: !isNewDatabase);
if (dataContainer.Server == null)
finally
{
throw new InvalidOperationException(serverNotExistsError);
connectionInfo.ConnectionDetails.DatabaseName = originalDatabaseName;
}
if (string.IsNullOrEmpty(objectURN))
{
objectURN = string.Format(System.Globalization.CultureInfo.InvariantCulture, "Server");
}
dataContainer.SqlDialogSubject = dataContainer.Server.GetSmoObject(objectURN);
return dataContainer;
}
private string ConfigureDatabase(InitializeViewRequestParams viewParams, DatabaseInfo database, ConfigAction configAction, RunType runType)
@@ -516,11 +622,7 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectManagement
}
finally
{
ServerConnection serverConnection = dataContainer.Server.ConnectionContext;
if (serverConnection.IsOpen)
{
serverConnection.Disconnect();
}
dataContainer.ServerConnection.Disconnect();
}
}
}

View File

@@ -17,6 +17,8 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectManagement
public DatabaseFile[] Files { get; set; }
public bool IsAzureDB { get; set; }
public bool IsManagedInstance { get; set; }
public bool IsSqlOnDemand { get; set; }
public string[] AzureBackupRedundancyLevels { get; set; }
public AzureEditionDetails[] AzureServiceLevelObjectives { get; set; }
public string[] AzureEditions { get; set; }

View File

@@ -439,6 +439,113 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.ObjectManagement
}
}
[Test]
public async Task DeleteDatabaseTest()
{
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 deleteParams = new DropDatabaseRequestParams()
{
ConnectionUri = connectionUri,
ObjectUrn = objUrn,
DropConnections = false,
DeleteBackupHistory = false,
GenerateScript = false
};
var script = handler.Drop(deleteParams);
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, $"Database '{testDatabase.Name}' was not deleted succesfully");
}
finally
{
DropDatabase(server, testDatabase.Name!);
}
}
}
[Test]
public async Task DeleteDatabaseScriptTest()
{
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;
var deleteParams = new DropDatabaseRequestParams()
{
ConnectionUri = connectionUri,
ObjectUrn = objUrn,
DropConnections = false,
DeleteBackupHistory = false,
GenerateScript = true
};
var expectedDeleteScript = $"DROP DATABASE [{testDatabase.Name}]";
var expectedAlterScript = $"ALTER DATABASE [{testDatabase.Name}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE";
var expectedBackupScript = $"EXEC msdb.dbo.sp_delete_database_backuphistory @database_name = N'{testDatabase.Name}'";
var actualScript = handler.Drop(deleteParams);
Assert.That(DatabaseExists(testDatabase.Name!, server), "Database should not have been deleted when just generating a script.");
Assert.That(actualScript, Does.Contain(expectedDeleteScript).IgnoreCase);
// Drop connections only
deleteParams.DropConnections = true;
actualScript = handler.Drop(deleteParams);
Assert.That(actualScript, Does.Contain(expectedDeleteScript).IgnoreCase);
Assert.That(actualScript, Does.Contain(expectedAlterScript).IgnoreCase);
// Delete backup/restore history only
deleteParams.DropConnections = false;
deleteParams.DeleteBackupHistory = true;
actualScript = handler.Drop(deleteParams);
Assert.That(actualScript, Does.Contain(expectedBackupScript).IgnoreCase);
// Both drop and update
deleteParams.DropConnections = true;
actualScript = handler.Drop(deleteParams);
Assert.That(actualScript, Does.Contain(expectedAlterScript).IgnoreCase);
Assert.That(actualScript, Does.Contain(expectedBackupScript).IgnoreCase);
}
finally
{
DropDatabase(server, testDatabase.Name!);
}
}
}
private bool DatabaseExists(string dbName, Server server)
{
server.Databases.Refresh();