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": {