From 7ba2011a1e3070110e9e102ae96c9d7860ac0514 Mon Sep 17 00:00:00 2001 From: Karl Burtram Date: Tue, 14 Mar 2017 22:35:17 -0700 Subject: [PATCH] Initial metadata and scripting services (#280) * Initial metadata service and scripting service files * Simple metadata lookup with SMO objects * Add metadata type class * Remove SMO from metadata service. * Cleanup metadata service SQL * Initial MetadataService test * Add scripting commands * Add metadata test case * Remove sleep used for testing * Use random table name in metadata test * Add scripting tests --- .gitignore | 6 +- .../HostLoader.cs | 8 + .../Metadata/Contracts/MetadataListRequest.cs | 26 +++ .../Metadata/Contracts/ObjectMetadata.cs | 32 ++++ .../Metadata/MetadataService.cs | 167 ++++++++++++++++++ .../Contracts/ScriptingSelectRequest.cs | 54 ++++++ .../Scripting/ScriptingService.cs | 82 +++++++++ .../Metadata/MetadataServiceTests.cs | 96 ++++++++++ .../Scripting/ScriptingServiceTests.cs | 118 +++++++++++++ 9 files changed, 588 insertions(+), 1 deletion(-) create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Metadata/Contracts/MetadataListRequest.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Metadata/Contracts/ObjectMetadata.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Metadata/MetadataService.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Scripting/Contracts/ScriptingSelectRequest.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Scripting/ScriptingService.cs create mode 100644 test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Metadata/MetadataServiceTests.cs create mode 100644 test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Scripting/ScriptingServiceTests.cs diff --git a/.gitignore b/.gitignore index ba8f8bd3..f1ed27fd 100644 --- a/.gitignore +++ b/.gitignore @@ -284,7 +284,7 @@ Session.vim # docfx generated files _site -metadata +docs/metadata # Stuff from cake /artifacts/ @@ -292,3 +292,7 @@ metadata /.dotnet/ /test/Microsoft.SqlTools.ServiceLayer.TestEnvConfig/Properties/launchSettings.json /test/Microsoft.SqlTools.ServiceLayer.PerfTests/Properties/launchSettings.json + +# Test output files +*cycle*txt* +*input*txt* diff --git a/src/Microsoft.SqlTools.ServiceLayer/HostLoader.cs b/src/Microsoft.SqlTools.ServiceLayer/HostLoader.cs index 2395a201..aa6fac63 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/HostLoader.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/HostLoader.cs @@ -10,7 +10,9 @@ using Microsoft.SqlTools.ServiceLayer.Connection; using Microsoft.SqlTools.ServiceLayer.EditData; using Microsoft.SqlTools.ServiceLayer.Hosting; using Microsoft.SqlTools.ServiceLayer.LanguageServices; +using Microsoft.SqlTools.ServiceLayer.Metadata; using Microsoft.SqlTools.ServiceLayer.QueryExecution; +using Microsoft.SqlTools.ServiceLayer.Scripting; using Microsoft.SqlTools.ServiceLayer.SqlContext; using Microsoft.SqlTools.ServiceLayer.Workspace; @@ -76,6 +78,12 @@ namespace Microsoft.SqlTools.ServiceLayer EditDataService.Instance.InitializeService(serviceHost); serviceProvider.RegisterSingleService(EditDataService.Instance); + MetadataService.Instance.InitializeService(serviceHost); + serviceProvider.RegisterSingleService(MetadataService.Instance); + + ScriptingService.Instance.InitializeService(serviceHost); + serviceProvider.RegisterSingleService(ScriptingService.Instance); + InitializeHostedServices(serviceProvider, serviceHost); serviceHost.InitializeRequestHandlers(); diff --git a/src/Microsoft.SqlTools.ServiceLayer/Metadata/Contracts/MetadataListRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/Metadata/Contracts/MetadataListRequest.cs new file mode 100644 index 00000000..c7286292 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Metadata/Contracts/MetadataListRequest.cs @@ -0,0 +1,26 @@ +// +// 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; + +namespace Microsoft.SqlTools.ServiceLayer.Metadata.Contracts +{ + public class MetadataQueryParams + { + public string OwnerUri { get; set; } + } + + public class MetadataQueryResult + { + public ObjectMetadata[] Metadata { get; set; } + } + + public class MetadataListRequest + { + public static readonly + RequestType Type = + RequestType.Create("metadata/list"); + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Metadata/Contracts/ObjectMetadata.cs b/src/Microsoft.SqlTools.ServiceLayer/Metadata/Contracts/ObjectMetadata.cs new file mode 100644 index 00000000..c0f208c8 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Metadata/Contracts/ObjectMetadata.cs @@ -0,0 +1,32 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace Microsoft.SqlTools.ServiceLayer.Metadata.Contracts +{ + /// + /// Metadata type enumeration + /// + public enum MetadataType + { + Table = 0, + View = 1, + SProc = 2, + Function = 3 + } + + /// + /// Object metadata information + /// + public class ObjectMetadata + { + public MetadataType MetadataType { get; set; } + + public string MetadataTypeName { get; set; } + + public string Schema { get; set; } + + public string Name { get; set; } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Metadata/MetadataService.cs b/src/Microsoft.SqlTools.ServiceLayer/Metadata/MetadataService.cs new file mode 100644 index 00000000..0de1fa0a --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Metadata/MetadataService.cs @@ -0,0 +1,167 @@ +// +// 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.Generic; +using System.Data.SqlClient; +using System.Threading.Tasks; +using Microsoft.SqlTools.Hosting.Protocol; +using Microsoft.SqlTools.ServiceLayer.Connection; +using Microsoft.SqlTools.ServiceLayer.Hosting; +using Microsoft.SqlTools.ServiceLayer.Metadata.Contracts; + +namespace Microsoft.SqlTools.ServiceLayer.Metadata +{ + /// + /// Main class for Metadata Service functionality + /// + public sealed class MetadataService + { + private static readonly Lazy LazyInstance = new Lazy(() => new MetadataService()); + + public static MetadataService Instance => LazyInstance.Value; + + private static ConnectionService connectionService = null; + + /// + /// Internal for testing purposes only + /// + internal static ConnectionService ConnectionServiceInstance + { + get + { + if (connectionService == null) + { + connectionService = ConnectionService.Instance; + } + return connectionService; + } + + set + { + connectionService = value; + } + } + + /// + /// Initializes the Metadata Service instance + /// + /// + /// + public void InitializeService(ServiceHost serviceHost) + { + serviceHost.SetRequestHandler(MetadataListRequest.Type, HandleMetadataListRequest); + } + + /// + /// Handle a metadata query request + /// + internal static async Task HandleMetadataListRequest( + MetadataQueryParams metadataParams, + RequestContext requestContext) + { + try + { + ConnectionInfo connInfo; + MetadataService.ConnectionServiceInstance.TryFindConnection( + metadataParams.OwnerUri, + out connInfo); + + var metadata = new List(); + if (connInfo != null) + { + SqlConnection sqlConn = OpenMetadataConnection(connInfo); + ReadMetadata(sqlConn, metadata); + } + + await requestContext.SendResult(new MetadataQueryResult() + { + Metadata = metadata.ToArray() + }); + } + catch (Exception ex) + { + await requestContext.SendError(ex.ToString()); + } + } + + /// + /// Create a SqlConnection to use for querying metadata + /// + internal static SqlConnection OpenMetadataConnection(ConnectionInfo connInfo) + { + try + { + // increase the connection timeout to at least 30 seconds and and build connection string + // enable PersistSecurityInfo to handle issues in SMO where the connection context is lost in reconnections + int? originalTimeout = connInfo.ConnectionDetails.ConnectTimeout; + bool? originalPersistSecurityInfo = connInfo.ConnectionDetails.PersistSecurityInfo; + connInfo.ConnectionDetails.ConnectTimeout = Math.Max(30, originalTimeout ?? 0); + connInfo.ConnectionDetails.PersistSecurityInfo = true; + string connectionString = ConnectionService.BuildConnectionString(connInfo.ConnectionDetails); + connInfo.ConnectionDetails.ConnectTimeout = originalTimeout; + connInfo.ConnectionDetails.PersistSecurityInfo = originalPersistSecurityInfo; + + // open a dedicated binding server connection + SqlConnection sqlConn = new SqlConnection(connectionString); + sqlConn.Open(); + return sqlConn; + } + catch (Exception) + { + } + + return null; + } + + /// + /// Read metadata for the current connection + /// + internal static void ReadMetadata(SqlConnection sqlConn, List metadata) + { + string sql = + @"SELECT s.name AS schema_name, o.[name] AS object_name, o.[type] AS object_type + FROM sys.all_objects o + INNER JOIN sys.schemas s ON o.schema_id = s.schema_id + WHERE o.is_ms_shipped != 1 + AND (o.[type] = 'P' OR o.[type] = 'V' OR o.[type] = 'U') + ORDER BY object_type, schema_name, object_name"; + + using (SqlCommand sqlCommand = new SqlCommand(sql, sqlConn)) + { + using (var reader = sqlCommand.ExecuteReader()) + { + while (reader.Read()) + { + var schemaName = reader[0] as string; + var objectName = reader[1] as string; + var objectType = reader[2] as string; + + MetadataType metadataType; + if (objectType.StartsWith("V")) + { + metadataType = MetadataType.View; + } + else if (objectType.StartsWith("P")) + { + metadataType = MetadataType.SProc; + } + else + { + metadataType = MetadataType.Table; + } + + metadata.Add(new ObjectMetadata + { + MetadataType = metadataType, + Schema = schemaName, + Name = objectName + }); + } + } + } + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Scripting/Contracts/ScriptingSelectRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/Scripting/Contracts/ScriptingSelectRequest.cs new file mode 100644 index 00000000..b829f5dc --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Scripting/Contracts/ScriptingSelectRequest.cs @@ -0,0 +1,54 @@ +// +// 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.ServiceLayer.Metadata.Contracts; + +namespace Microsoft.SqlTools.ServiceLayer.Scripting.Contracts +{ + /// + /// The type of scripting operation requested + /// + public enum ScriptOperation + { + Select = 0, + Create = 1, + Insert = 2, + Update = 3, + Delete = 4 + } + + /// + /// Script as request parameter type + /// + public class ScriptingScriptAsParams + { + public string OwnerUri { get; set; } + + public ScriptOperation Operation { get; set; } + + public ObjectMetadata Metadata { get; set; } + } + + /// + /// Script as request result type + /// + public class ScriptingScriptAsResult + { + public string OwnerUri { get; set; } + + public string Script { get; set; } + } + + /// + /// Script as request message type + /// + public class ScriptingScriptAsRequest + { + public static readonly + RequestType Type = + RequestType.Create("scripting/scriptas"); + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Scripting/ScriptingService.cs b/src/Microsoft.SqlTools.ServiceLayer/Scripting/ScriptingService.cs new file mode 100644 index 00000000..9fc0ca7e --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Scripting/ScriptingService.cs @@ -0,0 +1,82 @@ +// +// 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.SqlTools.Hosting.Protocol; +using Microsoft.SqlTools.ServiceLayer.Hosting; +using Microsoft.SqlTools.ServiceLayer.Scripting.Contracts; + +namespace Microsoft.SqlTools.ServiceLayer.Scripting +{ + /// + /// Main class for Scripting Service functionality + /// + public sealed class ScriptingService + { + private static readonly Lazy LazyInstance = new Lazy(() => new ScriptingService()); + + public static ScriptingService Instance => LazyInstance.Value; + + /// + /// Initializes the Scripting Service instance + /// + /// + /// + public void InitializeService(ServiceHost serviceHost) + { + serviceHost.SetRequestHandler(ScriptingScriptAsRequest.Type, HandleScriptingScriptAsRequest); + } + + /// + /// Handles script as request messages + /// + /// + /// + internal static async Task HandleScriptingScriptAsRequest( + ScriptingScriptAsParams scriptingParams, + RequestContext requestContext) + { + string script = string.Empty; + if (scriptingParams.Operation == ScriptOperation.Select) + { + script = string.Format( +@"SELECT * +FROM {0}.{1}", + scriptingParams.Metadata.Schema, scriptingParams.Metadata.Name); + } + else if (scriptingParams.Operation == ScriptOperation.Create) + { + script = string.Format( +@"CREATE {0}.{1}", + scriptingParams.Metadata.Schema, scriptingParams.Metadata.Name); + } + else if (scriptingParams.Operation == ScriptOperation.Update) + { + script = string.Format( +@"UPDATE {0}.{1}", + scriptingParams.Metadata.Schema, scriptingParams.Metadata.Name); + } + else if (scriptingParams.Operation == ScriptOperation.Insert) + { + script = string.Format( +@"INSERT {0}.{1}", + scriptingParams.Metadata.Schema, scriptingParams.Metadata.Name); + } + else if (scriptingParams.Operation == ScriptOperation.Delete) + { + script = string.Format( +@"DELETE {0}.{1}", + scriptingParams.Metadata.Schema, scriptingParams.Metadata.Name); + } + + await requestContext.SendResult(new ScriptingScriptAsResult() + { + OwnerUri = scriptingParams.OwnerUri, + Script = script + }); + } + } +} diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Metadata/MetadataServiceTests.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Metadata/MetadataServiceTests.cs new file mode 100644 index 00000000..aa5def9b --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Metadata/MetadataServiceTests.cs @@ -0,0 +1,96 @@ +// +// 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.ServiceLayer.Workspace.Contracts; +using Xunit; +using Microsoft.SqlTools.ServiceLayer.Test.Common; +using Microsoft.SqlTools.ServiceLayer.IntegrationTests.Utility; +using Microsoft.SqlTools.ServiceLayer.Metadata; +using System.Collections.Generic; +using Microsoft.SqlTools.ServiceLayer.Metadata.Contracts; +using System.Data.SqlClient; +using System; + +namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.Metadata +{ + /// + /// Tests for the Metadata service component + /// + public class MetadataServiceTests + { + private string testTableSchema = "dbo"; + private string testTableName = "MetadataTestTable"; + + private LiveConnectionHelper.TestConnectionResult GetLiveAutoCompleteTestObjects() + { + var textDocument = new TextDocumentPosition + { + TextDocument = new TextDocumentIdentifier { Uri = Constants.OwnerUri }, + Position = new Position + { + Line = 0, + Character = 0 + } + }; + + var result = LiveConnectionHelper.InitLiveConnectionInfo(); + result.TextDocumentPosition = textDocument; + return result; + } + + private void CreateTestTable(SqlConnection sqlConn) + { + string sql = string.Format("IF OBJECT_ID('{0}.{1}', 'U') IS NULL CREATE TABLE {0}.{1}(id int)", + this.testTableSchema, this.testTableName); + using (var sqlCommand = new SqlCommand(sql, sqlConn)) + { + sqlCommand.ExecuteNonQuery(); + } + } + + private void DeleteTestTable(SqlConnection sqlConn) + { + string sql = string.Format("IF OBJECT_ID('{0}.{1}', 'U') IS NOT NULL DROP TABLE {0}.{1}", + this.testTableSchema, this.testTableName); + using (var sqlCommand = new SqlCommand(sql, sqlConn)) + { + sqlCommand.ExecuteNonQuery(); + } + } + + /// + /// Verify that the metadata service correctly returns details for user tables + /// + [Fact] + public void MetadataReturnsUserTable() + { + this.testTableName += new Random().Next(1000000, 9999999).ToString(); + + var result = GetLiveAutoCompleteTestObjects(); + var sqlConn = MetadataService.OpenMetadataConnection(result.ConnectionInfo); + Assert.NotNull(sqlConn); + + CreateTestTable(sqlConn); + + var metadata = new List(); + MetadataService.ReadMetadata(sqlConn, metadata); + Assert.NotNull(metadata.Count > 0); + + bool foundTestTable = false; + foreach (var item in metadata) + { + if (string.Equals(item.Schema, this.testTableSchema, StringComparison.OrdinalIgnoreCase) + && string.Equals(item.Name, this.testTableName, StringComparison.OrdinalIgnoreCase)) + { + foundTestTable = true; + break; + } + } + Assert.True(foundTestTable); + + DeleteTestTable(sqlConn); + } + } +} diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Scripting/ScriptingServiceTests.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Scripting/ScriptingServiceTests.cs new file mode 100644 index 00000000..144066b9 --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Scripting/ScriptingServiceTests.cs @@ -0,0 +1,118 @@ +// +// 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.ServiceLayer.Workspace.Contracts; +using Xunit; +using Microsoft.SqlTools.ServiceLayer.IntegrationTests.Utility; +using Microsoft.SqlTools.ServiceLayer.Metadata.Contracts; +using Microsoft.SqlTools.ServiceLayer.Scripting; +using Moq; +using Microsoft.SqlTools.Hosting.Protocol; +using Microsoft.SqlTools.ServiceLayer.Scripting.Contracts; +using System.Threading.Tasks; + +namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.Scripting +{ + /// + /// Tests for the scripting service component + /// + public class ScriptingServiceTests + { + private const string SchemaName = "sys"; + private const string TableName = "all_objects"; + + private LiveConnectionHelper.TestConnectionResult GetLiveAutoCompleteTestObjects() + { + var textDocument = new TextDocumentPosition + { + TextDocument = new TextDocumentIdentifier { Uri = Test.Common.Constants.OwnerUri }, + Position = new Position + { + Line = 0, + Character = 0 + } + }; + + var result = LiveConnectionHelper.InitLiveConnectionInfo(); + result.TextDocumentPosition = textDocument; + return result; + } + + + private async Task>> SendAndValidateScriptRequest(ScriptOperation operation) + { + var result = GetLiveAutoCompleteTestObjects(); + var requestContext = new Mock>(); + requestContext.Setup(x => x.SendResult(It.IsAny())).Returns(Task.FromResult(new object())); + + var scriptingParams = new ScriptingScriptAsParams + { + OwnerUri = Test.Common.Constants.OwnerUri, + Operation = operation, + Metadata = new ObjectMetadata() + { + MetadataType = MetadataType.Table, + MetadataTypeName = "View", + Schema = SchemaName, + Name = TableName + } + }; + + await ScriptingService.HandleScriptingScriptAsRequest(scriptingParams, requestContext.Object); + + requestContext.Verify(x => x.SendResult(It.Is( + i => i.Script.Contains(operation.ToString().ToUpper()) + && i.Script.Contains(TableName) + && i.Script.Contains(SchemaName)))); + + return requestContext; + } + + /// + /// Verify the script as select request + /// + [Fact] + public async void ScriptingScriptAsSelect() + { + await SendAndValidateScriptRequest(ScriptOperation.Select); + } + + /// + /// Verify the script as create request + /// + [Fact] + public async void ScriptingScriptAsCreate() + { + await SendAndValidateScriptRequest(ScriptOperation.Create); + } + + /// + /// Verify the script as insert request + /// + [Fact] + public async void ScriptingScriptAsInsert() + { + await SendAndValidateScriptRequest(ScriptOperation.Insert); + } + + /// + /// Verify the script as update request + /// + [Fact] + public async void ScriptingScriptAsUpdate() + { + await SendAndValidateScriptRequest(ScriptOperation.Update); + } + + /// + /// Verify the script as delete request + /// + [Fact] + public async void ScriptingScriptAsDelete() + { + await SendAndValidateScriptRequest(ScriptOperation.Delete); + } + } +}