diff --git a/src/ServiceHost/Connection/ConnectionMessages.cs b/src/ServiceHost/Connection/ConnectionMessages.cs new file mode 100644 index 00000000..814e55f0 --- /dev/null +++ b/src/ServiceHost/Connection/ConnectionMessages.cs @@ -0,0 +1,63 @@ +// +// 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.EditorServices.Protocol.MessageProtocol; + +namespace Microsoft.SqlTools.EditorServices.Connection +{ + /// + /// Message format for the initial connection request + /// + public class ConnectionDetails + { + /// + /// Gets or sets the connection server name + /// + public string ServerName { get; set; } + + /// + /// Gets or sets the connection database name + /// + public string DatabaseName { get; set; } + + /// + /// Gets or sets the connection user name + /// + public string UserName { get; set; } + + /// + /// Gets or sets the connection password + /// + /// + public string Password { get; set; } + } + + /// + /// Message format for the connection result response + /// + public class ConnectionResult + { + /// + /// Gets or sets the connection id + /// + public int ConnectionId { get; set; } + + /// + /// Gets or sets any connection error messages + /// + public string Messages { get; set; } + } + + /// + /// Connect request mapping entry + /// + public class ConnectionRequest + { + public static readonly + RequestType Type = + RequestType.Create("connection/connect"); + } + +} diff --git a/src/ServiceHost/Connection/ConnectionService.cs b/src/ServiceHost/Connection/ConnectionService.cs new file mode 100644 index 00000000..8f0634a7 --- /dev/null +++ b/src/ServiceHost/Connection/ConnectionService.cs @@ -0,0 +1,160 @@ +// +// 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; + +namespace Microsoft.SqlTools.EditorServices.Connection +{ + /// + /// Main class for the Connection Management services + /// + public class ConnectionService + { + /// + /// Singleton service instance + /// + private static Lazy instance + = new Lazy(() => new ConnectionService()); + + /// + /// The SQL connection factory object + /// + private ISqlConnectionFactory connectionFactory; + + /// + /// The current connection id that was previously used + /// + private int maxConnectionId = 0; + + /// + /// Active connections lazy dictionary instance + /// + private Lazy> activeConnections + = new Lazy>(() + => new Dictionary()); + + /// + /// Callback for onconnection handler + /// + /// + public delegate Task OnConnectionHandler(ISqlConnection sqlConnection); + + /// + /// List of onconnection handlers + /// + private readonly List onConnectionActivities = new List(); + + /// + /// Gets the active connection map + /// + public Dictionary ActiveConnections + { + get + { + return activeConnections.Value; + } + } + + /// + /// Gets the singleton service instance + /// + public static ConnectionService Instance + { + get + { + return instance.Value; + } + } + + /// + /// Gets the SQL connection factory instance + /// + public ISqlConnectionFactory ConnectionFactory + { + get + { + if (this.connectionFactory == null) + { + this.connectionFactory = new SqlConnectionFactory(); + } + return this.connectionFactory; + } + } + + /// + /// Default constructor is private since it's a singleton class + /// + private ConnectionService() + { + } + + /// + /// Test constructor that injects dependency interfaces + /// + /// + public ConnectionService(ISqlConnectionFactory testFactory) + { + this.connectionFactory = testFactory; + } + + /// + /// Open a connection with the specified connection details + /// + /// + public ConnectionResult Connect(ConnectionDetails connectionDetails) + { + // build the connection string from the input parameters + string connectionString = BuildConnectionString(connectionDetails); + + // create a sql connection instance + ISqlConnection connection = this.ConnectionFactory.CreateSqlConnection(); + + // open the database + connection.OpenDatabaseConnection(connectionString); + + // map the connection id to the connection object for future lookups + this.ActiveConnections.Add(++maxConnectionId, connection); + + // invoke callback notifications + foreach (var activity in this.onConnectionActivities) + { + activity(connection); + } + + // return the connection result + return new ConnectionResult() + { + ConnectionId = maxConnectionId + }; + } + + /// + /// Add a new method to be called when the onconnection request is submitted + /// + /// + public void RegisterOnConnectionTask(OnConnectionHandler activity) + { + onConnectionActivities.Add(activity); + } + + /// + /// Build a connection string from a connection details instance + /// + /// + private string BuildConnectionString(ConnectionDetails connectionDetails) + { + SqlConnectionStringBuilder connectionBuilder = new SqlConnectionStringBuilder(); + connectionBuilder["Data Source"] = connectionDetails.ServerName; + connectionBuilder["Integrated Security"] = false; + connectionBuilder["User Id"] = connectionDetails.UserName; + connectionBuilder["Password"] = connectionDetails.Password; + connectionBuilder["Initial Catalog"] = connectionDetails.DatabaseName; + return connectionBuilder.ToString(); + } + } +} diff --git a/src/ServiceHost/Connection/ISqlConnection.cs b/src/ServiceHost/Connection/ISqlConnection.cs new file mode 100644 index 00000000..4ef80c5e --- /dev/null +++ b/src/ServiceHost/Connection/ISqlConnection.cs @@ -0,0 +1,34 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; + +namespace Microsoft.SqlTools.EditorServices.Connection +{ + /// + /// Interface for the SQL Connection factory + /// + public interface ISqlConnectionFactory + { + /// + /// Create a new SQL Connection object + /// + ISqlConnection CreateSqlConnection(); + } + + /// + /// Interface for the SQL Connection wrapper + /// + public interface ISqlConnection + { + /// + /// Open a connection to the provided connection string + /// + /// + void OpenDatabaseConnection(string connectionString); + + IEnumerable GetServerObjects(); + } +} diff --git a/src/ServiceHost/Connection/SqlConnection.cs b/src/ServiceHost/Connection/SqlConnection.cs new file mode 100644 index 00000000..74104fb9 --- /dev/null +++ b/src/ServiceHost/Connection/SqlConnection.cs @@ -0,0 +1,70 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; +using System.Data; +using System.Data.SqlClient; + +namespace Microsoft.SqlTools.EditorServices.Connection +{ + /// + /// Factory class to create SqlClientConnections + /// The purpose of the factory is to make it easier to mock out the database + /// in 'offline' unit test scenarios. + /// + public class SqlConnectionFactory : ISqlConnectionFactory + { + /// + /// Creates a new SqlClientConnection object + /// + public ISqlConnection CreateSqlConnection() + { + return new SqlClientConnection(); + } + } + + /// + /// Wrapper class that implements ISqlConnection and hosts a SqlConnection. + /// This wrapper exists primarily for decoupling to support unit testing. + /// + public class SqlClientConnection : ISqlConnection + { + /// + /// the underlying SQL connection + /// + private SqlConnection connection; + + /// + /// Opens a SqlConnection using provided connection string + /// + /// + public void OpenDatabaseConnection(string connectionString) + { + this.connection = new SqlConnection(connectionString); + this.connection.Open(); + } + + /// + /// Gets a list of database server schema objects + /// + /// + public IEnumerable GetServerObjects() + { + SqlCommand command = connection.CreateCommand(); + command.CommandText = "SELECT name FROM sys.objects"; + command.CommandTimeout = 15; + command.CommandType = CommandType.Text; + var reader = command.ExecuteReader(); + + List results = new List(); + while (reader.Read()) + { + results.Add(reader[0].ToString()); + } + + return results; + } + } +} diff --git a/src/ServiceHost/LanguageSupport/AutoCompleteService.cs b/src/ServiceHost/LanguageSupport/AutoCompleteService.cs new file mode 100644 index 00000000..48442b64 --- /dev/null +++ b/src/ServiceHost/LanguageSupport/AutoCompleteService.cs @@ -0,0 +1,49 @@ +// +// 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.EditorServices.Connection; +using System; +using System.Collections.Generic; + +namespace Microsoft.SqlTools.LanguageSupport +{ + /// + /// Main class for Autocomplete functionality + /// + public class AutoCompleteService + { + /// + /// Singleton service instance + /// + private static Lazy instance + = new Lazy(() => new AutoCompleteService()); + + private IEnumerable autoCompleteList; + + public IEnumerable AutoCompleteList + { + get + { + return this.autoCompleteList; + } + } + + /// + /// Gets the singleton service instance + /// + public static AutoCompleteService Instance + { + get + { + return instance.Value; + } + } + + public void UpdateAutoCompleteCache(ISqlConnection connection) + { + this.autoCompleteList = connection.GetServerObjects(); + } + } +} diff --git a/src/ServiceHost/Server/LanguageServer.cs b/src/ServiceHost/Server/LanguageServer.cs index 5dbbc310..eb054b45 100644 --- a/src/ServiceHost/Server/LanguageServer.cs +++ b/src/ServiceHost/Server/LanguageServer.cs @@ -13,6 +13,8 @@ using System.Text; using System.Threading; using System.Linq; using System; +using Microsoft.SqlTools.EditorServices.Connection; +using Microsoft.SqlTools.LanguageSupport; namespace Microsoft.SqlTools.EditorServices.Protocol.Server { @@ -57,7 +59,22 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.Server this.SetRequestHandler(DocumentHighlightRequest.Type, this.HandleDocumentHighlightRequest); this.SetRequestHandler(HoverRequest.Type, this.HandleHoverRequest); this.SetRequestHandler(DocumentSymbolRequest.Type, this.HandleDocumentSymbolRequest); - this.SetRequestHandler(WorkspaceSymbolRequest.Type, this.HandleWorkspaceSymbolRequest); + this.SetRequestHandler(WorkspaceSymbolRequest.Type, this.HandleWorkspaceSymbolRequest); + + this.SetRequestHandler(ConnectionRequest.Type, this.HandleConnectRequest); + + // register an OnConnection callback + ConnectionService.Instance.RegisterOnConnectionTask(OnConnection); + } + + /// + /// Callback for when a user connection is done processing + /// + /// + public Task OnConnection(ISqlConnection sqlConnection) + { + AutoCompleteService.Instance.UpdateAutoCompleteCache(sqlConnection); + return Task.FromResult(true); } /// @@ -245,7 +262,57 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.Server RequestContext requestContext) { Logger.Write(LogLevel.Verbose, "HandleCompletionRequest"); - await Task.FromResult(true); + + var connectionService = ConnectionService.Instance; + if (connectionService.ActiveConnections.Count > 0) + { + AutoCompleteService.Instance.UpdateAutoCompleteCache( + connectionService.ActiveConnections.First().Value); + } + + var autoCompleteList = AutoCompleteService.Instance.AutoCompleteList; + var completions = new List(); + + int i = 0; + if (autoCompleteList != null) + { + foreach (var autoCompleteItem in autoCompleteList) + { + completions.Add(new CompletionItem() + { + Label = autoCompleteItem, + Kind = CompletionItemKind.Keyword, + Detail = autoCompleteItem + " details", + Documentation = autoCompleteItem + " documentation", + //SortText = "SortText", + TextEdit = new TextEdit + { + NewText = "New Text", + Range = new Range + { + Start = new Position + { + Line = textDocumentPosition.Position.Line, + Character = textDocumentPosition.Position.Character + }, + End = new Position + { + Line = textDocumentPosition.Position.Line, + Character = textDocumentPosition.Position.Character + 5 + } + } + } + }); + + // only show 50 items + if (++i == 50) + { + break; + } + } + } + + await requestContext.SendResult(completions.ToArray()); } protected async Task HandleCompletionResolveRequest( @@ -296,6 +363,24 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.Server await Task.FromResult(true); } + /// + /// Handle new connection requests + /// + /// + /// + /// + protected async Task HandleConnectRequest( + ConnectionDetails connectionDetails, + RequestContext requestContext) + { + Logger.Write(LogLevel.Verbose, "HandleConnectRequest"); + + // open connection base on request details + ConnectionResult result = ConnectionService.Instance.Connect(connectionDetails); + + await requestContext.SendResult(result); + } + /// /// Runs script diagnostics on changed files /// diff --git a/src/ServiceHost/project.json b/src/ServiceHost/project.json index 91353567..31dac66d 100644 --- a/src/ServiceHost/project.json +++ b/src/ServiceHost/project.json @@ -6,7 +6,9 @@ }, "dependencies": { "Newtonsoft.Json": "9.0.1", - "Microsoft.SqlServer.SqlParser": "140.1.3" + "Microsoft.SqlServer.SqlParser": "140.1.3", + "System.Data.Common": "4.1.0", + "System.Data.SqlClient": "4.1.0" }, "frameworks": { "netcoreapp1.0": { diff --git a/test/ServiceHost.Test/Connection/ConnectionServiceTests.cs b/test/ServiceHost.Test/Connection/ConnectionServiceTests.cs new file mode 100644 index 00000000..b7356b28 --- /dev/null +++ b/test/ServiceHost.Test/Connection/ConnectionServiceTests.cs @@ -0,0 +1,63 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +#define USE_LIVE_CONNECTION + +using System.Threading.Tasks; +using Microsoft.SqlTools.Test.Utility; +using Xunit; + +namespace Microsoft.SqlTools.Test.Connection +{ + /// + /// Tests for the ServiceHost Connection Service tests + /// + public class ConnectionServiceTests + { + #region "Connection tests" + + /// + /// Verify that the SQL parser correctly detects errors in text + /// + [Fact] + public void ConnectToDatabaseTest() + { + // connect to a database instance + var connectionResult = + TestObjects.GetTestConnectionService() + .Connect(TestObjects.GetTestConnectionDetails()); + + // verify that a valid connection id was returned + Assert.True(connectionResult.ConnectionId > 0); + } + + /// + /// Verify that the SQL parser correctly detects errors in text + /// + [Fact] + public void OnConnectionCallbackHandlerTest() + { + bool callbackInvoked = false; + + // setup connection service with callback + var connectionService = TestObjects.GetTestConnectionService(); + connectionService.RegisterOnConnectionTask( + (sqlConnection) => { + callbackInvoked = true; + return Task.FromResult(true); + } + ); + + // connect to a database instance + var connectionResult = TestObjects.GetTestConnectionService() + .Connect(TestObjects.GetTestConnectionDetails()); + + // verify that a valid connection id was returned + Assert.True(callbackInvoked); + } + + #endregion + } +} diff --git a/test/ServiceHost.Test/LanguageServer/LanguageServiceTests.cs b/test/ServiceHost.Test/LanguageServer/LanguageServiceTests.cs index 70fd2acd..36aac6b4 100644 --- a/test/ServiceHost.Test/LanguageServer/LanguageServiceTests.cs +++ b/test/ServiceHost.Test/LanguageServer/LanguageServiceTests.cs @@ -6,6 +6,8 @@ using Microsoft.SqlTools.EditorServices; using Microsoft.SqlTools.EditorServices.Session; using Microsoft.SqlTools.LanguageSupport; +using Microsoft.SqlTools.Test.Connection; +using Microsoft.SqlTools.Test.Utility; using Xunit; namespace Microsoft.SqlTools.Test.LanguageServer @@ -15,15 +17,6 @@ namespace Microsoft.SqlTools.Test.LanguageServer /// public class LanguageServiceTests { - /// - /// Create a test language service instance - /// - /// - private LanguageService CreateTestService() - { - return new LanguageService(new SqlToolsContext(null, null)); - } - #region "Diagnostics tests" /// @@ -36,7 +29,7 @@ namespace Microsoft.SqlTools.Test.LanguageServer const string sqlWithErrors = "SELECT * FROM sys.objects"; // get the test service - LanguageService service = CreateTestService(); + LanguageService service = TestObjects.GetTestLanguageService(); // parse the sql statement var scriptFile = new ScriptFile(); @@ -57,7 +50,7 @@ namespace Microsoft.SqlTools.Test.LanguageServer const string sqlWithErrors = "SELECT *** FROM sys.objects"; // get test service - LanguageService service = CreateTestService(); + LanguageService service = TestObjects.GetTestLanguageService(); // parse sql statement var scriptFile = new ScriptFile(); @@ -87,7 +80,7 @@ namespace Microsoft.SqlTools.Test.LanguageServer "SELECT *** FROM sys.objects;\n"; // get test service - LanguageService service = CreateTestService(); + LanguageService service = TestObjects.GetTestLanguageService(); // parse sql var scriptFile = new ScriptFile(); @@ -111,6 +104,23 @@ namespace Microsoft.SqlTools.Test.LanguageServer } #endregion + + #region "Autocomplete Tests" + + /// + /// Verify that the SQL parser correctly detects errors in text + /// + [Fact] + public void AutocompleteTest() + { + var autocompleteService = TestObjects.GetAutoCompleteService(); + var connectionService = TestObjects.GetTestConnectionService(); + var connectionResult = connectionService.Connect(TestObjects.GetTestConnectionDetails()); + var sqlConnection = connectionService.ActiveConnections[connectionResult.ConnectionId]; + autocompleteService.UpdateAutoCompleteCache(sqlConnection); + } + + #endregion } } diff --git a/test/ServiceHost.Test/Message/TestMessageTypes.cs b/test/ServiceHost.Test/Message/TestMessageTypes.cs index cc5981dc..75cf1291 100644 --- a/test/ServiceHost.Test/Message/TestMessageTypes.cs +++ b/test/ServiceHost.Test/Message/TestMessageTypes.cs @@ -4,7 +4,6 @@ // using Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol; -using System; using System.Threading.Tasks; namespace Microsoft.SqlTools.EditorServices.Test.Protocol.MessageProtocol diff --git a/test/ServiceHost.Test/Utility/TestObjects.cs b/test/ServiceHost.Test/Utility/TestObjects.cs new file mode 100644 index 00000000..22fc3f4d --- /dev/null +++ b/test/ServiceHost.Test/Utility/TestObjects.cs @@ -0,0 +1,108 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +//#define USE_LIVE_CONNECTION + +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.SqlTools.EditorServices.Connection; +using Microsoft.SqlTools.EditorServices.Session; +using Microsoft.SqlTools.LanguageSupport; +using Xunit; + +namespace Microsoft.SqlTools.Test.Utility +{ + /// + /// Tests for the ServiceHost Connection Service tests + /// + public class TestObjects + { + /// + /// Creates a test connection service + /// + public static ConnectionService GetTestConnectionService() + { +#if !USE_LIVE_CONNECTION + // use mock database connection + return new ConnectionService(new TestSqlConnectionFactory()); +#else + // connect to a real server instance + return ConnectionService.Instance; +#endif + } + + /// + /// Creates a test connection details object + /// + public static ConnectionDetails GetTestConnectionDetails() + { + return new ConnectionDetails() + { + UserName = "sa", + Password = "Yukon900", + DatabaseName = "AdventureWorks2016CTP3_2", + ServerName = "sqltools11" + }; + } + + /// + /// Create a test language service instance + /// + /// + public static LanguageService GetTestLanguageService() + { + return new LanguageService(new SqlToolsContext(null, null)); + } + + /// + /// Creates a test autocomplete service instance + /// + public static AutoCompleteService GetAutoCompleteService() + { + return AutoCompleteService.Instance; + } + + /// + /// Creates a test sql connection factory instance + /// + public static ISqlConnectionFactory GetTestSqlConnectionFactory() + { +#if !USE_LIVE_CONNECTION + // use mock database connection + return new TestSqlConnectionFactory(); +#else + // connect to a real server instance + return ConnectionService.Instance.ConnectionFactory; +#endif + + } + } + + /// + /// Test mock class for SqlConnection wrapper + /// + public class TestSqlConnection : ISqlConnection + { + public void OpenDatabaseConnection(string connectionString) + { + } + + public IEnumerable GetServerObjects() + { + return null; + } + } + + /// + /// Test mock class for SqlConnection factory + /// + public class TestSqlConnectionFactory : ISqlConnectionFactory + { + public ISqlConnection CreateSqlConnection() + { + return new TestSqlConnection(); + } + } +} diff --git a/test/ServiceHost.Test/project.json b/test/ServiceHost.Test/project.json index b7f00724..947660d5 100644 --- a/test/ServiceHost.Test/project.json +++ b/test/ServiceHost.Test/project.json @@ -6,11 +6,13 @@ "dependencies": { "Newtonsoft.Json": "9.0.1", "System.Runtime.Serialization.Primitives": "4.1.1", + "System.Data.Common": "4.1.0", + "System.Data.SqlClient": "4.1.0", "xunit": "2.1.0", "dotnet-test-xunit": "1.0.0-rc2-192208-24", - "ServiceHost": { - "target": "project" - } + "ServiceHost": { + "target": "project" + } }, "testRunner": "xunit", "frameworks": {