mirror of
https://github.com/ckaczor/sqltoolsservice.git
synced 2026-01-13 17:23:02 -05:00
Enable Detach Database in database handler (#2110)
This commit is contained in:
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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<RenameRequestResponse> requestContext)
|
||||
@@ -197,6 +198,13 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectManagement
|
||||
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)
|
||||
{
|
||||
foreach (var handler in objectTypeHandlers)
|
||||
|
||||
@@ -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<InitializeViewResult> 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,6 +126,8 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectManagement
|
||||
if (!requestParams.IsNewObject)
|
||||
{
|
||||
var smoDatabase = dataContainer.SqlDialogSubject as Database;
|
||||
if (smoDatabase != null)
|
||||
{
|
||||
databaseViewInfo.ObjectInfo = new DatabaseInfo()
|
||||
{
|
||||
Name = smoDatabase.Name,
|
||||
@@ -142,6 +144,7 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectManagement
|
||||
Status = smoDatabase.Status.ToString()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// azure sql db doesn't have a sysadmin fixed role
|
||||
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
|
||||
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)
|
||||
/// <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);
|
||||
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<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>
|
||||
/// Get supported database compatibility levels for this Azure server.
|
||||
/// </summary>
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user