diff --git a/src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/Contracts/DetachDatabase.cs b/src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/Contracts/DetachDatabaseRequest.cs similarity index 100% rename from src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/Contracts/DetachDatabase.cs rename to src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/Contracts/DetachDatabaseRequest.cs diff --git a/src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/Contracts/DropDatabaseRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/Contracts/DropDatabaseRequest.cs new file mode 100644 index 00000000..735288fb --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/Contracts/DropDatabaseRequest.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 DropDatabaseRequestParams : 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 delete the backup and restore history for this database + /// + public bool DeleteBackupHistory { get; set; } + /// + /// Whether to generate a TSQL script for the operation instead of dropping the database + /// + public bool GenerateScript { get; set; } + } + + public class DropDatabaseRequest + { + public static readonly RequestType Type = RequestType.Create("objectManagement/dropDatabase"); + } +} \ 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 08ad2ea1..13fc075f 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/ObjectManagementService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/ObjectManagementService.cs @@ -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 requestContext) @@ -206,6 +207,13 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectManagement await requestContext.SendResult(sqlScript); } + internal async Task HandleDropDatabaseRequest(DropDatabaseRequestParams requestParams, RequestContext 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) diff --git a/src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/ObjectTypes/Database/DatabaseHandler.cs b/src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/ObjectTypes/Database/DatabaseHandler.cs index 293478fc..1136e591 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/ObjectTypes/Database/DatabaseHandler.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/ObjectTypes/Database/DatabaseHandler.cs @@ -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(); } + /// + /// Used to drop the specified database + /// + /// The various parameters needed for the Drop operation + 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(); } } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/ObjectTypes/Database/DatabaseViewInfo.cs b/src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/ObjectTypes/Database/DatabaseViewInfo.cs index fab4dad4..cddc2e3f 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/ObjectTypes/Database/DatabaseViewInfo.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/ObjectTypes/Database/DatabaseViewInfo.cs @@ -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; } diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/ObjectManagement/DatabaseHandlerTests.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/ObjectManagement/DatabaseHandlerTests.cs index 40e0b822..2e380330 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/ObjectManagement/DatabaseHandlerTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/ObjectManagement/DatabaseHandlerTests.cs @@ -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();