diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/ConnectionService.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/ConnectionService.cs
index 83b860a8..290a8680 100644
--- a/src/Microsoft.SqlTools.ServiceLayer/Connection/ConnectionService.cs
+++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/ConnectionService.cs
@@ -93,11 +93,21 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection
///
public delegate Task OnConnectionHandler(ConnectionInfo info);
+ ///
+ // Callback for ondisconnect handler
+ ///
+ public delegate Task OnDisconnectHandler(ConnectionSummary summary);
+
///
/// List of onconnection handlers
///
private readonly List onConnectionActivities = new List();
+ ///
+ /// List of ondisconnect handlers
+ ///
+ private readonly List onDisconnectActivities = new List();
+
///
/// Gets the SQL connection factory instance
///
@@ -127,16 +137,6 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection
{
return this.ownerToConnectionMap.TryGetValue(ownerUri, out connectionInfo);
}
-
- private static ConnectionSummary CopySummary(ConnectionSummary summary)
- {
- return new ConnectionSummary()
- {
- ServerName = summary.ServerName,
- DatabaseName = summary.DatabaseName,
- UserName = summary.UserName
- };
- }
///
/// Open a connection with the specified connection details
@@ -153,10 +153,16 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection
};
}
+ // Resolve if it is an existing connection
+ // Disconnect active connection if the URI is already connected
ConnectionInfo connectionInfo;
if (ownerToConnectionMap.TryGetValue(connectionParams.OwnerUri, out connectionInfo) )
{
- // TODO disconnect
+ var disconnectParams = new DisconnectParams()
+ {
+ OwnerUri = connectionParams.OwnerUri
+ };
+ Disconnect(disconnectParams);
}
connectionInfo = new ConnectionInfo(ConnectionFactory, connectionParams.OwnerUri, connectionParams.Connection);
@@ -185,10 +191,45 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection
return response;
}
+ ///
+ /// Close a connection with the specified connection details.
+ ///
+ public bool Disconnect(DisconnectParams disconnectParams)
+ {
+ // Validate parameters
+ if (disconnectParams == null || String.IsNullOrEmpty(disconnectParams.OwnerUri))
+ {
+ return false;
+ }
+
+ // Lookup the connection owned by the URI
+ ConnectionInfo info;
+ if (!ownerToConnectionMap.TryGetValue(disconnectParams.OwnerUri, out info))
+ {
+ return false;
+ }
+
+ // Close the connection
+ info.SqlConnection.Close();
+
+ // Remove URI mapping
+ ownerToConnectionMap.Remove(disconnectParams.OwnerUri);
+
+ // Invoke callback notifications
+ foreach (var activity in this.onDisconnectActivities)
+ {
+ activity(info.ConnectionDetails);
+ }
+
+ // Success
+ return true;
+ }
+
public void InitializeService(IProtocolEndpoint serviceHost)
{
// Register request and event handlers with the Service Host
serviceHost.SetRequestHandler(ConnectionRequest.Type, HandleConnectRequest);
+ serviceHost.SetRequestHandler(DisconnectRequest.Type, HandleDisconnectRequest);
// Register the configuration update handler
WorkspaceService.Instance.RegisterConfigChangeCallback(HandleDidChangeConfigurationNotification);
@@ -202,6 +243,14 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection
{
onConnectionActivities.Add(activity);
}
+
+ ///
+ /// Add a new method to be called when the ondisconnect request is submitted
+ ///
+ public void RegisterOnDisconnectTask(OnDisconnectHandler activity)
+ {
+ onDisconnectActivities.Add(activity);
+ }
///
/// Handle new connection requests
@@ -226,6 +275,27 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection
await requestContext.SendError(ex.Message);
}
}
+
+ ///
+ /// Handle disconnect requests
+ ///
+ protected async Task HandleDisconnectRequest(
+ DisconnectParams disconnectParams,
+ RequestContext requestContext)
+ {
+ Logger.Write(LogLevel.Verbose, "HandleDisconnectRequest");
+
+ try
+ {
+ bool result = ConnectionService.Instance.Disconnect(disconnectParams);
+ await requestContext.SendResult(result);
+ }
+ catch(Exception ex)
+ {
+ await requestContext.SendError(ex.Message);
+ }
+
+ }
public Task HandleDidChangeConfigurationNotification(
SqlToolsSettings newSettings,
diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectionMessages.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectionMessages.cs
index baa426e2..aa27da3e 100644
--- a/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectionMessages.cs
+++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectionMessages.cs
@@ -7,57 +7,57 @@ using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
namespace Microsoft.SqlTools.ServiceLayer.Connection.Contracts
{
- ///
- /// Parameters for the Connect Request.
+ ///
+ /// Parameters for the Connect Request.
///
- public class ConnectParams
- {
- ///
- /// A URI identifying the owner of the connection. This will most commonly be a file in the workspace
- /// or a virtual file representing an object in a database.
- ///
- public string OwnerUri { get; set; }
- ///
- /// Contains the required parameters to initialize a connection to a database.
- /// A connection will identified by its server name, database name and user name.
- /// This may be changed in the future to support multiple connections with different
- /// connection properties to the same database.
- ///
- public ConnectionDetails Connection { get; set; }
+ public class ConnectParams
+ {
+ ///
+ /// A URI identifying the owner of the connection. This will most commonly be a file in the workspace
+ /// or a virtual file representing an object in a database.
+ ///
+ public string OwnerUri { get; set; }
+ ///
+ /// Contains the required parameters to initialize a connection to a database.
+ /// A connection will identified by its server name, database name and user name.
+ /// This may be changed in the future to support multiple connections with different
+ /// connection properties to the same database.
+ ///
+ public ConnectionDetails Connection { get; set; }
}
- ///
- /// Parameters for the Disconnect Request.
+ ///
+ /// Parameters for the Disconnect Request.
///
- public class DisconnectParams
- {
- ///
- /// A URI identifying the owner of the connection. This will most commonly be a file in the workspace
- /// or a virtual file representing an object in a database.
- ///
- public string ownerUri { get; set; }
+ public class DisconnectParams
+ {
+ ///
+ /// A URI identifying the owner of the connection. This will most commonly be a file in the workspace
+ /// or a virtual file representing an object in a database.
+ ///
+ public string OwnerUri { get; set; }
}
- ///
- /// Parameters for the ConnectionChanged Notification.
+ ///
+ /// Parameters for the ConnectionChanged Notification.
///
- public class ConnectionChangedParams
- {
- ///
- /// A URI identifying the owner of the connection. This will most commonly be a file in the workspace
- /// or a virtual file representing an object in a database.
- ///
- public string ownerUri { get; set; }
- ///
- /// Contains the high-level properties about the connection, for display to the user.
- ///
- public ConnectionSummary Connection { get; set; }
+ public class ConnectionChangedParams
+ {
+ ///
+ /// A URI identifying the owner of the connection. This will most commonly be a file in the workspace
+ /// or a virtual file representing an object in a database.
+ ///
+ public string OwnerUri { get; set; }
+ ///
+ /// Contains the high-level properties about the connection, for display to the user.
+ ///
+ public ConnectionSummary Connection { get; set; }
}
- ///
- /// Provides high level information about a connection.
+ ///
+ /// Provides high level information about a connection.
///
- public class ConnectionSummary
+ public class ConnectionSummary
{
///
/// Gets or sets the connection server name
@@ -72,7 +72,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection.Contracts
///
/// Gets or sets the connection user name
///
- public string UserName { get; set; }
+ public string UserName { get; set; }
}
///
/// Message format for the initial connection request
@@ -102,8 +102,8 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection.Contracts
/// Gets or sets any connection error messages
///
public string Messages { get; set; }
- }
-
+ }
+
///
/// Connect request mapping entry
///
@@ -122,8 +122,8 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection.Contracts
public static readonly
RequestType Type =
RequestType.Create("connection/disconnect");
- }
-
+ }
+
///
/// ConnectionChanged notification mapping entry
///
diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteService.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteService.cs
index 347de0c7..cb866b2a 100644
--- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteService.cs
+++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteService.cs
@@ -21,8 +21,13 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
// connection used to query for intellisense info
private DbConnection connection;
+ // number of documents (URI's) that are using the cache for the same database
+ // the autocomplete service uses this to remove unreferenced caches
+ public int ReferenceCount { get; set; }
+
public IntellisenseCache(ISqlConnectionFactory connectionFactory, ConnectionDetails connectionDetails)
{
+ ReferenceCount = 0;
DatabaseInfo = CopySummary(connectionDetails);
// TODO error handling on this. Intellisense should catch or else the service should handle
@@ -201,6 +206,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
// Dictionary of unique intellisense caches for each Connection
private Dictionary caches =
new Dictionary(new ConnectionSummaryComparer());
+ private Object cachesLock = new Object(); // Used when we insert/remove something from the cache dictionary
private ISqlConnectionFactory factory;
private Object factoryLock = new Object();
@@ -233,6 +239,9 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
{
// Register a callback for when a connection is created
ConnectionService.Instance.RegisterOnConnectionTask(UpdateAutoCompleteCache);
+
+ // Register a callback for when a connection is closed
+ ConnectionService.Instance.RegisterOnDisconnectTask(RemoveAutoCompleteCacheUriReference);
}
private async Task UpdateAutoCompleteCache(ConnectionInfo connectionInfo)
@@ -243,18 +252,48 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
}
}
+ ///
+ /// Remove a reference to an autocomplete cache from a URI. If
+ /// it is the last URI connected to a particular connection,
+ /// then remove the cache.
+ ///
+ public async Task RemoveAutoCompleteCacheUriReference(ConnectionSummary summary)
+ {
+ await Task.Run( () =>
+ {
+ lock(cachesLock)
+ {
+ IntellisenseCache cache;
+ if( caches.TryGetValue(summary, out cache) )
+ {
+ cache.ReferenceCount--;
+
+ // Remove unused caches
+ if( cache.ReferenceCount == 0 )
+ {
+ caches.Remove(summary);
+ }
+ }
+ }
+ });
+ }
+
+
///
/// Update the cached autocomplete candidate list when the user connects to a database
- /// TODO: Update with refactoring/async
///
///
public async Task UpdateAutoCompleteCache(ConnectionDetails details)
{
IntellisenseCache cache;
- if(!caches.TryGetValue(details, out cache))
+ lock(cachesLock)
{
- cache = new IntellisenseCache(ConnectionFactory, details);
- caches[cache.DatabaseInfo] = cache;
+ if(!caches.TryGetValue(details, out cache))
+ {
+ cache = new IntellisenseCache(ConnectionFactory, details);
+ caches[cache.DatabaseInfo] = cache;
+ }
+ cache.ReferenceCount++;
}
await cache.UpdateCache();
diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs
index 35ee0ebd..6cdbd745 100644
--- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs
+++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs
@@ -103,10 +103,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
WorkspaceService.Instance.RegisterTextDocChangeCallback(HandleDidChangeTextDocumentNotification);
// Register the file open update handler
- WorkspaceService.Instance.RegisterTextDocOpenCallback(HandleDidOpenTextDocumentNotification);
-
- // register an OnConnection callback
- ConnectionService.Instance.RegisterOnConnectionTask(OnConnection);
+ WorkspaceService.Instance.RegisterTextDocOpenCallback(HandleDidOpenTextDocumentNotification);
// Store the SqlToolsContext for future use
Context = context;
@@ -305,16 +302,6 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
CurrentSettings.ScriptAnalysis.Update(newSettings.ScriptAnalysis, CurrentWorkspace.WorkspacePath);
}
- ///
- /// Callback for when a user connection is done processing
- ///
- ///
- public async Task OnConnection(ConnectionInfo connectionInfo)
- {
- // TODO consider whether this is needed at all - currently AutoComplete service handles its own updating
- await Task.FromResult(true);
- }
-
#endregion
#region Private Helpers
diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/Connection/ConnectionServiceTests.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/Connection/ConnectionServiceTests.cs
index d0c991f8..9e3d5339 100644
--- a/test/Microsoft.SqlTools.ServiceLayer.Test/Connection/ConnectionServiceTests.cs
+++ b/test/Microsoft.SqlTools.ServiceLayer.Test/Connection/ConnectionServiceTests.cs
@@ -19,6 +19,53 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.Connection
///
public class ConnectionServiceTests
{
+ ///
+ /// Verify that when a connection is started for a URI with an already existing
+ /// connection, we disconnect first before connecting.
+ ///
+ [Fact]
+ public void ConnectingWhenConnectionExistCausesDisconnectThenConnect()
+ {
+ bool callbackInvoked = false;
+
+ // first connect
+ string ownerUri = "file://my/sample/file.sql";
+ var connectionService = TestObjects.GetTestConnectionService();
+ var connectionResult =
+ connectionService
+ .Connect(new ConnectParams()
+ {
+ OwnerUri = ownerUri,
+ Connection = TestObjects.GetTestConnectionDetails()
+ });
+
+ // verify that we are connected
+ Assert.NotEmpty(connectionResult.ConnectionId);
+
+ // register disconnect callback
+ connectionService.RegisterOnDisconnectTask(
+ (result) => {
+ callbackInvoked = true;
+ return Task.FromResult(true);
+ }
+ );
+
+ // send annother connect request
+ connectionResult =
+ connectionService
+ .Connect(new ConnectParams()
+ {
+ OwnerUri = ownerUri,
+ Connection = TestObjects.GetTestConnectionDetails()
+ });
+
+ // verify that the event was fired (we disconnected first before connecting)
+ Assert.True(callbackInvoked);
+
+ // verify that we connected again
+ Assert.NotEmpty(connectionResult.ConnectionId);
+ }
+
///
/// Verify that when connecting with invalid credentials, an error is thrown.
///
@@ -117,6 +164,151 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.Connection
Assert.NotEmpty(connectionResult.ConnectionId);
}
+ ///
+ /// Verify that we can disconnect from an active connection succesfully
+ ///
+ [Fact]
+ public void DisconnectFromDatabaseTest()
+ {
+ // first connect
+ string ownerUri = "file://my/sample/file.sql";
+ var connectionService = TestObjects.GetTestConnectionService();
+ var connectionResult =
+ connectionService
+ .Connect(new ConnectParams()
+ {
+ OwnerUri = ownerUri,
+ Connection = TestObjects.GetTestConnectionDetails()
+ });
+
+ // verify that we are connected
+ Assert.NotEmpty(connectionResult.ConnectionId);
+
+ // send disconnect request
+ var disconnectResult =
+ connectionService
+ .Disconnect(new DisconnectParams()
+ {
+ OwnerUri = ownerUri
+ });
+ Assert.True(disconnectResult);
+ }
+
+ ///
+ /// Test that when a disconnect is performed, the callback event is fired
+ ///
+ [Fact]
+ public void DisconnectFiresCallbackEvent()
+ {
+ bool callbackInvoked = false;
+
+ // first connect
+ string ownerUri = "file://my/sample/file.sql";
+ var connectionService = TestObjects.GetTestConnectionService();
+ var connectionResult =
+ connectionService
+ .Connect(new ConnectParams()
+ {
+ OwnerUri = ownerUri,
+ Connection = TestObjects.GetTestConnectionDetails()
+ });
+
+ // verify that we are connected
+ Assert.NotEmpty(connectionResult.ConnectionId);
+
+ // register disconnect callback
+ connectionService.RegisterOnDisconnectTask(
+ (result) => {
+ callbackInvoked = true;
+ return Task.FromResult(true);
+ }
+ );
+
+ // send disconnect request
+ var disconnectResult =
+ connectionService
+ .Disconnect(new DisconnectParams()
+ {
+ OwnerUri = ownerUri
+ });
+ Assert.True(disconnectResult);
+
+ // verify that the event was fired
+ Assert.True(callbackInvoked);
+ }
+
+ ///
+ /// Test that disconnecting an active connection removes the Owner URI -> ConnectionInfo mapping
+ ///
+ [Fact]
+ public void DisconnectRemovesOwnerMapping()
+ {
+ // first connect
+ string ownerUri = "file://my/sample/file.sql";
+ var connectionService = TestObjects.GetTestConnectionService();
+ var connectionResult =
+ connectionService
+ .Connect(new ConnectParams()
+ {
+ OwnerUri = ownerUri,
+ Connection = TestObjects.GetTestConnectionDetails()
+ });
+
+ // verify that we are connected
+ Assert.NotEmpty(connectionResult.ConnectionId);
+
+ // check that the owner mapping exists
+ ConnectionInfo info;
+ Assert.True(connectionService.TryFindConnection(ownerUri, out info));
+
+ // send disconnect request
+ var disconnectResult =
+ connectionService
+ .Disconnect(new DisconnectParams()
+ {
+ OwnerUri = ownerUri
+ });
+ Assert.True(disconnectResult);
+
+ // check that the owner mapping no longer exists
+ Assert.False(connectionService.TryFindConnection(ownerUri, out info));
+ }
+
+ ///
+ /// Test that disconnecting validates parameters and doesn't succeed when they are invalid
+ ///
+ [Theory]
+ [InlineDataAttribute(null)]
+ [InlineDataAttribute("")]
+
+ public void DisconnectValidatesParameters(string disconnectUri)
+ {
+ // first connect
+ string ownerUri = "file://my/sample/file.sql";
+ var connectionService = TestObjects.GetTestConnectionService();
+ var connectionResult =
+ connectionService
+ .Connect(new ConnectParams()
+ {
+ OwnerUri = ownerUri,
+ Connection = TestObjects.GetTestConnectionDetails()
+ });
+
+ // verify that we are connected
+ Assert.NotEmpty(connectionResult.ConnectionId);
+
+ // send disconnect request
+ var disconnectResult =
+ connectionService
+ .Disconnect(new DisconnectParams()
+ {
+ OwnerUri = disconnectUri
+ });
+
+ // verify that disconnect failed
+ Assert.False(disconnectResult);
+ }
+
///
/// Verify that the SQL parser correctly detects errors in text
///
@@ -151,24 +343,6 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.Connection
var service = TestObjects.GetTestConnectionService();
var connectParams = TestObjects.GetTestConnectionParams();
- //var endpoint = new Mock();
- //Func, Task> connectRequestHandler = null;
- //endpoint.Setup(e => e.SetRequestHandler(ConnectionRequest.Type, It.IsAny, Task>>()))
- // .Callback, Task>>(handler => connectRequestHandler = handler);
-
- // when I initialize the service
- //service.InitializeService(endpoint.Object);
-
- // then I expect the handler to be captured
- //Assert.NotNull(connectRequestHandler);
-
- // when I call the service
- //var requestContext = new Mock>();
-
- //connectRequestHandler(connectParams, requestContext.Object);
- // then I should get a live connection
-
- // and then I should have
// connect to a database instance
var connectionResult = service.Connect(connectParams);
diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/Utility/TestObjects.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/Utility/TestObjects.cs
index cda0ed5a..b973bfd9 100644
--- a/test/Microsoft.SqlTools.ServiceLayer.Test/Utility/TestObjects.cs
+++ b/test/Microsoft.SqlTools.ServiceLayer.Test/Utility/TestObjects.cs
@@ -322,7 +322,7 @@ namespace Microsoft.SqlTools.Test.Utility
public override void Close()
{
- throw new NotImplementedException();
+ // No Op
}
public override void Open()