From 88eaa647550dfc6455c1ac48f52b9eafcf6e9b0e Mon Sep 17 00:00:00 2001 From: M-Patrone Date: Thu, 22 Sep 2022 23:42:23 +0200 Subject: [PATCH] Feature rename sql objects (#1686) --- .../HostLoader.cs | 4 + .../Localization/sr.cs | 10 ++ .../Localization/sr.strings | 5 +- .../Contracts/RenameRequest.cs | 29 ++++ .../ObjectManagementService.cs | 98 ++++++++++++ .../ObjectManagementServiceTests.cs | 149 ++++++++++++++++++ 6 files changed, 294 insertions(+), 1 deletion(-) create mode 100644 src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/Contracts/RenameRequest.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/ObjectManagementService.cs create mode 100644 test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/ObjectManagement/ObjectManagementServiceTests.cs diff --git a/src/Microsoft.SqlTools.ServiceLayer/HostLoader.cs b/src/Microsoft.SqlTools.ServiceLayer/HostLoader.cs index f017bf6f..c8ca7258 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/HostLoader.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/HostLoader.cs @@ -40,6 +40,7 @@ using Microsoft.SqlTools.ServiceLayer.ModelManagement; using Microsoft.SqlTools.ServiceLayer.TableDesigner; using Microsoft.SqlTools.ServiceLayer.AzureBlob; using Microsoft.SqlTools.ServiceLayer.ExecutionPlan; +using Microsoft.SqlTools.ServiceLayer.ObjectManagement; using System.IO; namespace Microsoft.SqlTools.ServiceLayer @@ -179,6 +180,9 @@ namespace Microsoft.SqlTools.ServiceLayer ExecutionPlanService.Instance.InitializeService(serviceHost); serviceProvider.RegisterSingleService(ExecutionPlanService.Instance); + ObjectManagementService.Instance.InitializeService(serviceHost); + serviceProvider.RegisterSingleService(ObjectManagementService.Instance); + serviceHost.InitializeRequestHandlers(); } diff --git a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs index bd28ccb4..01ec43f5 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs @@ -9613,6 +9613,14 @@ namespace Microsoft.SqlTools.ServiceLayer } } + public static string ErrorConnectionNotFound + { + get + { + return Keys.GetString(Keys.ErrorConnectionNotFound); + } + } + public static string ConnectionServiceListDbErrorNotConnected(string uri) { return Keys.GetString(Keys.ConnectionServiceListDbErrorNotConnected, uri); @@ -13878,6 +13886,8 @@ namespace Microsoft.SqlTools.ServiceLayer public const string GetUserDefinedObjectsFromModelFailed = "GetUserDefinedObjectsFromModelFailed"; + public const string ErrorConnectionNotFound = "ErrorConnectionNotFound"; + private Keys() { } diff --git a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings index 186cc61b..0945eaa0 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings +++ b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings @@ -2415,4 +2415,7 @@ TableDesignerConfirmationText = I have read the summary and understand the poten SqlProjectModelNotFound(string projectUri) = Could not find SQL model from project: {0}. UnsupportedModelType(string type) = Unsupported model type: {0}. -GetUserDefinedObjectsFromModelFailed = Failed to get user defined objects from model. \ No newline at end of file +GetUserDefinedObjectsFromModelFailed = Failed to get user defined objects from model. + +#ObjectManagement Service +ErrorConnectionNotFound = The connection could not be found \ No newline at end of file diff --git a/src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/Contracts/RenameRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/Contracts/RenameRequest.cs new file mode 100644 index 00000000..74c50b2d --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/Contracts/RenameRequest.cs @@ -0,0 +1,29 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// +using Microsoft.SqlTools.Hosting.Protocol.Contracts; +using Microsoft.SqlTools.Utility; + +namespace Microsoft.SqlTools.ServiceLayer.ObjectManagement.Contracts +{ + public class RenameRequestParams : GeneralRequestDetails + { + /// + /// SFC (SMO) URN identifying the object + /// + public string ObjectUrn { get; set; } + /// + /// the new name of the object + /// + public string NewName { get; set; } + /// + /// Connection uri + /// + public string ConnectionUri { get; set; } + } + public class RenameRequest + { + public static readonly RequestType Type = RequestType.Create("objectmanagement/rename"); + } +} \ No newline at end of file diff --git a/src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/ObjectManagementService.cs b/src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/ObjectManagementService.cs new file mode 100644 index 00000000..0fd44f9d --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/ObjectManagementService.cs @@ -0,0 +1,98 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// +using System; +using System.Threading.Tasks; +using Microsoft.Data.SqlClient; +using Microsoft.SqlServer.Management.Common; +using Microsoft.SqlServer.Management.Sdk.Sfc; +using Microsoft.SqlServer.Management.Smo; +using Microsoft.SqlTools.Hosting.Protocol; +using Microsoft.SqlTools.ServiceLayer.Connection; +using Microsoft.SqlTools.ServiceLayer.ObjectManagement.Contracts; +using Microsoft.SqlTools.Utility; + +namespace Microsoft.SqlTools.ServiceLayer.ObjectManagement +{ + /// + /// Main class for ObjectManagement Service functionality + /// + public class ObjectManagementService + { + private const string ObjectManagementServiceApplicationName = "azdata-object-management"; + private static Lazy objectManagementServiceInstance = new Lazy(() => new ObjectManagementService()); + public static ObjectManagementService Instance => objectManagementServiceInstance.Value; + public static ConnectionService connectionService; + private IProtocolEndpoint serviceHost; + public ObjectManagementService() { } + + /// + /// Internal for testing purposes only + /// + internal static ConnectionService ConnectionServiceInstance + { + get + { + connectionService ??= ConnectionService.Instance; + return connectionService; + } + set + { + connectionService = value; + } + } + + public void InitializeService(IProtocolEndpoint serviceHost) + { + this.serviceHost = serviceHost; + this.serviceHost.SetRequestHandler(RenameRequest.Type, HandleRenameRequest); + } + + /// + /// Method to handle the renaming operation + /// + /// parameters which are needed to execute renaming operation + /// Request Context + /// + internal async Task HandleRenameRequest(RenameRequestParams requestParams, RequestContext requestContext) + { + Logger.Verbose("Handle Request in HandleProcessRenameEditRequest()"); + ConnectionInfo connInfo; + + if (connectionService.TryFindConnection( + requestParams.ConnectionUri, + out connInfo)) + { + using (SqlConnection sqlConn = ConnectionService.OpenSqlConnection(connInfo, ObjectManagementServiceApplicationName)) + { + + IRenamable renameObject = this.GetRenamable(requestParams, sqlConn); + + renameObject.Rename(requestParams.NewName); + } + } + else + { + Logger.Error($"The connection {requestParams.ConnectionUri} could not be found."); + throw new Exception(SR.ErrorConnectionNotFound); + } + + await requestContext.SendResult(true); + } + + /// + /// Method to get the sql object, which should be renamed + /// + /// parameters which are required for the rename operation + /// the sqlconnection on the server to search for the sqlobject + /// the sql object if implements the interface IRenamable, so they can be renamed + private IRenamable GetRenamable(RenameRequestParams requestParams, SqlConnection connection) + { + ServerConnection serverConnection = new ServerConnection(connection); + Server server = new Server(serverConnection); + SqlSmoObject dbObject = server.GetSmoObject(new Urn(requestParams.ObjectUrn)); + return (IRenamable)dbObject; + } + } +} \ No newline at end of file diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/ObjectManagement/ObjectManagementServiceTests.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/ObjectManagement/ObjectManagementServiceTests.cs new file mode 100644 index 00000000..aea0a4d3 --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/ObjectManagement/ObjectManagementServiceTests.cs @@ -0,0 +1,149 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// +using System; +using System.Collections.Concurrent; +using System.Threading.Tasks; +using Microsoft.SqlServer.Management.Smo; +using Microsoft.SqlTools.Hosting.Protocol; +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.QueryExecution; +using Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage; +using Microsoft.SqlTools.ServiceLayer.SqlContext; +using Microsoft.SqlTools.ServiceLayer.Test.Common; +using Moq; +using NUnit.Framework; +using static Microsoft.SqlTools.ServiceLayer.IntegrationTests.Utility.LiveConnectionHelper; + +namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.ObjectManagement +{ + public class ObjectManagementServiceTests + { + private const string TableQuery = @"CREATE TABLE testTable1_RenamingTable (c1 int)"; + private const string OwnerUri = "testDB"; + private ObjectManagementService objectManagementService; + private SqlTestDb testDb; + private Mock> requestContextMock; + + [SetUp] + public async Task TestInitialize() + { + this.testDb = await SqlTestDb.CreateNewAsync(serverType: TestServerType.OnPrem, query: TableQuery, dbNamePrefix: "RenameTest"); + + requestContextMock = new Mock>(); + ConnectionService connectionService = LiveConnectionHelper.GetLiveTestConnectionService(); + + TestConnectionResult connectionResult = await LiveConnectionHelper.InitLiveConnectionInfoAsync(testDb.DatabaseName, OwnerUri, ConnectionType.Default); + + ObjectManagementService.ConnectionServiceInstance = connectionService; + this.objectManagementService = new ObjectManagementService(); + } + + [TearDown] + public async Task TearDownTestDatabase() + { + await SqlTestDb.DropDatabase(testDb.DatabaseName); + } + + [Test] + public async Task TestRenameTable() + { + //arrange & act + await objectManagementService.HandleRenameRequest(this.InitRequestParams("RenamingTable", String.Format("Server/Database[@Name='{0}']/Table[@Name='testTable1_RenamingTable' and @Schema='dbo']", testDb.DatabaseName)), requestContextMock.Object); + + //assert + requestContextMock.Verify(x => x.SendResult(It.Is(r => r == true))); + + Query queryRenameObject = ExecuteQuery("SELECT * FROM " + testDb.DatabaseName + ".sys.tables WHERE name='RenamingTable'"); + Assert.That(queryRenameObject.HasExecuted, Is.True, "The query to check for the renamed table was not executed"); + Assert.That(queryRenameObject.HasErrored, Is.False, "There were errors on the execution of the query to check for the renamed table"); + Assert.That(queryRenameObject.Batches[0].ResultSets[0].RowCount, Is.EqualTo(1), "Did not find the table with the new name after the rename operation"); + + Query queryOldObject = ExecuteQuery("SELECT * FROM " + testDb.DatabaseName + ".sys.tables WHERE name='testTable1_RenamingTable'"); + Assert.That(queryOldObject.HasExecuted, Is.True, "The query to check for the old table was not executed"); + Assert.That(queryOldObject.HasErrored, Is.False, "There were errors on the execution of the query to check for the old table"); + Assert.That(queryOldObject.Batches[0].ResultSets[0].RowCount, Is.EqualTo(0), "Did find the old table which should have been renamed"); + } + + [Test] + public async Task TestRenameColumn() + { + //arrange & act + await objectManagementService.HandleRenameRequest(this.InitRequestParams("RenameColumn", String.Format("Server/Database[@Name='{0}']/Table[@Name='testTable1_RenamingTable' and @Schema='dbo']/Column[@Name='C1']", testDb.DatabaseName)), requestContextMock.Object); + + //assert + requestContextMock.Verify(x => x.SendResult(It.Is(r => r == true))); + + Query queryRenameObject = ExecuteQuery("SELECT * FROM " + testDb.DatabaseName + ".sys.columns WHERE name='RenameColumn'"); + Assert.That(queryRenameObject.HasExecuted, Is.True, "The query to check for the renamed column was not executed"); + Assert.That(queryRenameObject.HasErrored, Is.False, "There were errors on the execution of the query to check for the renamed column"); + Assert.That(queryRenameObject.Batches[0].ResultSets[0].RowCount, Is.EqualTo(1), "Did not find the column with the new name after the rename operation"); + + Query queryOldObject = ExecuteQuery("SELECT * FROM " + testDb.DatabaseName + ".sys.columns WHERE name='C1'"); + Assert.That(queryOldObject.HasExecuted, Is.True, "The query to check for the old column was not executed"); + Assert.That(queryOldObject.HasErrored, Is.False, "There were errors on the execution of the query to check for the old column"); + Assert.That(queryOldObject.Batches[0].ResultSets[0].RowCount, Is.EqualTo(0), "Did find the old column which should have been renamed"); + } + + [Test] + public async Task TestRenameColumnNotExisting() + { + Assert.That(async () => + { + await objectManagementService.HandleRenameRequest(this.InitRequestParams("RenameColumn", String.Format("Server/Database[@Name='{0}']/Table[@Name='testTable1_RenamingTable' and @Schema='dbo']/Column[@Name='C1_NOT']", testDb.DatabaseName)), requestContextMock.Object); + }, Throws.Exception.TypeOf(), "Did find the column, which should not have existed"); + } + + [Test] + public async Task TestRenameTableNotExisting() + { + Assert.That(async () => + { + await objectManagementService.HandleRenameRequest(this.InitRequestParams("RenamingTable", String.Format("Server/Database[@Name='{0}']/Table[@Name='testTable1_Not' and @Schema='dbo']", testDb.DatabaseName)), requestContextMock.Object); + }, Throws.Exception.TypeOf(), "Did find the table, which should not have existed"); + } + + [Test] + public async Task TestConnectionNotFound() + { + var testRenameRequestParams = new RenameRequestParams + { + NewName = "RenamingTable", + ConnectionUri = "NOT_EXISTING", + ObjectUrn = String.Format("Server/Database[@Name='{0}']/Table[@Name='testTable1_Not' and @Schema='dbo']", testDb.DatabaseName), + }; + Assert.That(async () => + { + await objectManagementService.HandleRenameRequest(testRenameRequestParams, requestContextMock.Object); + + }, Throws.Exception.TypeOf(), "Did find the connection, which should not have existed"); + } + + private RenameRequestParams InitRequestParams(string newName, string UrnOfObject) + { + return new RenameRequestParams + { + NewName = newName, + ConnectionUri = OwnerUri, + ObjectUrn = UrnOfObject, + }; + } + + private Query ExecuteQuery(string queryText) + { + TestConnectionResult conResult = LiveConnectionHelper.InitLiveConnectionInfo(); + ConnectionInfo connInfo = conResult.ConnectionInfo; + IFileStreamFactory fileStreamFactory = MemoryFileSystem.GetFileStreamFactory(new ConcurrentDictionary()); + + QueryExecutionSettings settings = new QueryExecutionSettings() { IsSqlCmdMode = false }; + Query query = new Query(queryText, connInfo, settings, fileStreamFactory); + query.Execute(); + query.ExecutionTask.Wait(); + return query; + } + } +} \ No newline at end of file