diff --git a/.gitignore b/.gitignore
index a52a0fe0..97e1cc41 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,6 +14,10 @@ project.lock.json
*.userosscache
*.sln.docstates
*.exe
+scratch.txt
+
+# mergetool conflict files
+*.orig
# Build results
[Dd]ebug/
@@ -52,6 +56,7 @@ cross/rootfs/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
+test*json
#NUNIT
*.VisualState.xml
diff --git a/nuget.config b/nuget.config
index edd564a3..f5d41658 100644
--- a/nuget.config
+++ b/nuget.config
@@ -1,6 +1,7 @@
+
diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/ConnectionService.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/ConnectionService.cs
index 6cdd62aa..36e86791 100644
--- a/src/Microsoft.SqlTools.ServiceLayer/Connection/ConnectionService.cs
+++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/ConnectionService.cs
@@ -4,10 +4,12 @@
//
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using System.Data.SqlClient;
+using System.Threading;
using System.Threading.Tasks;
using Microsoft.SqlTools.ServiceLayer.Connection.Contracts;
using Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection;
@@ -47,6 +49,22 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection
private Dictionary ownerToConnectionMap = new Dictionary();
+ private ConcurrentDictionary ownerToCancellationTokenSourceMap = new ConcurrentDictionary();
+
+ private Object cancellationTokenSourceLock = new Object();
+
+ ///
+ /// Map from script URIs to ConnectionInfo objects
+ /// This is internal for testing access only
+ ///
+ internal Dictionary OwnerToConnectionMap
+ {
+ get
+ {
+ return this.ownerToConnectionMap;
+ }
+ }
+
///
/// Service host object for sending/receiving requests/events.
/// Internal for testing purposes.
@@ -119,21 +137,22 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection
/// Open a connection with the specified connection details
///
///
- public ConnectResponse Connect(ConnectParams connectionParams)
+ public async Task Connect(ConnectParams connectionParams)
{
// Validate parameters
string paramValidationErrorMessage;
if (connectionParams == null)
{
- return new ConnectResponse
+ return new ConnectionCompleteParams
{
Messages = SR.ConnectionServiceConnectErrorNullParams
};
}
if (!connectionParams.IsValid(out paramValidationErrorMessage))
{
- return new ConnectResponse
+ return new ConnectionCompleteParams
{
+ OwnerUri = connectionParams.OwnerUri,
Messages = paramValidationErrorMessage
};
}
@@ -152,7 +171,9 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection
connectionInfo = new ConnectionInfo(ConnectionFactory, connectionParams.OwnerUri, connectionParams.Connection);
// try to connect
- var response = new ConnectResponse();
+ var response = new ConnectionCompleteParams();
+ response.OwnerUri = connectionParams.OwnerUri;
+ CancellationTokenSource source = null;
try
{
// build the connection string from the input parameters
@@ -160,13 +181,75 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection
// create a sql connection instance
connectionInfo.SqlConnection = connectionInfo.Factory.CreateSqlConnection(connectionString);
- connectionInfo.SqlConnection.Open();
+
+ // turning on MARS to avoid break in LanguageService with multiple editors
+ // we'll remove this once ConnectionService is refactored to not own the LanguageService connection
+ connectionInfo.ConnectionDetails.MultipleActiveResultSets = true;
+
+ // Add a cancellation token source so that the connection OpenAsync() can be cancelled
+ using (source = new CancellationTokenSource())
+ {
+ // Locking here to perform two operations as one atomic operation
+ lock (cancellationTokenSourceLock)
+ {
+ // If the URI is currently connecting from a different request, cancel it before we try to connect
+ CancellationTokenSource currentSource;
+ if (ownerToCancellationTokenSourceMap.TryGetValue(connectionParams.OwnerUri, out currentSource))
+ {
+ currentSource.Cancel();
+ }
+ ownerToCancellationTokenSourceMap[connectionParams.OwnerUri] = source;
+ }
+
+ // Create a task to handle cancellation requests
+ var cancellationTask = Task.Run(() =>
+ {
+ source.Token.WaitHandle.WaitOne();
+ source.Token.ThrowIfCancellationRequested();
+ });
+
+ var openTask = Task.Run(async () => {
+ await connectionInfo.SqlConnection.OpenAsync(source.Token);
+ });
+
+ // Open the connection
+ await Task.WhenAny(openTask, cancellationTask).Unwrap();
+ source.Cancel();
+ }
}
- catch(Exception ex)
+ catch (SqlException ex)
{
+ response.ErrorNumber = ex.Number;
+ response.ErrorMessage = ex.Message;
response.Messages = ex.ToString();
return response;
}
+ catch (OperationCanceledException)
+ {
+ // OpenAsync was cancelled
+ response.Messages = SR.ConnectionServiceConnectionCanceled;
+ return response;
+ }
+ catch (Exception ex)
+ {
+ response.ErrorMessage = ex.Message;
+ response.Messages = ex.ToString();
+ return response;
+ }
+ finally
+ {
+ // Remove our cancellation token from the map since we're no longer connecting
+ // Using a lock here to perform two operations as one atomic operation
+ lock (cancellationTokenSourceLock)
+ {
+ // Only remove the token from the map if it is the same one created by this request
+ CancellationTokenSource sourceValue;
+ if (ownerToCancellationTokenSourceMap.TryGetValue(connectionParams.OwnerUri, out sourceValue) && sourceValue == source)
+ {
+ ownerToCancellationTokenSourceMap.TryRemove(connectionParams.OwnerUri, out sourceValue);
+ }
+ }
+ }
ownerToConnectionMap[connectionParams.OwnerUri] = connectionInfo;
@@ -181,15 +264,15 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection
};
// invoke callback notifications
- foreach (var activity in this.onConnectionActivities)
- {
- activity(connectionInfo);
- }
+ invokeOnConnectionActivities(connectionInfo);
// try to get information about the connected SQL Server instance
try
{
- ReliableConnectionHelper.ServerInfo serverInfo = ReliableConnectionHelper.GetServerVersion(connectionInfo.SqlConnection);
+ var reliableConnection = connectionInfo.SqlConnection as ReliableSqlConnection;
+ DbConnection connection = reliableConnection != null ? reliableConnection.GetUnderlyingConnection() : connectionInfo.SqlConnection;
+
+ ReliableConnectionHelper.ServerInfo serverInfo = ReliableConnectionHelper.GetServerVersion(connection);
response.ServerInfo = new Contracts.ServerInfo()
{
ServerMajorVersion = serverInfo.ServerMajorVersion,
@@ -214,6 +297,37 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection
return response;
}
+ ///
+ /// Cancel a connection that is in the process of opening.
+ ///
+ public bool CancelConnect(CancelConnectParams cancelParams)
+ {
+ // Validate parameters
+ if (cancelParams == null || string.IsNullOrEmpty(cancelParams.OwnerUri))
+ {
+ return false;
+ }
+
+ // Cancel any current connection attempts for this URI
+ CancellationTokenSource source;
+ if (ownerToCancellationTokenSourceMap.TryGetValue(cancelParams.OwnerUri, out source))
+ {
+ try
+ {
+ source.Cancel();
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+ else
+ {
+ return false;
+ }
+ }
+
///
/// Close a connection with the specified connection details.
///
@@ -225,6 +339,12 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection
return false;
}
+ // Cancel if we are in the middle of connecting
+ if (CancelConnect(new CancelConnectParams() { OwnerUri = disconnectParams.OwnerUri }))
+ {
+ return false;
+ }
+
// Lookup the connection owned by the URI
ConnectionInfo info;
if (!ownerToConnectionMap.TryGetValue(disconnectParams.OwnerUri, out info))
@@ -274,7 +394,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection
connection.Open();
DbCommand command = connection.CreateCommand();
- command.CommandText = "SELECT name FROM sys.databases";
+ command.CommandText = "SELECT name FROM sys.databases ORDER BY database_id ASC";
command.CommandTimeout = 15;
command.CommandType = CommandType.Text;
var reader = command.ExecuteReader();
@@ -299,6 +419,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection
// Register request and event handlers with the Service Host
serviceHost.SetRequestHandler(ConnectionRequest.Type, HandleConnectRequest);
+ serviceHost.SetRequestHandler(CancelConnectRequest.Type, HandleCancelConnectRequest);
serviceHost.SetRequestHandler(DisconnectRequest.Type, HandleDisconnectRequest);
serviceHost.SetRequestHandler(ListDatabasesRequest.Type, HandleListDatabasesRequest);
@@ -331,14 +452,55 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection
///
protected async Task HandleConnectRequest(
ConnectParams connectParams,
- RequestContext requestContext)
+ RequestContext requestContext)
{
Logger.Write(LogLevel.Verbose, "HandleConnectRequest");
try
{
- // open connection base on request details
- ConnectResponse result = ConnectionService.Instance.Connect(connectParams);
+ RunConnectRequestHandlerTask(connectParams, requestContext);
+ await requestContext.SendResult(true);
+ }
+ catch
+ {
+ await requestContext.SendResult(false);
+ }
+ }
+
+ private void RunConnectRequestHandlerTask(ConnectParams connectParams, RequestContext requestContext)
+ {
+ // create a task to connect asynchronously so that other requests are not blocked in the meantime
+ Task.Run(async () =>
+ {
+ try
+ {
+ // open connection based on request details
+ ConnectionCompleteParams result = await ConnectionService.Instance.Connect(connectParams);
+ await ServiceHost.SendEvent(ConnectionCompleteNotification.Type, result);
+ }
+ catch (Exception ex)
+ {
+ ConnectionCompleteParams result = new ConnectionCompleteParams()
+ {
+ Messages = ex.ToString()
+ };
+ await ServiceHost.SendEvent(ConnectionCompleteNotification.Type, result);
+ }
+ });
+ }
+
+ ///
+ /// Handle cancel connect requests
+ ///
+ protected async Task HandleCancelConnectRequest(
+ CancelConnectParams cancelParams,
+ RequestContext requestContext)
+ {
+ Logger.Write(LogLevel.Verbose, "HandleCancelConnectRequest");
+
+ try
+ {
+ bool result = ConnectionService.Instance.CancelConnect(cancelParams);
await requestContext.SendResult(result);
}
catch(Exception ex)
@@ -563,5 +725,14 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection
}
}
}
+
+ private void invokeOnConnectionActivities(ConnectionInfo connectionInfo)
+ {
+ foreach (var activity in this.onConnectionActivities)
+ {
+ // not awaiting here to allow handlers to run in the background
+ activity(connectionInfo);
+ }
+ }
}
}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/CancelConnectParams.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/CancelConnectParams.cs
new file mode 100644
index 00000000..9f2efdb0
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/CancelConnectParams.cs
@@ -0,0 +1,19 @@
+//
+// 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.Connection.Contracts
+{
+ ///
+ /// Parameters for the Cancel Connect Request.
+ ///
+ public class CancelConnectParams
+ {
+ ///
+ /// 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; }
+ }
+}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/CancelConnectRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/CancelConnectRequest.cs
new file mode 100644
index 00000000..a284f317
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/CancelConnectRequest.cs
@@ -0,0 +1,19 @@
+//
+// 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.Hosting.Protocol.Contracts;
+
+namespace Microsoft.SqlTools.ServiceLayer.Connection.Contracts
+{
+ ///
+ /// Cancel connect request mapping entry
+ ///
+ public class CancelConnectRequest
+ {
+ public static readonly
+ RequestType Type =
+ RequestType.Create("connection/cancelconnect");
+ }
+}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectResponse.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectResponse.cs
deleted file mode 100644
index 9066efa8..00000000
--- a/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectResponse.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-//
-// 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.Connection.Contracts
-{
- ///
- /// Message format for the connection result response
- ///
- public class ConnectResponse
- {
- ///
- /// A GUID representing a unique connection ID
- ///
- public string ConnectionId { get; set; }
-
- ///
- /// Gets or sets any connection error messages
- ///
- public string Messages { get; set; }
-
- ///
- /// Information about the connected server.
- ///
- public ServerInfo ServerInfo { get; set; }
-
- ///
- /// Gets or sets the actual Connection established, including Database Name
- ///
- public ConnectionSummary ConnectionSummary { get; set; }
- }
-}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectionCompleteNotification.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectionCompleteNotification.cs
new file mode 100644
index 00000000..50517a52
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectionCompleteNotification.cs
@@ -0,0 +1,61 @@
+//
+// 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.Hosting.Protocol.Contracts;
+
+namespace Microsoft.SqlTools.ServiceLayer.Connection.Contracts
+{
+ ///
+ /// Parameters to be sent back with a connection complete event
+ ///
+ public class ConnectionCompleteParams
+ {
+ ///
+ /// 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; }
+
+ ///
+ /// A GUID representing a unique connection ID
+ ///
+ public string ConnectionId { get; set; }
+
+ ///
+ /// Gets or sets any detailed connection error messages.
+ ///
+ public string Messages { get; set; }
+
+ ///
+ /// Error message returned from the engine for a connection failure reason, if any.
+ ///
+ public string ErrorMessage { get; set; }
+
+ ///
+ /// Error number returned from the engine for connection failure reason, if any.
+ ///
+ public int ErrorNumber { get; set; }
+
+ ///
+ /// Information about the connected server.
+ ///
+ public ServerInfo ServerInfo { get; set; }
+
+ ///
+ /// Gets or sets the actual Connection established, including Database Name
+ ///
+ public ConnectionSummary ConnectionSummary { get; set; }
+ }
+
+ ///
+ /// ConnectionComplete notification mapping entry
+ ///
+ public class ConnectionCompleteNotification
+ {
+ public static readonly
+ EventType Type =
+ EventType.Create("connection/complete");
+ }
+}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectionRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectionRequest.cs
index 50251e12..74320bdd 100644
--- a/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectionRequest.cs
+++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectionRequest.cs
@@ -13,7 +13,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection.Contracts
public class ConnectionRequest
{
public static readonly
- RequestType Type =
- RequestType.Create("connection/connect");
+ RequestType Type =
+ RequestType.Create("connection/connect");
}
}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/Hosting/ServiceHost.cs b/src/Microsoft.SqlTools.ServiceLayer/Hosting/ServiceHost.cs
index 326cf5ed..92b097aa 100644
--- a/src/Microsoft.SqlTools.ServiceLayer/Hosting/ServiceHost.cs
+++ b/src/Microsoft.SqlTools.ServiceLayer/Hosting/ServiceHost.cs
@@ -4,13 +4,13 @@
//
using System;
-using System.Linq;
-using System.Threading.Tasks;
using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Threading.Tasks;
using Microsoft.SqlTools.ServiceLayer.Hosting.Contracts;
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol;
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Channel;
-using System.Reflection;
using Microsoft.SqlTools.ServiceLayer.Utility;
namespace Microsoft.SqlTools.ServiceLayer.Hosting
@@ -22,6 +22,12 @@ namespace Microsoft.SqlTools.ServiceLayer.Hosting
///
public sealed class ServiceHost : ServiceHostBase
{
+ ///
+ /// This timeout limits the amount of time that shutdown tasks can take to complete
+ /// prior to the process shutting down.
+ ///
+ private const int ShutdownTimeoutInSeconds = 120;
+
#region Singleton Instance Code
///
@@ -63,8 +69,18 @@ namespace Microsoft.SqlTools.ServiceLayer.Hosting
#region Member Variables
+ ///
+ /// Delegate definition for the host shutdown event
+ ///
+ ///
+ ///
public delegate Task ShutdownCallback(object shutdownParams, RequestContext
///
///
- internal static void PrepopulateCommonMetadata(ConnectionInfo info, ScriptParseInfo scriptInfo)
+ internal static void PrepopulateCommonMetadata(
+ ConnectionInfo info,
+ ScriptParseInfo scriptInfo,
+ ConnectedBindingQueue bindingQueue)
{
if (scriptInfo.IsConnected)
{
- var scriptFile = WorkspaceService.Instance.Workspace.GetFile(info.OwnerUri);
+ var scriptFile = AutoCompleteHelper.WorkspaceServiceInstance.Workspace.GetFile(info.OwnerUri);
LanguageService.Instance.ParseAndBind(scriptFile, info);
if (scriptInfo.BuildingMetadataEvent.WaitOne(LanguageService.OnConnectionWaitTimeout))
@@ -536,44 +578,53 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
{
scriptInfo.BuildingMetadataEvent.Reset();
- // parse a simple statement that returns common metadata
- ParseResult parseResult = Parser.Parse(
- "select ",
- scriptInfo.ParseOptions);
+ QueueItem queueItem = bindingQueue.QueueBindingOperation(
+ key: scriptInfo.ConnectionKey,
+ bindingTimeout: AutoCompleteHelper.PrepopulateBindTimeout,
+ bindOperation: (bindingContext, cancelToken) =>
+ {
+ // parse a simple statement that returns common metadata
+ ParseResult parseResult = Parser.Parse(
+ "select ",
+ bindingContext.ParseOptions);
- List parseResults = new List();
- parseResults.Add(parseResult);
- scriptInfo.Binder.Bind(
- parseResults,
- info.ConnectionDetails.DatabaseName,
- BindMode.Batch);
+ List parseResults = new List();
+ parseResults.Add(parseResult);
+ bindingContext.Binder.Bind(
+ parseResults,
+ info.ConnectionDetails.DatabaseName,
+ BindMode.Batch);
- // get the completion list from SQL Parser
- var suggestions = Resolver.FindCompletions(
- parseResult, 1, 8,
- scriptInfo.MetadataDisplayInfoProvider);
+ // get the completion list from SQL Parser
+ var suggestions = Resolver.FindCompletions(
+ parseResult, 1, 8,
+ bindingContext.MetadataDisplayInfoProvider);
- // this forces lazy evaluation of the suggestion metadata
- AutoCompleteHelper.ConvertDeclarationsToCompletionItems(suggestions, 1, 8, 8);
+ // this forces lazy evaluation of the suggestion metadata
+ AutoCompleteHelper.ConvertDeclarationsToCompletionItems(suggestions, 1, 8, 8);
- parseResult = Parser.Parse(
- "exec ",
- scriptInfo.ParseOptions);
+ parseResult = Parser.Parse(
+ "exec ",
+ bindingContext.ParseOptions);
- parseResults = new List();
- parseResults.Add(parseResult);
- scriptInfo.Binder.Bind(
- parseResults,
- info.ConnectionDetails.DatabaseName,
- BindMode.Batch);
+ parseResults = new List();
+ parseResults.Add(parseResult);
+ bindingContext.Binder.Bind(
+ parseResults,
+ info.ConnectionDetails.DatabaseName,
+ BindMode.Batch);
- // get the completion list from SQL Parser
- suggestions = Resolver.FindCompletions(
- parseResult, 1, 6,
- scriptInfo.MetadataDisplayInfoProvider);
+ // get the completion list from SQL Parser
+ suggestions = Resolver.FindCompletions(
+ parseResult, 1, 6,
+ bindingContext.MetadataDisplayInfoProvider);
- // this forces lazy evaluation of the suggestion metadata
- AutoCompleteHelper.ConvertDeclarationsToCompletionItems(suggestions, 1, 6, 6);
+ // this forces lazy evaluation of the suggestion metadata
+ AutoCompleteHelper.ConvertDeclarationsToCompletionItems(suggestions, 1, 6, 6);
+ return null;
+ });
+
+ queueItem.ItemProcessed.WaitOne();
}
catch
{
@@ -585,5 +636,53 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
}
}
}
+
+
+ ///
+ /// Converts a SQL Parser QuickInfo object into a VS Code Hover object
+ ///
+ ///
+ ///
+ ///
+ ///
+ internal static Hover ConvertQuickInfoToHover(
+ Babel.CodeObjectQuickInfo quickInfo,
+ int row,
+ int startColumn,
+ int endColumn)
+ {
+ // convert from the parser format to the VS Code wire format
+ var markedStrings = new MarkedString[1];
+ if (quickInfo != null)
+ {
+ markedStrings[0] = new MarkedString()
+ {
+ Language = "SQL",
+ Value = quickInfo.Text
+ };
+
+ return new Hover()
+ {
+ Contents = markedStrings,
+ Range = new Range
+ {
+ Start = new Position
+ {
+ Line = row,
+ Character = startColumn
+ },
+ End = new Position
+ {
+ Line = row,
+ Character = endColumn
+ }
+ }
+ };
+ }
+ else
+ {
+ return null;
+ }
+ }
}
}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/BindingQueue.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/BindingQueue.cs
new file mode 100644
index 00000000..2b165dc8
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/BindingQueue.cs
@@ -0,0 +1,259 @@
+//
+// 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.Threading;
+using System.Threading.Tasks;
+using Microsoft.SqlTools.ServiceLayer.Utility;
+
+namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
+{
+ ///
+ /// Main class for the Binding Queue
+ ///
+ public class BindingQueue where T : IBindingContext, new()
+ {
+ private CancellationTokenSource processQueueCancelToken = new CancellationTokenSource();
+
+ private ManualResetEvent itemQueuedEvent = new ManualResetEvent(initialState: false);
+
+ private object bindingQueueLock = new object();
+
+ private LinkedList bindingQueue = new LinkedList();
+
+ private object bindingContextLock = new object();
+
+ private Task queueProcessorTask;
+
+ ///
+ /// Map from context keys to binding context instances
+ /// Internal for testing purposes only
+ ///
+ internal Dictionary BindingContextMap { get; set; }
+
+ ///
+ /// Constructor for a binding queue instance
+ ///
+ public BindingQueue()
+ {
+ this.BindingContextMap = new Dictionary();
+
+ this.queueProcessorTask = StartQueueProcessor();
+ }
+
+ ///
+ /// Stops the binding queue by sending cancellation request
+ ///
+ ///
+ public bool StopQueueProcessor(int timeout)
+ {
+ this.processQueueCancelToken.Cancel();
+ return this.queueProcessorTask.Wait(timeout);
+ }
+
+ ///
+ /// Queue a binding request item
+ ///
+ public QueueItem QueueBindingOperation(
+ string key,
+ Func bindOperation,
+ Func timeoutOperation = null,
+ int? bindingTimeout = null)
+ {
+ // don't add null operations to the binding queue
+ if (bindOperation == null)
+ {
+ return null;
+ }
+
+ QueueItem queueItem = new QueueItem()
+ {
+ Key = key,
+ BindOperation = bindOperation,
+ TimeoutOperation = timeoutOperation,
+ BindingTimeout = bindingTimeout
+ };
+
+ lock (this.bindingQueueLock)
+ {
+ this.bindingQueue.AddLast(queueItem);
+ }
+
+ this.itemQueuedEvent.Set();
+
+ return queueItem;
+ }
+
+ ///
+ /// Gets or creates a binding context for the provided context key
+ ///
+ ///
+ protected IBindingContext GetOrCreateBindingContext(string key)
+ {
+ // use a default binding context for disconnected requests
+ if (string.IsNullOrWhiteSpace(key))
+ {
+ key = "disconnected_binding_context";
+ }
+
+ lock (this.bindingContextLock)
+ {
+ if (!this.BindingContextMap.ContainsKey(key))
+ {
+ this.BindingContextMap.Add(key, new T());
+ }
+
+ return this.BindingContextMap[key];
+ }
+ }
+
+ private bool HasPendingQueueItems
+ {
+ get
+ {
+ lock (this.bindingQueueLock)
+ {
+ return this.bindingQueue.Count > 0;
+ }
+ }
+ }
+
+ ///
+ /// Gets the next pending queue item
+ ///
+ private QueueItem GetNextQueueItem()
+ {
+ lock (this.bindingQueueLock)
+ {
+ if (this.bindingQueue.Count == 0)
+ {
+ return null;
+ }
+
+ QueueItem queueItem = this.bindingQueue.First.Value;
+ this.bindingQueue.RemoveFirst();
+ return queueItem;
+ }
+ }
+
+ ///
+ /// Starts the queue processing thread
+ ///
+ private Task StartQueueProcessor()
+ {
+ return Task.Factory.StartNew(
+ ProcessQueue,
+ this.processQueueCancelToken.Token,
+ TaskCreationOptions.LongRunning,
+ TaskScheduler.Default);
+ }
+
+ ///
+ /// The core queue processing method
+ ///
+ ///
+ private void ProcessQueue()
+ {
+ CancellationToken token = this.processQueueCancelToken.Token;
+ WaitHandle[] waitHandles = new WaitHandle[2]
+ {
+ this.itemQueuedEvent,
+ token.WaitHandle
+ };
+
+ while (true)
+ {
+ // wait for with an item to be queued or the a cancellation request
+ WaitHandle.WaitAny(waitHandles);
+ if (token.IsCancellationRequested)
+ {
+ break;
+ }
+
+ try
+ {
+ // dispatch all pending queue items
+ while (this.HasPendingQueueItems)
+ {
+ QueueItem queueItem = GetNextQueueItem();
+ if (queueItem == null)
+ {
+ continue;
+ }
+
+ IBindingContext bindingContext = GetOrCreateBindingContext(queueItem.Key);
+ if (bindingContext == null)
+ {
+ queueItem.ItemProcessed.Set();
+ continue;
+ }
+
+ try
+ {
+ // prefer the queue item binding item, otherwise use the context default timeout
+ int bindTimeout = queueItem.BindingTimeout ?? bindingContext.BindingTimeout;
+
+ // handle the case a previous binding operation is still running
+ if (!bindingContext.BindingLocked.WaitOne(bindTimeout))
+ {
+ queueItem.Result = queueItem.TimeoutOperation(bindingContext);
+ queueItem.ItemProcessed.Set();
+ continue;
+ }
+
+ // execute the binding operation
+ object result = null;
+ CancellationTokenSource cancelToken = new CancellationTokenSource();
+ var bindTask = Task.Run(() =>
+ {
+ result = queueItem.BindOperation(
+ bindingContext,
+ cancelToken.Token);
+ });
+
+ // check if the binding tasks completed within the binding timeout
+ if (bindTask.Wait(bindTimeout))
+ {
+ queueItem.Result = result;
+ }
+ else
+ {
+ // if the task didn't complete then call the timeout callback
+ if (queueItem.TimeoutOperation != null)
+ {
+ cancelToken.Cancel();
+ queueItem.Result = queueItem.TimeoutOperation(bindingContext);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ // catch and log any exceptions raised in the binding calls
+ // set item processed to avoid deadlocks
+ Logger.Write(LogLevel.Error, "Binding queue threw exception " + ex.ToString());
+ }
+ finally
+ {
+ bindingContext.BindingLocked.Set();
+ queueItem.ItemProcessed.Set();
+ }
+
+ // if a queue processing cancellation was requested then exit the loop
+ if (token.IsCancellationRequested)
+ {
+ break;
+ }
+ }
+ }
+ finally
+ {
+ // reset the item queued event since we've processed all the pending items
+ this.itemQueuedEvent.Reset();
+ }
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/ConnectedBindingContext.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/ConnectedBindingContext.cs
new file mode 100644
index 00000000..8851abe1
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/ConnectedBindingContext.cs
@@ -0,0 +1,208 @@
+//
+// 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;
+using Microsoft.SqlServer.Management.Common;
+using Microsoft.SqlServer.Management.SmoMetadataProvider;
+using Microsoft.SqlServer.Management.SqlParser.Binder;
+using Microsoft.SqlServer.Management.SqlParser.Common;
+using Microsoft.SqlServer.Management.SqlParser.MetadataProvider;
+using Microsoft.SqlServer.Management.SqlParser.Parser;
+
+namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
+{
+ ///
+ /// Class for the binding context for connected sessions
+ ///
+ public class ConnectedBindingContext : IBindingContext
+ {
+ private ParseOptions parseOptions;
+
+ private ServerConnection serverConnection;
+
+ ///
+ /// Connected binding context constructor
+ ///
+ public ConnectedBindingContext()
+ {
+ this.BindingLocked = new ManualResetEvent(initialState: true);
+ this.BindingTimeout = ConnectedBindingQueue.DefaultBindingTimeout;
+ this.MetadataDisplayInfoProvider = new MetadataDisplayInfoProvider();
+ }
+
+ ///
+ /// Gets or sets a flag indicating if the binder is connected
+ ///
+ public bool IsConnected { get; set; }
+
+ ///
+ /// Gets or sets the binding server connection
+ ///
+ public ServerConnection ServerConnection
+ {
+ get
+ {
+ return this.serverConnection;
+ }
+ set
+ {
+ this.serverConnection = value;
+
+ // reset the parse options so the get recreated for the current connection
+ this.parseOptions = null;
+ }
+ }
+
+ ///
+ /// Gets or sets the metadata display info provider
+ ///
+ public MetadataDisplayInfoProvider MetadataDisplayInfoProvider { get; set; }
+
+ ///
+ /// Gets or sets the SMO metadata provider
+ ///
+ public SmoMetadataProvider SmoMetadataProvider { get; set; }
+
+ ///
+ /// Gets or sets the binder
+ ///
+ public IBinder Binder { get; set; }
+
+ ///
+ /// Gets or sets an event to signal if a binding operation is in progress
+ ///
+ public ManualResetEvent BindingLocked { get; set; }
+
+ ///
+ /// Gets or sets the binding operation timeout in milliseconds
+ ///
+ public int BindingTimeout { get; set; }
+
+ ///
+ /// Gets the Language Service ServerVersion
+ ///
+ public ServerVersion ServerVersion
+ {
+ get
+ {
+ return this.ServerConnection != null
+ ? this.ServerConnection.ServerVersion
+ : null;
+ }
+ }
+
+ ///
+ /// Gets the current DataEngineType
+ ///
+ public DatabaseEngineType DatabaseEngineType
+ {
+ get
+ {
+ return this.ServerConnection != null
+ ? this.ServerConnection.DatabaseEngineType
+ : DatabaseEngineType.Standalone;
+ }
+ }
+
+ ///
+ /// Gets the current connections TransactSqlVersion
+ ///
+ public TransactSqlVersion TransactSqlVersion
+ {
+ get
+ {
+ return this.IsConnected
+ ? GetTransactSqlVersion(this.ServerVersion)
+ : TransactSqlVersion.Current;
+ }
+ }
+
+ ///
+ /// Gets the current DatabaseCompatibilityLevel
+ ///
+ public DatabaseCompatibilityLevel DatabaseCompatibilityLevel
+ {
+ get
+ {
+ return this.IsConnected
+ ? GetDatabaseCompatibilityLevel(this.ServerVersion)
+ : DatabaseCompatibilityLevel.Current;
+ }
+ }
+
+ ///
+ /// Gets the current ParseOptions
+ ///
+ public ParseOptions ParseOptions
+ {
+ get
+ {
+ if (this.parseOptions == null)
+ {
+ this.parseOptions = new ParseOptions(
+ batchSeparator: LanguageService.DefaultBatchSeperator,
+ isQuotedIdentifierSet: true,
+ compatibilityLevel: DatabaseCompatibilityLevel,
+ transactSqlVersion: TransactSqlVersion);
+ }
+ return this.parseOptions;
+ }
+ }
+
+
+ ///
+ /// Gets the database compatibility level from a server version
+ ///
+ ///
+ private static DatabaseCompatibilityLevel GetDatabaseCompatibilityLevel(ServerVersion serverVersion)
+ {
+ int versionMajor = Math.Max(serverVersion.Major, 8);
+
+ switch (versionMajor)
+ {
+ case 8:
+ return DatabaseCompatibilityLevel.Version80;
+ case 9:
+ return DatabaseCompatibilityLevel.Version90;
+ case 10:
+ return DatabaseCompatibilityLevel.Version100;
+ case 11:
+ return DatabaseCompatibilityLevel.Version110;
+ case 12:
+ return DatabaseCompatibilityLevel.Version120;
+ case 13:
+ return DatabaseCompatibilityLevel.Version130;
+ default:
+ return DatabaseCompatibilityLevel.Current;
+ }
+ }
+
+ ///
+ /// Gets the transaction sql version from a server version
+ ///
+ ///
+ private static TransactSqlVersion GetTransactSqlVersion(ServerVersion serverVersion)
+ {
+ int versionMajor = Math.Max(serverVersion.Major, 9);
+
+ switch (versionMajor)
+ {
+ case 9:
+ case 10:
+ // In case of 10.0 we still use Version 10.5 as it is the closest available.
+ return TransactSqlVersion.Version105;
+ case 11:
+ return TransactSqlVersion.Version110;
+ case 12:
+ return TransactSqlVersion.Version120;
+ case 13:
+ return TransactSqlVersion.Version130;
+ default:
+ return TransactSqlVersion.Current;
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/ConnectedBindingQueue.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/ConnectedBindingQueue.cs
new file mode 100644
index 00000000..c99f0cc6
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/ConnectedBindingQueue.cs
@@ -0,0 +1,109 @@
+//
+// 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.Data.SqlClient;
+using Microsoft.SqlServer.Management.Common;
+using Microsoft.SqlServer.Management.SmoMetadataProvider;
+using Microsoft.SqlServer.Management.SqlParser.Binder;
+using Microsoft.SqlServer.Management.SqlParser.MetadataProvider;
+using Microsoft.SqlTools.ServiceLayer.Connection;
+using Microsoft.SqlTools.ServiceLayer.Connection.Contracts;
+using Microsoft.SqlTools.ServiceLayer.SqlContext;
+using Microsoft.SqlTools.ServiceLayer.Workspace;
+
+namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
+{
+ ///
+ /// ConnectedBindingQueue class for processing online binding requests
+ ///
+ public class ConnectedBindingQueue : BindingQueue
+ {
+ internal const int DefaultBindingTimeout = 60000;
+
+ internal const int DefaultMinimumConnectionTimeout = 30;
+
+ ///
+ /// Gets the current settings
+ ///
+ internal SqlToolsSettings CurrentSettings
+ {
+ get { return WorkspaceService.Instance.CurrentSettings; }
+ }
+
+ ///
+ /// Generate a unique key based on the ConnectionInfo object
+ ///
+ ///
+ private string GetConnectionContextKey(ConnectionInfo connInfo)
+ {
+ ConnectionDetails details = connInfo.ConnectionDetails;
+ return string.Format("{0}_{1}_{2}_{3}",
+ details.ServerName ?? "NULL",
+ details.DatabaseName ?? "NULL",
+ details.UserName ?? "NULL",
+ details.AuthenticationType ?? "NULL"
+ );
+ }
+
+ ///
+ /// Use a ConnectionInfo item to create a connected binding context
+ ///
+ ///
+ public virtual string AddConnectionContext(ConnectionInfo connInfo)
+ {
+ if (connInfo == null)
+ {
+ return string.Empty;
+ }
+
+ // lookup the current binding context
+ string connectionKey = GetConnectionContextKey(connInfo);
+ IBindingContext bindingContext = this.GetOrCreateBindingContext(connectionKey);
+
+ 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(DefaultMinimumConnectionTimeout, 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);
+ if (sqlConn != null)
+ {
+ sqlConn.Open();
+
+ // populate the binding context to work with the SMO metadata provider
+ ServerConnection serverConn = new ServerConnection(sqlConn);
+ bindingContext.SmoMetadataProvider = SmoMetadataProvider.CreateConnectedProvider(serverConn);
+ bindingContext.MetadataDisplayInfoProvider = new MetadataDisplayInfoProvider();
+ bindingContext.MetadataDisplayInfoProvider.BuiltInCasing =
+ this.CurrentSettings.SqlTools.IntelliSense.LowerCaseSuggestions.Value
+ ? CasingStyle.Lowercase : CasingStyle.Uppercase;
+ bindingContext.Binder = BinderProvider.CreateBinder(bindingContext.SmoMetadataProvider);
+ bindingContext.ServerConnection = serverConn;
+ bindingContext.BindingTimeout = ConnectedBindingQueue.DefaultBindingTimeout;
+ bindingContext.IsConnected = true;
+ }
+ }
+ catch (Exception)
+ {
+ bindingContext.IsConnected = false;
+ }
+ finally
+ {
+ bindingContext.BindingLocked.Set();
+ }
+
+ return connectionKey;
+ }
+ }
+}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/IBindingContext.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/IBindingContext.cs
new file mode 100644
index 00000000..c83a28d7
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/IBindingContext.cs
@@ -0,0 +1,81 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+
+using System.Threading;
+using Microsoft.SqlServer.Management.Common;
+using Microsoft.SqlServer.Management.SmoMetadataProvider;
+using Microsoft.SqlServer.Management.SqlParser.Binder;
+using Microsoft.SqlServer.Management.SqlParser.Common;
+using Microsoft.SqlServer.Management.SqlParser.MetadataProvider;
+using Microsoft.SqlServer.Management.SqlParser.Parser;
+
+namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
+{
+ ///
+ /// The context used for binding requests
+ ///
+ public interface IBindingContext
+ {
+ ///
+ /// Gets or sets a flag indicating if the context is connected
+ ///
+ bool IsConnected { get; set; }
+
+ ///
+ /// Gets or sets the binding server connection
+ ///
+ ServerConnection ServerConnection { get; set; }
+
+ ///
+ /// Gets or sets the metadata display info provider
+ ///
+ MetadataDisplayInfoProvider MetadataDisplayInfoProvider { get; set; }
+
+ ///
+ /// Gets or sets the SMO metadata provider
+ ///
+ SmoMetadataProvider SmoMetadataProvider { get; set; }
+
+ ///
+ /// Gets or sets the binder
+ ///
+ IBinder Binder { get; set; }
+
+ ///
+ /// Gets or sets an event to signal if a binding operation is in progress
+ ///
+ ManualResetEvent BindingLocked { get; set; }
+
+ ///
+ /// Gets or sets the binding operation timeout in milliseconds
+ ///
+ int BindingTimeout { get; set; }
+
+ ///
+ /// Gets or sets the current connection parse options
+ ///
+ ParseOptions ParseOptions { get; }
+
+ ///
+ /// Gets or sets the current connection server version
+ ///
+ ServerVersion ServerVersion { get; }
+
+ ///
+ /// Gets or sets the database engine type
+ ///
+ DatabaseEngineType DatabaseEngineType { get; }
+
+ ///
+ /// Gets or sets the T-SQL version
+ ///
+ TransactSqlVersion TransactSqlVersion { get; }
+
+ ///
+ /// Gets or sets the database compatibility level
+ ///
+ DatabaseCompatibilityLevel DatabaseCompatibilityLevel { get; }
+ }
+}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs
index cc834adc..889ad882 100644
--- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs
+++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs
@@ -11,13 +11,11 @@ using System.Threading.Tasks;
using Microsoft.SqlServer.Management.Common;
using Microsoft.SqlServer.Management.SqlParser;
using Microsoft.SqlServer.Management.SqlParser.Binder;
+using Microsoft.SqlServer.Management.SqlParser.Common;
using Microsoft.SqlServer.Management.SqlParser.Intellisense;
-using Microsoft.SqlServer.Management.SqlParser.MetadataProvider;
using Microsoft.SqlServer.Management.SqlParser.Parser;
-using Microsoft.SqlServer.Management.SmoMetadataProvider;
using Microsoft.SqlTools.ServiceLayer.Connection;
using Microsoft.SqlTools.ServiceLayer.Connection.Contracts;
-using Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection;
using Microsoft.SqlTools.ServiceLayer.Hosting;
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol;
using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts;
@@ -39,31 +37,54 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
internal const int DiagnosticParseDelay = 750;
- internal const int FindCompletionsTimeout = 3000;
+ internal const int HoverTimeout = 3000;
+
+ internal const int BindingTimeout = 3000;
internal const int FindCompletionStartTimeout = 50;
internal const int OnConnectionWaitTimeout = 300000;
+ private static ConnectionService connectionService = null;
+
+ private static WorkspaceService workspaceServiceInstance;
+
private object parseMapLock = new object();
private ScriptParseInfo currentCompletionParseInfo;
- internal bool ShouldEnableAutocomplete()
- {
- return true;
- }
+ private ConnectedBindingQueue bindingQueue = new ConnectedBindingQueue();
- private ConnectionService connectionService = null;
+ private ParseOptions defaultParseOptions = new ParseOptions(
+ batchSeparator: LanguageService.DefaultBatchSeperator,
+ isQuotedIdentifierSet: true,
+ compatibilityLevel: DatabaseCompatibilityLevel.Current,
+ transactSqlVersion: TransactSqlVersion.Current);
+
+ ///
+ /// Gets or sets the binding queue instance
+ /// Internal for testing purposes only
+ ///
+ internal ConnectedBindingQueue BindingQueue
+ {
+ get
+ {
+ return this.bindingQueue;
+ }
+ set
+ {
+ this.bindingQueue = value;
+ }
+ }
///
/// Internal for testing purposes only
///
- internal ConnectionService ConnectionServiceInstance
+ internal static ConnectionService ConnectionServiceInstance
{
get
{
- if(connectionService == null)
+ if (connectionService == null)
{
connectionService = ConnectionService.Instance;
}
@@ -83,6 +104,9 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
private Lazy> scriptParseInfoMap
= new Lazy>(() => new Dictionary());
+ ///
+ /// Gets a mapping dictionary for SQL file URIs to ScriptParseInfo objects
+ ///
internal Dictionary ScriptParseInfoMap
{
get
@@ -91,11 +115,22 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
}
}
+ ///
+ /// Gets the singleton instance object
+ ///
public static LanguageService Instance
{
get { return instance.Value; }
}
+ private ParseOptions DefaultParseOptions
+ {
+ get
+ {
+ return this.defaultParseOptions;
+ }
+ }
+
///
/// Default, parameterless constructor.
///
@@ -109,14 +144,40 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
private static CancellationTokenSource ExistingRequestCancellation { get; set; }
+ ///
+ /// Gets the current settings
+ ///
internal SqlToolsSettings CurrentSettings
{
get { return WorkspaceService.Instance.CurrentSettings; }
}
+ ///
+ /// Gets or sets the current workspace service instance
+ /// Setter for internal testing purposes only
+ ///
+ internal static WorkspaceService WorkspaceServiceInstance
+ {
+ get
+ {
+ if (LanguageService.workspaceServiceInstance == null)
+ {
+ LanguageService.workspaceServiceInstance = WorkspaceService.Instance;
+ }
+ return LanguageService.workspaceServiceInstance;
+ }
+ set
+ {
+ LanguageService.workspaceServiceInstance = value;
+ }
+ }
+
+ ///
+ /// Gets the current workspace instance
+ ///
internal Workspace.Workspace CurrentWorkspace
{
- get { return WorkspaceService.Instance.Workspace; }
+ get { return LanguageService.WorkspaceServiceInstance.Workspace; }
}
///
@@ -181,23 +242,31 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
///
///
///
- private static async Task HandleCompletionRequest(
+ internal static async Task HandleCompletionRequest(
TextDocumentPosition textDocumentPosition,
RequestContext requestContext)
{
- // get the current list of completion items and return to client
- var scriptFile = WorkspaceService.Instance.Workspace.GetFile(
- textDocumentPosition.TextDocument.Uri);
+ // check if Intellisense suggestions are enabled
+ if (!WorkspaceService.Instance.CurrentSettings.IsSuggestionsEnabled)
+ {
+ await Task.FromResult(true);
+ }
+ else
+ {
+ // get the current list of completion items and return to client
+ var scriptFile = LanguageService.WorkspaceServiceInstance.Workspace.GetFile(
+ textDocumentPosition.TextDocument.Uri);
- ConnectionInfo connInfo;
- ConnectionService.Instance.TryFindConnection(
- scriptFile.ClientFilePath,
- out connInfo);
+ ConnectionInfo connInfo;
+ LanguageService.ConnectionServiceInstance.TryFindConnection(
+ scriptFile.ClientFilePath,
+ out connInfo);
- var completionItems = Instance.GetCompletionItems(
- textDocumentPosition, scriptFile, connInfo);
+ var completionItems = Instance.GetCompletionItems(
+ textDocumentPosition, scriptFile, connInfo);
- await requestContext.SendResult(completionItems);
+ await requestContext.SendResult(completionItems);
+ }
}
///
@@ -211,8 +280,16 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
CompletionItem completionItem,
RequestContext requestContext)
{
- completionItem = LanguageService.Instance.ResolveCompletionItem(completionItem);
- await requestContext.SendResult(completionItem);
+ // check if Intellisense suggestions are enabled
+ if (!WorkspaceService.Instance.CurrentSettings.IsSuggestionsEnabled)
+ {
+ await Task.FromResult(true);
+ }
+ else
+ {
+ completionItem = LanguageService.Instance.ResolveCompletionItem(completionItem);
+ await requestContext.SendResult(completionItem);
+ }
}
private static async Task HandleDefinitionRequest(
@@ -246,8 +323,21 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
private static async Task HandleHoverRequest(
TextDocumentPosition textDocumentPosition,
RequestContext requestContext)
- {
- await Task.FromResult(true);
+ {
+ // check if Quick Info hover tooltips are enabled
+ if (WorkspaceService.Instance.CurrentSettings.IsQuickInfoEnabled)
+ {
+ var scriptFile = WorkspaceService.Instance.Workspace.GetFile(
+ textDocumentPosition.TextDocument.Uri);
+
+ var hover = LanguageService.Instance.GetHoverItem(textDocumentPosition, scriptFile);
+ if (hover != null)
+ {
+ await requestContext.SendResult(hover);
+ }
+ }
+
+ await requestContext.SendResult(new Hover());
}
#endregion
@@ -264,7 +354,9 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
ScriptFile scriptFile,
EventContext eventContext)
{
- if (!IsPreviewWindow(scriptFile))
+ // if not in the preview window and diagnostics are enabled then run diagnostics
+ if (!IsPreviewWindow(scriptFile)
+ && WorkspaceService.Instance.CurrentSettings.IsDiagnositicsEnabled)
{
await RunScriptDiagnostics(
new ScriptFile[] { scriptFile },
@@ -278,13 +370,15 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
/// Handles text document change events
///
///
- ///
- ///
+ ///
public async Task HandleDidChangeTextDocumentNotification(ScriptFile[] changedFiles, EventContext eventContext)
{
- await this.RunScriptDiagnostics(
- changedFiles.ToArray(),
- eventContext);
+ if (WorkspaceService.Instance.CurrentSettings.IsDiagnositicsEnabled)
+ {
+ await this.RunScriptDiagnostics(
+ changedFiles.ToArray(),
+ eventContext);
+ }
await Task.FromResult(true);
}
@@ -300,13 +394,18 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
SqlToolsSettings oldSettings,
EventContext eventContext)
{
- // If script analysis settings have changed we need to clear & possibly update the current diagnostic records.
- bool oldScriptAnalysisEnabled = oldSettings.ScriptAnalysis.Enable.HasValue;
- if ((oldScriptAnalysisEnabled != newSettings.ScriptAnalysis.Enable))
+ bool oldEnableIntelliSense = oldSettings.SqlTools.EnableIntellisense;
+ bool? oldEnableDiagnostics = oldSettings.SqlTools.IntelliSense.EnableDiagnostics;
+
+ // update the current settings to reflect any changes
+ CurrentSettings.Update(newSettings);
+
+ // if script analysis settings have changed we need to clear the current diagnostic markers
+ if (oldEnableIntelliSense != newSettings.SqlTools.EnableIntellisense
+ || oldEnableDiagnostics != newSettings.SqlTools.IntelliSense.EnableDiagnostics)
{
- // If the user just turned off script analysis or changed the settings path, send a diagnostics
- // event to clear the analysis markers that they already have.
- if (!newSettings.ScriptAnalysis.Enable.Value)
+ // if the user just turned off diagnostics then send an event to clear the error markers
+ if (!newSettings.IsDiagnositicsEnabled)
{
ScriptFileMarker[] emptyAnalysisDiagnostics = new ScriptFileMarker[0];
@@ -315,15 +414,12 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
await DiagnosticsHelper.PublishScriptDiagnostics(scriptFile, emptyAnalysisDiagnostics, eventContext);
}
}
+ // otherwise rerun diagnostic analysis on all opened SQL files
else
{
await this.RunScriptDiagnostics(CurrentWorkspace.GetOpenedFiles(), eventContext);
}
}
-
- // Update the settings in the current
- CurrentSettings.EnableProfileLoading = newSettings.EnableProfileLoading;
- CurrentSettings.ScriptAnalysis.Update(newSettings.ScriptAnalysis, CurrentWorkspace.WorkspacePath);
}
#endregion
@@ -350,52 +446,85 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
///
///
///
- ///
+ /// The ParseResult instance returned from SQL Parser
public ParseResult ParseAndBind(ScriptFile scriptFile, ConnectionInfo connInfo)
{
// get or create the current parse info object
ScriptParseInfo parseInfo = GetScriptParseInfo(scriptFile.ClientFilePath, createIfNotExists: true);
- if (parseInfo.BuildingMetadataEvent.WaitOne(LanguageService.FindCompletionsTimeout))
+ if (parseInfo.BuildingMetadataEvent.WaitOne(LanguageService.BindingTimeout))
{
try
{
parseInfo.BuildingMetadataEvent.Reset();
- // parse current SQL file contents to retrieve a list of errors
- ParseResult parseResult = Parser.IncrementalParse(
- scriptFile.Contents,
- parseInfo.ParseResult,
- parseInfo.ParseOptions);
-
- parseInfo.ParseResult = parseResult;
-
- if (connInfo != null && parseInfo.IsConnected)
+ if (connInfo == null || !parseInfo.IsConnected)
{
- try
- {
- List parseResults = new List();
- parseResults.Add(parseResult);
- parseInfo.Binder.Bind(
- parseResults,
- connInfo.ConnectionDetails.DatabaseName,
- BindMode.Batch);
- }
- catch (ConnectionException)
- {
- Logger.Write(LogLevel.Error, "Hit connection exception while binding - disposing binder object...");
- }
- catch (SqlParserInternalBinderError)
- {
- Logger.Write(LogLevel.Error, "Hit connection exception while binding - disposing binder object...");
- }
+ // parse current SQL file contents to retrieve a list of errors
+ ParseResult parseResult = Parser.IncrementalParse(
+ scriptFile.Contents,
+ parseInfo.ParseResult,
+ this.DefaultParseOptions);
+
+ parseInfo.ParseResult = parseResult;
}
+ else
+ {
+ QueueItem queueItem = this.BindingQueue.QueueBindingOperation(
+ key: parseInfo.ConnectionKey,
+ bindingTimeout: LanguageService.BindingTimeout,
+ bindOperation: (bindingContext, cancelToken) =>
+ {
+ try
+ {
+ ParseResult parseResult = Parser.IncrementalParse(
+ scriptFile.Contents,
+ parseInfo.ParseResult,
+ bindingContext.ParseOptions);
+
+ parseInfo.ParseResult = parseResult;
+
+ List parseResults = new List();
+ parseResults.Add(parseResult);
+ bindingContext.Binder.Bind(
+ parseResults,
+ connInfo.ConnectionDetails.DatabaseName,
+ BindMode.Batch);
+ }
+ catch (ConnectionException)
+ {
+ Logger.Write(LogLevel.Error, "Hit connection exception while binding - disposing binder object...");
+ }
+ catch (SqlParserInternalBinderError)
+ {
+ Logger.Write(LogLevel.Error, "Hit connection exception while binding - disposing binder object...");
+ }
+ catch (Exception ex)
+ {
+ Logger.Write(LogLevel.Error, "Unknown exception during parsing " + ex.ToString());
+ }
+
+ return null;
+ });
+
+ queueItem.ItemProcessed.WaitOne();
+ }
+ }
+ catch (Exception ex)
+ {
+ // reset the parse result to do a full parse next time
+ parseInfo.ParseResult = null;
+ Logger.Write(LogLevel.Error, "Unknown exception during parsing " + ex.ToString());
}
finally
{
parseInfo.BuildingMetadataEvent.Set();
}
}
+ else
+ {
+ Logger.Write(LogLevel.Warning, "Binding metadata lock timeout in ParseAndBind");
+ }
return parseInfo.ParseResult;
}
@@ -406,42 +535,32 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
///
public async Task UpdateLanguageServiceOnConnection(ConnectionInfo info)
{
- await Task.Run( () =>
+ await Task.Run(() =>
{
- if (ShouldEnableAutocomplete())
+ ScriptParseInfo scriptInfo = GetScriptParseInfo(info.OwnerUri, createIfNotExists: true);
+ if (scriptInfo.BuildingMetadataEvent.WaitOne(LanguageService.OnConnectionWaitTimeout))
{
- ScriptParseInfo scriptInfo = GetScriptParseInfo(info.OwnerUri, createIfNotExists: true);
- if (scriptInfo.BuildingMetadataEvent.WaitOne(LanguageService.OnConnectionWaitTimeout))
+ try
{
- try
- {
- scriptInfo.BuildingMetadataEvent.Reset();
- var sqlConn = info.SqlConnection as ReliableSqlConnection;
- if (sqlConn != null)
- {
- ServerConnection serverConn = new ServerConnection(sqlConn.GetUnderlyingConnection());
- scriptInfo.MetadataDisplayInfoProvider = new MetadataDisplayInfoProvider();
- scriptInfo.MetadataProvider = SmoMetadataProvider.CreateConnectedProvider(serverConn);
- scriptInfo.Binder = BinderProvider.CreateBinder(scriptInfo.MetadataProvider);
- scriptInfo.ServerConnection = new ServerConnection(sqlConn.GetUnderlyingConnection());
- scriptInfo.IsConnected = true;
- }
- }
- catch (Exception)
- {
- scriptInfo.IsConnected = false;
- }
- finally
- {
- // Set Metadata Build event to Signal state.
- // (Tell Language Service that I am ready with Metadata Provider Object)
- scriptInfo.BuildingMetadataEvent.Set();
- }
+ scriptInfo.BuildingMetadataEvent.Reset();
+ scriptInfo.ConnectionKey = this.BindingQueue.AddConnectionContext(info);
+ scriptInfo.IsConnected = true;
+
}
+ catch (Exception ex)
+ {
+ Logger.Write(LogLevel.Error, "Unknown error in OnConnection " + ex.ToString());
+ scriptInfo.IsConnected = false;
+ }
+ finally
+ {
+ // Set Metadata Build event to Signal state.
+ // (Tell Language Service that I am ready with Metadata Provider Object)
+ scriptInfo.BuildingMetadataEvent.Set();
+ }
+ }
- // populate SMO metadata provider with most common info
- AutoCompleteHelper.PrepopulateCommonMetadata(info, scriptInfo);
- }
+ AutoCompleteHelper.PrepopulateCommonMetadata(info, scriptInfo, this.BindingQueue);
});
}
@@ -469,22 +588,88 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
///
internal CompletionItem ResolveCompletionItem(CompletionItem completionItem)
{
- var scriptParseInfo = LanguageService.Instance.currentCompletionParseInfo;
- if (scriptParseInfo != null && scriptParseInfo.CurrentSuggestions != null)
+ try
{
- foreach (var suggestion in scriptParseInfo.CurrentSuggestions)
+ var scriptParseInfo = LanguageService.Instance.currentCompletionParseInfo;
+ if (scriptParseInfo != null && scriptParseInfo.CurrentSuggestions != null)
{
- if (string.Equals(suggestion.Title, completionItem.Label))
+ foreach (var suggestion in scriptParseInfo.CurrentSuggestions)
{
- completionItem.Detail = suggestion.DatabaseQualifiedName;
- completionItem.Documentation = suggestion.Description;
- break;
+ if (string.Equals(suggestion.Title, completionItem.Label))
+ {
+ completionItem.Detail = suggestion.DatabaseQualifiedName;
+ completionItem.Documentation = suggestion.Description;
+ break;
+ }
}
}
}
+ catch (Exception ex)
+ {
+ // if any exceptions are raised looking up extended completion metadata
+ // then just return the original completion item
+ Logger.Write(LogLevel.Error, "Exeception in ResolveCompletionItem " + ex.ToString());
+ }
+
return completionItem;
}
+ ///
+ /// Get quick info hover tooltips for the current position
+ ///
+ ///
+ ///
+ internal Hover GetHoverItem(TextDocumentPosition textDocumentPosition, ScriptFile scriptFile)
+ {
+ int startLine = textDocumentPosition.Position.Line;
+ int startColumn = TextUtilities.PositionOfPrevDelimeter(
+ scriptFile.Contents,
+ textDocumentPosition.Position.Line,
+ textDocumentPosition.Position.Character);
+ int endColumn = textDocumentPosition.Position.Character;
+
+ ScriptParseInfo scriptParseInfo = GetScriptParseInfo(textDocumentPosition.TextDocument.Uri);
+ if (scriptParseInfo != null && scriptParseInfo.ParseResult != null)
+ {
+ if (scriptParseInfo.BuildingMetadataEvent.WaitOne(LanguageService.FindCompletionStartTimeout))
+ {
+ scriptParseInfo.BuildingMetadataEvent.Reset();
+ try
+ {
+ QueueItem queueItem = this.BindingQueue.QueueBindingOperation(
+ key: scriptParseInfo.ConnectionKey,
+ bindingTimeout: LanguageService.HoverTimeout,
+ bindOperation: (bindingContext, cancelToken) =>
+ {
+ // get the current quick info text
+ Babel.CodeObjectQuickInfo quickInfo = Resolver.GetQuickInfo(
+ scriptParseInfo.ParseResult,
+ startLine + 1,
+ endColumn + 1,
+ bindingContext.MetadataDisplayInfoProvider);
+
+ // convert from the parser format to the VS Code wire format
+ return AutoCompleteHelper.ConvertQuickInfoToHover(
+ quickInfo,
+ startLine,
+ startColumn,
+ endColumn);
+ });
+
+ queueItem.ItemProcessed.WaitOne();
+ return queueItem.GetResultAsT();
+ }
+ finally
+ {
+ scriptParseInfo.BuildingMetadataEvent.Set();
+ }
+ }
+ }
+
+ // return null if there isn't a tooltip for the current location
+ return null;
+ }
+
///
/// Return the completion item list for the current text position.
/// This method does not await cache builds since it expects to return quickly
@@ -502,6 +687,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
textDocumentPosition.Position.Line,
textDocumentPosition.Position.Character);
int endColumn = textDocumentPosition.Position.Character;
+ bool useLowerCaseSuggestions = this.CurrentSettings.SqlTools.IntelliSense.LowerCaseSuggestions.Value;
this.currentCompletionParseInfo = null;
@@ -510,7 +696,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
ScriptParseInfo scriptParseInfo = GetScriptParseInfo(textDocumentPosition.TextDocument.Uri);
if (connInfo == null || scriptParseInfo == null)
{
- return AutoCompleteHelper.GetDefaultCompletionItems(startLine, startColumn, endColumn);
+ return AutoCompleteHelper.GetDefaultCompletionItems(startLine, startColumn, endColumn, useLowerCaseSuggestions);
}
// reparse and bind the SQL statement if needed
@@ -521,49 +707,60 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
if (scriptParseInfo.ParseResult == null)
{
- return AutoCompleteHelper.GetDefaultCompletionItems(startLine, startColumn, endColumn);
+ return AutoCompleteHelper.GetDefaultCompletionItems(startLine, startColumn, endColumn, useLowerCaseSuggestions);
}
if (scriptParseInfo.IsConnected
&& scriptParseInfo.BuildingMetadataEvent.WaitOne(LanguageService.FindCompletionStartTimeout))
{
- scriptParseInfo.BuildingMetadataEvent.Reset();
- Task findCompletionsTask = Task.Run(() => {
- try
+ scriptParseInfo.BuildingMetadataEvent.Reset();
+
+ QueueItem queueItem = this.BindingQueue.QueueBindingOperation(
+ key: scriptParseInfo.ConnectionKey,
+ bindingTimeout: LanguageService.BindingTimeout,
+ bindOperation: (bindingContext, cancelToken) =>
{
- // get the completion list from SQL Parser
- scriptParseInfo.CurrentSuggestions = Resolver.FindCompletions(
- scriptParseInfo.ParseResult,
- textDocumentPosition.Position.Line + 1,
- textDocumentPosition.Position.Character + 1,
- scriptParseInfo.MetadataDisplayInfoProvider);
+ CompletionItem[] completions = null;
+ try
+ {
+ // get the completion list from SQL Parser
+ scriptParseInfo.CurrentSuggestions = Resolver.FindCompletions(
+ scriptParseInfo.ParseResult,
+ textDocumentPosition.Position.Line + 1,
+ textDocumentPosition.Position.Character + 1,
+ bindingContext.MetadataDisplayInfoProvider);
- // cache the current script parse info object to resolve completions later
- this.currentCompletionParseInfo = scriptParseInfo;
+ // cache the current script parse info object to resolve completions later
+ this.currentCompletionParseInfo = scriptParseInfo;
- // convert the suggestion list to the VS Code format
- return AutoCompleteHelper.ConvertDeclarationsToCompletionItems(
- scriptParseInfo.CurrentSuggestions,
- startLine,
- startColumn,
- endColumn);
- }
- finally
+ // convert the suggestion list to the VS Code format
+ completions = AutoCompleteHelper.ConvertDeclarationsToCompletionItems(
+ scriptParseInfo.CurrentSuggestions,
+ startLine,
+ startColumn,
+ endColumn);
+ }
+ finally
+ {
+ scriptParseInfo.BuildingMetadataEvent.Set();
+ }
+
+ return completions;
+ },
+ timeoutOperation: (bindingContext) =>
{
- scriptParseInfo.BuildingMetadataEvent.Set();
- }
- });
-
- findCompletionsTask.Wait(LanguageService.FindCompletionsTimeout);
- if (findCompletionsTask.IsCompleted
- && findCompletionsTask.Result != null
- && findCompletionsTask.Result.Length > 0)
+ return AutoCompleteHelper.GetDefaultCompletionItems(startLine, startColumn, endColumn, useLowerCaseSuggestions);
+ });
+
+ queueItem.ItemProcessed.WaitOne();
+ var completionItems = queueItem.GetResultAsT();
+ if (completionItems != null && completionItems.Length > 0)
{
- return findCompletionsTask.Result;
+ return completionItems;
}
}
- return AutoCompleteHelper.GetDefaultCompletionItems(startLine, startColumn, endColumn);
+ return AutoCompleteHelper.GetDefaultCompletionItems(startLine, startColumn, endColumn, useLowerCaseSuggestions);
}
#endregion
@@ -614,7 +811,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
///
private Task RunScriptDiagnostics(ScriptFile[] filesToAnalyze, EventContext eventContext)
{
- if (!CurrentSettings.ScriptAnalysis.Enable.Value)
+ if (!CurrentSettings.IsDiagnositicsEnabled)
{
// If the user has disabled script analysis, skip it entirely
return Task.FromResult(true);
@@ -710,7 +907,12 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
#endregion
- private void AddOrUpdateScriptParseInfo(string uri, ScriptParseInfo scriptInfo)
+ ///
+ /// Adds a new or updates an existing script parse info instance in local cache
+ ///
+ ///
+ ///
+ internal void AddOrUpdateScriptParseInfo(string uri, ScriptParseInfo scriptInfo)
{
lock (this.parseMapLock)
{
@@ -726,7 +928,13 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
}
}
- private ScriptParseInfo GetScriptParseInfo(string uri, bool createIfNotExists = false)
+ ///
+ /// Gets a script parse info object for a file from the local cache
+ /// Internal for testing purposes only
+ ///
+ ///
+ /// Creates a new instance if one doesn't exist
+ internal ScriptParseInfo GetScriptParseInfo(string uri, bool createIfNotExists = false)
{
lock (this.parseMapLock)
{
@@ -736,6 +944,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
}
else if (createIfNotExists)
{
+ // create a new script parse info object and initialize with the current settings
ScriptParseInfo scriptInfo = new ScriptParseInfo();
this.ScriptParseInfoMap.Add(uri, scriptInfo);
return scriptInfo;
@@ -752,10 +961,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
lock (this.parseMapLock)
{
if (this.ScriptParseInfoMap.ContainsKey(uri))
- {
- var scriptInfo = this.ScriptParseInfoMap[uri];
- scriptInfo.ServerConnection.Disconnect();
- scriptInfo.ServerConnection = null;
+ {
return this.ScriptParseInfoMap.Remove(uri);
}
else
diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/QueueItem.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/QueueItem.cs
new file mode 100644
index 00000000..adf5fa18
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/QueueItem.cs
@@ -0,0 +1,66 @@
+//
+// 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;
+using System.Threading.Tasks;
+
+namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
+{
+ ///
+ /// Class that stores the state of a binding queue request item
+ ///
+ public class QueueItem
+ {
+ ///
+ /// QueueItem constructor
+ ///
+ public QueueItem()
+ {
+ this.ItemProcessed = new ManualResetEvent(initialState: false);
+ }
+
+ ///
+ /// Gets or sets the queue item key
+ ///
+ public string Key { get; set; }
+
+ ///
+ /// Gets or sets the bind operation callback method
+ ///
+ public Func BindOperation { get; set; }
+
+ ///
+ /// Gets or sets the timeout operation to call if the bind operation doesn't finish within timeout period
+ ///
+ public Func TimeoutOperation { get; set; }
+
+ ///
+ /// Gets or sets an event to signal when this queue item has been processed
+ ///
+ public ManualResetEvent ItemProcessed { get; set; }
+
+ ///
+ /// Gets or sets the result of the queued task
+ ///
+ public object Result { get; set; }
+
+ ///
+ /// Gets or sets the binding operation timeout in milliseconds
+ ///
+ public int? BindingTimeout { get; set; }
+
+ ///
+ /// Converts the result of the execution to type T
+ ///
+ public T GetResultAsT() where T : class
+ {
+ //var task = this.ResultsTask;
+ return (this.Result != null)
+ ? this.Result as T
+ : null;
+ }
+ }
+}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/ScriptParseInfo.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/ScriptParseInfo.cs
index 7dca96ab..2c56d497 100644
--- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/ScriptParseInfo.cs
+++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/ScriptParseInfo.cs
@@ -3,15 +3,9 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
-using System;
using System.Collections.Generic;
using System.Threading;
-using Microsoft.SqlServer.Management.Common;
-using Microsoft.SqlServer.Management.SmoMetadataProvider;
-using Microsoft.SqlServer.Management.SqlParser.Binder;
-using Microsoft.SqlServer.Management.SqlParser.Common;
using Microsoft.SqlServer.Management.SqlParser.Intellisense;
-using Microsoft.SqlServer.Management.SqlParser.MetadataProvider;
using Microsoft.SqlServer.Management.SqlParser.Parser;
namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
@@ -23,10 +17,6 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
{
private ManualResetEvent buildingMetadataEvent = new ManualResetEvent(initialState: true);
- private ParseOptions parseOptions = new ParseOptions();
-
- private ServerConnection serverConnection;
-
///
/// Event which tells if MetadataProvider is built fully or not
///
@@ -41,163 +31,18 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
public bool IsConnected { get; set; }
///
- /// Gets or sets the LanguageService SMO ServerConnection
+ /// Gets or sets the binding queue connection context key
///
- public ServerConnection ServerConnection
- {
- get
- {
- return this.serverConnection;
- }
- set
- {
- this.serverConnection = value;
- this.parseOptions = new ParseOptions(
- batchSeparator: LanguageService.DefaultBatchSeperator,
- isQuotedIdentifierSet: true,
- compatibilityLevel: DatabaseCompatibilityLevel,
- transactSqlVersion: TransactSqlVersion);
- }
- }
-
- ///
- /// Gets the Language Service ServerVersion
- ///
- public ServerVersion ServerVersion
- {
- get
- {
- return this.ServerConnection != null
- ? this.ServerConnection.ServerVersion
- : null;
- }
- }
-
- ///
- /// Gets the current DataEngineType
- ///
- public DatabaseEngineType DatabaseEngineType
- {
- get
- {
- return this.ServerConnection != null
- ? this.ServerConnection.DatabaseEngineType
- : DatabaseEngineType.Standalone;
- }
- }
-
- ///
- /// Gets the current connections TransactSqlVersion
- ///
- public TransactSqlVersion TransactSqlVersion
- {
- get
- {
- return this.IsConnected
- ? GetTransactSqlVersion(this.ServerVersion)
- : TransactSqlVersion.Current;
- }
- }
-
- ///
- /// Gets the current DatabaseCompatibilityLevel
- ///
- public DatabaseCompatibilityLevel DatabaseCompatibilityLevel
- {
- get
- {
- return this.IsConnected
- ? GetDatabaseCompatibilityLevel(this.ServerVersion)
- : DatabaseCompatibilityLevel.Current;
- }
- }
-
- ///
- /// Gets the current ParseOptions
- ///
- public ParseOptions ParseOptions
- {
- get
- {
- return this.parseOptions;
- }
- }
-
- ///
- /// Gets or sets the SMO binder for schema-aware intellisense
- ///
- public IBinder Binder { get; set; }
+ public string ConnectionKey { get; set; }
///
/// Gets or sets the previous SQL parse result
///
public ParseResult ParseResult { get; set; }
-
- ///
- /// Gets or set the SMO metadata provider that's bound to the current connection
- ///
- public SmoMetadataProvider MetadataProvider { get; set; }
-
- ///
- /// Gets or sets the SMO metadata display info provider
- ///
- public MetadataDisplayInfoProvider MetadataDisplayInfoProvider { get; set; }
///
/// Gets or sets the current autocomplete suggestion list
///
public IEnumerable CurrentSuggestions { get; set; }
-
- ///
- /// Gets the database compatibility level from a server version
- ///
- ///
- private static DatabaseCompatibilityLevel GetDatabaseCompatibilityLevel(ServerVersion serverVersion)
- {
- int versionMajor = Math.Max(serverVersion.Major, 8);
-
- switch (versionMajor)
- {
- case 8:
- return DatabaseCompatibilityLevel.Version80;
- case 9:
- return DatabaseCompatibilityLevel.Version90;
- case 10:
- return DatabaseCompatibilityLevel.Version100;
- case 11:
- return DatabaseCompatibilityLevel.Version110;
- case 12:
- return DatabaseCompatibilityLevel.Version120;
- case 13:
- return DatabaseCompatibilityLevel.Version130;
- default:
- return DatabaseCompatibilityLevel.Current;
- }
- }
-
- ///
- /// Gets the transaction sql version from a server version
- ///
- ///
- private static TransactSqlVersion GetTransactSqlVersion(ServerVersion serverVersion)
- {
- int versionMajor = Math.Max(serverVersion.Major, 9);
-
- switch (versionMajor)
- {
- case 9:
- case 10:
- // In case of 10.0 we still use Version 10.5 as it is the closest available.
- return TransactSqlVersion.Version105;
- case 11:
- return TransactSqlVersion.Version110;
- case 12:
- return TransactSqlVersion.Version120;
- case 13:
- return TransactSqlVersion.Version130;
- default:
- return TransactSqlVersion.Current;
- }
- }
}
}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/Program.cs b/src/Microsoft.SqlTools.ServiceLayer/Program.cs
index 3e5a8655..35a659fe 100644
--- a/src/Microsoft.SqlTools.ServiceLayer/Program.cs
+++ b/src/Microsoft.SqlTools.ServiceLayer/Program.cs
@@ -16,26 +16,22 @@ namespace Microsoft.SqlTools.ServiceLayer
///
/// Main application class for SQL Tools API Service Host executable
///
- class Program
+ internal class Program
{
///
/// Main entry point into the SQL Tools API Service Host
///
- static void Main(string[] args)
+ internal static void Main(string[] args)
{
// turn on Verbose logging during early development
// we need to switch to Normal when preparing for public preview
Logger.Initialize(minimumLogLevel: LogLevel.Verbose);
Logger.Write(LogLevel.Normal, "Starting SQL Tools Service Host");
- const string hostName = "SQL Tools Service Host";
- const string hostProfileId = "SQLToolsService";
- Version hostVersion = new Version(1,0);
-
// set up the host details and profile paths
- var hostDetails = new HostDetails(hostName, hostProfileId, hostVersion);
- var profilePaths = new ProfilePaths(hostProfileId, "baseAllUsersPath", "baseCurrentUserPath");
- SqlToolsContext sqlToolsContext = new SqlToolsContext(hostDetails, profilePaths);
+ var hostDetails = new HostDetails(version: new Version(1,0));
+
+ SqlToolsContext sqlToolsContext = new SqlToolsContext(hostDetails);
// Grab the instance of the service host
ServiceHost serviceHost = ServiceHost.Instance;
diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Batch.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Batch.cs
index acdd70a9..51a35a7d 100644
--- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Batch.cs
+++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Batch.cs
@@ -6,6 +6,7 @@ using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
+using System.Diagnostics;
using System.Data.SqlClient;
using System.Linq;
using System.Threading;
@@ -37,7 +38,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
///
/// Internal representation of the messages so we can modify internally
///
- private readonly List resultMessages;
+ private readonly List resultMessages;
///
/// Internal representation of the result sets so we can modify internally
@@ -46,7 +47,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
#endregion
- internal Batch(string batchText, int startLine, IFileStreamFactory outputFileFactory)
+ internal Batch(string batchText, int startLine, int startColumn, int endLine, int endColumn, IFileStreamFactory outputFileFactory)
{
// Sanity check for input
Validate.IsNotNullOrEmptyString(nameof(batchText), batchText);
@@ -54,10 +55,10 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
// Initialize the internal state
BatchText = batchText;
- StartLine = startLine - 1; // -1 to make sure that the line number of the batch is 0-indexed, since SqlParser gives 1-indexed line numbers
+ Selection = new SelectionData(startLine, startColumn, endLine, endColumn);
HasExecuted = false;
resultSets = new List();
- resultMessages = new List();
+ resultMessages = new List();
this.outputFileFactory = outputFileFactory;
}
@@ -81,7 +82,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
///
/// Messages that have come back from the server
///
- public IEnumerable ResultMessages
+ public IEnumerable ResultMessages
{
get { return resultMessages; }
}
@@ -111,9 +112,9 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
}
///
- /// The 0-indexed line number that this batch started on
+ /// The range from the file that is this batch
///
- internal int StartLine { get; set; }
+ internal SelectionData Selection { get; set; }
#endregion
@@ -134,19 +135,30 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
try
{
+ DbCommand command = null;
+
// Register the message listener to *this instance* of the batch
// Note: This is being done to associate messages with batches
ReliableSqlConnection sqlConn = conn as ReliableSqlConnection;
if (sqlConn != null)
{
sqlConn.GetUnderlyingConnection().InfoMessage += StoreDbMessage;
+ command = sqlConn.GetUnderlyingConnection().CreateCommand();
+ }
+ else
+ {
+ command = conn.CreateCommand();
}
+ // Make sure we aren't using a ReliableCommad since we do not want automatic retry
+ Debug.Assert(!(command is ReliableSqlConnection.ReliableSqlCommand), "ReliableSqlCommand command should not be used to execute queries");
+
// Create a command that we'll use for executing the query
- using (DbCommand command = conn.CreateCommand())
+ using (command)
{
command.CommandText = BatchText;
command.CommandType = CommandType.Text;
+ command.CommandTimeout = 0;
// Execute the command to get back a reader
using (DbDataReader reader = await command.ExecuteReaderAsync(cancellationToken))
@@ -157,9 +169,9 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
if (!reader.HasRows && reader.FieldCount == 0)
{
// Create a message with the number of affected rows -- IF the query affects rows
- resultMessages.Add(reader.RecordsAffected >= 0
- ? SR.QueryServiceAffectedRows(reader.RecordsAffected)
- : SR.QueryServiceCompletedSuccessfully);
+ resultMessages.Add(new ResultMessage(reader.RecordsAffected >= 0
+ ? SR.QueryServiceAffectedRows(reader.RecordsAffected)
+ : SR.QueryServiceCompletedSuccessfully));
continue;
}
@@ -172,7 +184,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
resultSets.Add(resultSet);
// Add a message for the number of rows the query returned
- resultMessages.Add(SR.QueryServiceAffectedRows(resultSet.RowCount));
+ resultMessages.Add(new ResultMessage(SR.QueryServiceAffectedRows(resultSet.RowCount)));
} while (await reader.NextResultAsync(cancellationToken));
}
}
@@ -190,10 +202,10 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
finally
{
// Remove the message event handler from the connection
- SqlConnection sqlConn = conn as SqlConnection;
+ ReliableSqlConnection sqlConn = conn as ReliableSqlConnection;
if (sqlConn != null)
{
- sqlConn.InfoMessage -= StoreDbMessage;
+ sqlConn.GetUnderlyingConnection().InfoMessage -= StoreDbMessage;
}
// Mark that we have executed
@@ -233,7 +245,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
/// Arguments from the event
private void StoreDbMessage(object sender, SqlInfoMessageEventArgs args)
{
- resultMessages.Add(args.Message);
+ resultMessages.Add(new ResultMessage(args.Message));
}
///
@@ -253,16 +265,17 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
SqlError sqlError = error as SqlError;
if (sqlError != null)
{
- int lineNumber = sqlError.LineNumber + StartLine;
- string message = SR.QueryServiceErrorFormat(sqlError.Number, sqlError.Class, sqlError.State,
- lineNumber, Environment.NewLine, sqlError.Message);
- resultMessages.Add(message);
+ int lineNumber = sqlError.LineNumber + Selection.StartLine;
+ string message = string.Format("Msg {0}, Level {1}, State {2}, Line {3}{4}{5}",
+ sqlError.Number, sqlError.Class, sqlError.State, lineNumber,
+ Environment.NewLine, sqlError.Message);
+ resultMessages.Add(new ResultMessage(message));
}
}
}
else
{
- resultMessages.Add(dbe.Message);
+ resultMessages.Add(new ResultMessage(dbe.Message));
}
}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/BatchSummary.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/BatchSummary.cs
index 73d1d4c8..7e1b2837 100644
--- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/BatchSummary.cs
+++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/BatchSummary.cs
@@ -20,10 +20,15 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts
///
public int Id { get; set; }
+ ///
+ /// The selection from the file for this batch
+ ///
+ public SelectionData Selection { get; set; }
+
///
/// Any messages that came back from the server during execution of the batch
///
- public string[] Messages { get; set; }
+ public ResultMessage[] Messages { get; set; }
///
/// The summaries of the result sets inside the batch
diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/DbCellValue.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/DbCellValue.cs
new file mode 100644
index 00000000..6eabb4d3
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/DbCellValue.cs
@@ -0,0 +1,23 @@
+//
+// 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.QueryExecution.Contracts
+{
+ ///
+ /// Class used for internally passing results from a cell around.
+ ///
+ public class DbCellValue
+ {
+ ///
+ /// Display value for the cell, suitable to be passed back to the client
+ ///
+ public string DisplayValue { get; set; }
+
+ ///
+ /// The raw object for the cell, for use internally
+ ///
+ internal object RawObject { get; set; }
+ }
+}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/DbColumnWrapper.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/DbColumnWrapper.cs
index 7574a7de..9e387f8c 100644
--- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/DbColumnWrapper.cs
+++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/DbColumnWrapper.cs
@@ -182,7 +182,12 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts
///
/// Whether or not the column is XML
///
- public bool IsXml { get; private set; }
+ public bool IsXml { get; set; }
+
+ ///
+ /// Whether or not the column is JSON
+ ///
+ public bool IsJson { get; set; }
#endregion
diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryExecuteRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryExecuteRequest.cs
index cac98c1a..6079bf51 100644
--- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryExecuteRequest.cs
+++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryExecuteRequest.cs
@@ -7,15 +7,30 @@ using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts
{
+ ///
+ /// Container class for a selection range from file
+ ///
+ public class SelectionData {
+ public int StartLine { get; set; }
+ public int StartColumn { get; set; }
+ public int EndLine { get; set; }
+ public int EndColumn { get; set; }
+ public SelectionData(int startLine, int startColumn, int endLine, int endColumn) {
+ StartLine = startLine;
+ StartColumn = startColumn;
+ EndLine = endLine;
+ EndColumn = endColumn;
+ }
+ }
///
/// Parameters for the query execute request
///
public class QueryExecuteParams
{
///
- /// The text of the query to execute
+ /// The selection from the document
///
- public string QueryText { get; set; }
+ public SelectionData QuerySelection { get; set; }
///
/// URI for the editor that is asking for the query execute
diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/ResultMessage.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/ResultMessage.cs
new file mode 100644
index 00000000..27e6713b
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/ResultMessage.cs
@@ -0,0 +1,44 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+
+using System;
+
+namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts
+{
+ ///
+ /// Result message object with timestamp and actual message
+ ///
+ public class ResultMessage
+ {
+ ///
+ /// Timestamp of the message
+ /// Stored in UTC ISO 8601 format; should be localized before displaying to any user
+ ///
+ public string Time { get; set; }
+
+ ///
+ /// Message contents
+ ///
+ public string Message { get; set; }
+
+ ///
+ /// Full constructor
+ ///
+ public ResultMessage(string timeStamp, string message)
+ {
+ Time = timeStamp;
+ Message = message;
+ }
+
+ ///
+ /// Constructor with default "Now" time
+ ///
+ public ResultMessage(string message)
+ {
+ Time = DateTime.Now.ToString("o");
+ Message = message;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/ResultSetSubset.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/ResultSetSubset.cs
index 8e2b49a9..62308824 100644
--- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/ResultSetSubset.cs
+++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/ResultSetSubset.cs
@@ -19,6 +19,6 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts
///
/// 2D array of the cell values requested from result set
///
- public object[][] Rows { get; set; }
+ public string[][] Rows { get; set; }
}
}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/SaveResultsRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/SaveResultsRequest.cs
index 721d13c9..1cf2390e 100644
--- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/SaveResultsRequest.cs
+++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/SaveResultsRequest.cs
@@ -32,6 +32,28 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts
/// URI for the editor that called save results
///
public string OwnerUri { get; set; }
+
+ ///
+ /// Start index of the selected rows (inclusive)
+ ///
+ public int? RowStartIndex { get; set; }
+
+ ///
+ /// End index of the selected rows (inclusive)
+ ///
+ public int? RowEndIndex { get; set; }
+
+ ///
+ /// Start index of the selected columns (inclusive)
+ ///
+ ///
+ public int? ColumnStartIndex { get; set; }
+
+ ///
+ /// End index of the selected columns (inclusive)
+ ///
+ ///
+ public int? ColumnEndIndex { get; set; }
}
///
diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/FileStreamReadResult.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/FileStreamReadResult.cs
index 61ee62e0..0939c9d4 100644
--- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/FileStreamReadResult.cs
+++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/FileStreamReadResult.cs
@@ -3,25 +3,16 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
+using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts;
+
namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
{
///
/// Represents a value returned from a read from a file stream. This is used to eliminate ref
/// parameters used in the read methods.
///
- /// The type of the value that was read
- public struct FileStreamReadResult
+ public struct FileStreamReadResult
{
- ///
- /// Whether or not the value of the field is null
- ///
- public bool IsNull { get; set; }
-
- ///
- /// The value of the field. If is true, this will be set to default(T)
- ///
- public T Value { get; set; }
-
///
/// The total length in bytes of the value, (including the bytes used to store the length
/// of the value)
@@ -34,17 +25,20 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
///
public int TotalLength { get; set; }
+ ///
+ /// Value of the cell
+ ///
+ public DbCellValue Value { get; set; }
+
///
/// Constructs a new FileStreamReadResult
///
- /// The value of the result
- /// The number of bytes for the used to store the value's length and value
- /// Whether or not the value is null
- public FileStreamReadResult(T value, int totalLength, bool isNull)
+ /// The value of the result, ready for consumption by a client
+ /// The number of bytes for the used to store the value's length and values
+ public FileStreamReadResult(DbCellValue value, int totalLength)
{
Value = value;
TotalLength = totalLength;
- IsNull = isNull;
}
}
}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/FileStreamWrapper.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/FileStreamWrapper.cs
index b8737cb9..74297a42 100644
--- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/FileStreamWrapper.cs
+++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/FileStreamWrapper.cs
@@ -53,7 +53,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
{
// Sanity check for valid buffer length, fileName, and accessMethod
Validate.IsGreaterThan(nameof(bufferLength), bufferLength, 0);
- Validate.IsNotNullOrEmptyString(nameof(fileName), fileName);
+ Validate.IsNotNullOrWhitespaceString(nameof(fileName), fileName);
if (accessMethod == FileAccess.Write)
{
throw new ArgumentException(SR.QueryServiceFileWrapperWriteOnly, nameof(fileName));
diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/IFileStreamReader.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/IFileStreamReader.cs
index ea5584f1..cfbe4fa1 100644
--- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/IFileStreamReader.cs
+++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/IFileStreamReader.cs
@@ -5,7 +5,6 @@
using System;
using System.Collections.Generic;
-using System.Data.SqlTypes;
using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts;
namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
@@ -15,21 +14,23 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
///
public interface IFileStreamReader : IDisposable
{
- object[] ReadRow(long offset, IEnumerable columns);
- FileStreamReadResult ReadInt16(long i64Offset);
- FileStreamReadResult ReadInt32(long i64Offset);
- FileStreamReadResult ReadInt64(long i64Offset);
- FileStreamReadResult ReadByte(long i64Offset);
- FileStreamReadResult ReadChar(long i64Offset);
- FileStreamReadResult ReadBoolean(long i64Offset);
- FileStreamReadResult ReadSingle(long i64Offset);
- FileStreamReadResult ReadDouble(long i64Offset);
- FileStreamReadResult ReadSqlDecimal(long i64Offset);
- FileStreamReadResult ReadDecimal(long i64Offset);
- FileStreamReadResult ReadDateTime(long i64Offset);
- FileStreamReadResult ReadTimeSpan(long i64Offset);
- FileStreamReadResult ReadString(long i64Offset);
- FileStreamReadResult ReadBytes(long i64Offset);
- FileStreamReadResult ReadDateTimeOffset(long i64Offset);
+ IList ReadRow(long offset, IEnumerable columns);
+ FileStreamReadResult ReadInt16(long i64Offset);
+ FileStreamReadResult ReadInt32(long i64Offset);
+ FileStreamReadResult ReadInt64(long i64Offset);
+ FileStreamReadResult ReadByte(long i64Offset);
+ FileStreamReadResult ReadChar(long i64Offset);
+ FileStreamReadResult ReadBoolean(long i64Offset);
+ FileStreamReadResult ReadSingle(long i64Offset);
+ FileStreamReadResult ReadDouble(long i64Offset);
+ FileStreamReadResult ReadSqlDecimal(long i64Offset);
+ FileStreamReadResult ReadDecimal(long i64Offset);
+ FileStreamReadResult ReadDateTime(long i64Offset);
+ FileStreamReadResult ReadTimeSpan(long i64Offset);
+ FileStreamReadResult ReadString(long i64Offset);
+ FileStreamReadResult ReadBytes(long i64Offset);
+ FileStreamReadResult ReadDateTimeOffset(long i64Offset);
+ FileStreamReadResult ReadGuid(long offset);
+ FileStreamReadResult ReadMoney(long offset);
}
}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/IFileStreamWriter.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/IFileStreamWriter.cs
index 968701ed..7cfffee8 100644
--- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/IFileStreamWriter.cs
+++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/IFileStreamWriter.cs
@@ -29,7 +29,9 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
int WriteDateTimeOffset(DateTimeOffset dtoVal);
int WriteTimeSpan(TimeSpan val);
int WriteString(string val);
- int WriteBytes(byte[] bytes, int length);
+ int WriteBytes(byte[] bytes);
+ int WriteGuid(Guid val);
+ int WriteMoney(SqlMoney val);
void FlushBuffer();
}
}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/ServiceBufferFileStreamReader.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/ServiceBufferFileStreamReader.cs
index 9772744f..8547999b 100644
--- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/ServiceBufferFileStreamReader.cs
+++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/ServiceBufferFileStreamReader.cs
@@ -6,7 +6,6 @@
using System;
using System.Collections.Generic;
using System.Data.SqlTypes;
-using System.Diagnostics;
using System.IO;
using System.Text;
using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts;
@@ -26,6 +25,8 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
private readonly IFileStreamWrapper fileStream;
+ private Dictionary> readMethods;
+
#endregion
///
@@ -41,6 +42,40 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
// Create internal buffer
buffer = new byte[DefaultBufferSize];
+
+ // Create the methods that will be used to read back
+ readMethods = new Dictionary>
+ {
+ {typeof(string), ReadString},
+ {typeof(short), ReadInt16},
+ {typeof(int), ReadInt32},
+ {typeof(long), ReadInt64},
+ {typeof(byte), ReadByte},
+ {typeof(char), ReadChar},
+ {typeof(bool), ReadBoolean},
+ {typeof(double), ReadDouble},
+ {typeof(float), ReadSingle},
+ {typeof(decimal), ReadDecimal},
+ {typeof(DateTime), ReadDateTime},
+ {typeof(DateTimeOffset), ReadDateTimeOffset},
+ {typeof(TimeSpan), ReadTimeSpan},
+ {typeof(byte[]), ReadBytes},
+
+ {typeof(SqlString), ReadString},
+ {typeof(SqlInt16), ReadInt16},
+ {typeof(SqlInt32), ReadInt32},
+ {typeof(SqlInt64), ReadInt64},
+ {typeof(SqlByte), ReadByte},
+ {typeof(SqlBoolean), ReadBoolean},
+ {typeof(SqlDouble), ReadDouble},
+ {typeof(SqlSingle), ReadSingle},
+ {typeof(SqlDecimal), ReadSqlDecimal},
+ {typeof(SqlDateTime), ReadDateTime},
+ {typeof(SqlBytes), ReadBytes},
+ {typeof(SqlBinary), ReadBytes},
+ {typeof(SqlGuid), ReadGuid},
+ {typeof(SqlMoney), ReadMoney},
+ };
}
#region IFileStreamStorage Implementation
@@ -50,12 +85,12 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
///
/// Offset into the file where the row starts
/// The columns that were encoded
- /// The objects from the row
- public object[] ReadRow(long fileOffset, IEnumerable columns)
+ /// The objects from the row, ready for output to the client
+ public IList ReadRow(long fileOffset, IEnumerable columns)
{
// Initialize for the loop
long currentFileOffset = fileOffset;
- List
[Fact]
- public void SaveResultsAsCsvSuccessTest()
+ public async void SaveResultsAsCsvSuccessTest()
{
+
+ // Set up file for returning the query
+ var fileMock = new Mock();
+ fileMock.SetupGet(file => file.Contents).Returns(Common.StandardQuery);
+ // Set up workspace mock
+ var workspaceService = new Mock>();
+ workspaceService.Setup(service => service.Workspace.GetFile(It.IsAny()))
+ .Returns(fileMock.Object);
// Execute a query
- var queryService = Common.GetPrimedExecutionService(Common.CreateMockFactory(null, false), true);
- var executeParams = new QueryExecuteParams { QueryText = Common.StandardQuery, OwnerUri = Common.OwnerUri };
+ var queryService = await Common.GetPrimedExecutionService(Common.CreateMockFactory(null, false), true, workspaceService.Object);
+ var executeParams = new QueryExecuteParams { QuerySelection = Common.WholeDocument, OwnerUri = Common.OwnerUri };
var executeRequest = GetQueryExecuteResultContextMock(null, null, null);
queryService.HandleExecuteRequest(executeParams, executeRequest.Object).Wait();
@@ -57,15 +68,75 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution
}
}
+ ///
+ /// Test save results to a file as CSV with a selection of cells and correct parameters
+ ///
+ [Fact]
+ public async void SaveResultsAsCsvWithSelectionSuccessTest()
+ {
+
+ // Set up file for returning the query
+ var fileMock = new Mock();
+ fileMock.SetupGet(file => file.Contents).Returns(Common.StandardQuery);
+ // Set up workspace mock
+ var workspaceService = new Mock>();
+ workspaceService.Setup(service => service.Workspace.GetFile(It.IsAny()))
+ .Returns(fileMock.Object);
+
+ // Execute a query
+ var queryService = await Common.GetPrimedExecutionService(Common.CreateMockFactory(null, false), true, workspaceService.Object);
+ var executeParams = new QueryExecuteParams { QuerySelection = Common.WholeDocument , OwnerUri = Common.OwnerUri };
+ var executeRequest = GetQueryExecuteResultContextMock(null, null, null);
+ queryService.HandleExecuteRequest(executeParams, executeRequest.Object).Wait();
+
+ // Request to save the results as csv with correct parameters
+ var saveParams = new SaveResultsAsCsvRequestParams
+ {
+ OwnerUri = Common.OwnerUri,
+ ResultSetIndex = 0,
+ BatchIndex = 0,
+ FilePath = "testwrite_2.csv",
+ IncludeHeaders = true,
+ RowStartIndex = 0,
+ RowEndIndex = 0,
+ ColumnStartIndex = 0,
+ ColumnEndIndex = 0
+ };
+ SaveResultRequestResult result = null;
+ var saveRequest = GetSaveResultsContextMock(qcr => result = qcr, null);
+ queryService.ActiveQueries[Common.OwnerUri].Batches[0] = Common.GetBasicExecutedBatch();
+ queryService.HandleSaveResultsAsCsvRequest(saveParams, saveRequest.Object).Wait();
+
+ // Expect to see a file successfully created in filepath and a success message
+ Assert.Null(result.Messages);
+ Assert.True(File.Exists(saveParams.FilePath));
+ VerifySaveResultsCallCount(saveRequest, Times.Once(), Times.Never());
+
+ // Delete temp file after test
+ if (File.Exists(saveParams.FilePath))
+ {
+ File.Delete(saveParams.FilePath);
+ }
+ }
+
///
/// Test handling exception in saving results to CSV file
///
[Fact]
- public void SaveResultsAsCsvExceptionTest()
+ public async void SaveResultsAsCsvExceptionTest()
{
+
+ // Set up file for returning the query
+ var fileMock = new Mock();
+ fileMock.SetupGet(file => file.Contents).Returns(Common.StandardQuery);
+ // Set up workspace mock
+ var workspaceService = new Mock>();
+ workspaceService.Setup(service => service.Workspace.GetFile(It.IsAny()))
+ .Returns(fileMock.Object);
+
// Execute a query
- var queryService = Common.GetPrimedExecutionService(Common.CreateMockFactory(null, false), true);
- var executeParams = new QueryExecuteParams { QueryText = Common.StandardQuery, OwnerUri = Common.OwnerUri };
+ var queryService = await Common.GetPrimedExecutionService(Common.CreateMockFactory(null, false), true, workspaceService.Object);
+ var executeParams = new QueryExecuteParams { QuerySelection = Common.WholeDocument, OwnerUri = Common.OwnerUri };
var executeRequest = GetQueryExecuteResultContextMock(null, null, null);
queryService.HandleExecuteRequest(executeParams, executeRequest.Object).Wait();
@@ -93,11 +164,13 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution
/// Test saving results to CSV file when the requested result set is no longer active
///
[Fact]
- public void SaveResultsAsCsvQueryNotFoundTest()
+ public async void SaveResultsAsCsvQueryNotFoundTest()
{
+
+ var workspaceService = new Mock>();
// Execute a query
- var queryService = Common.GetPrimedExecutionService(Common.CreateMockFactory(null, false), true);
- var executeParams = new QueryExecuteParams { QueryText = Common.StandardQuery, OwnerUri = Common.OwnerUri };
+ var queryService = await Common.GetPrimedExecutionService(Common.CreateMockFactory(null, false), true, workspaceService.Object);
+ var executeParams = new QueryExecuteParams { QuerySelection = Common.WholeDocument, OwnerUri = Common.OwnerUri };
var executeRequest = GetQueryExecuteResultContextMock(null, null, null);
queryService.HandleExecuteRequest(executeParams, executeRequest.Object).Wait();
@@ -123,11 +196,19 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution
/// Test save results to a file as JSON with correct parameters
///
[Fact]
- public void SaveResultsAsJsonSuccessTest()
+ public async void SaveResultsAsJsonSuccessTest()
{
+
+ // Set up file for returning the query
+ var fileMock = new Mock();
+ fileMock.SetupGet(file => file.Contents).Returns(Common.StandardQuery);
+ // Set up workspace mock
+ var workspaceService = new Mock>();
+ workspaceService.Setup(service => service.Workspace.GetFile(It.IsAny()))
+ .Returns(fileMock.Object);
// Execute a query
- var queryService = Common.GetPrimedExecutionService(Common.CreateMockFactory(null, false), true);
- var executeParams = new QueryExecuteParams { QueryText = Common.StandardQuery, OwnerUri = Common.OwnerUri };
+ var queryService = await Common.GetPrimedExecutionService(Common.CreateMockFactory(null, false), true, workspaceService.Object);
+ var executeParams = new QueryExecuteParams { QuerySelection = Common.WholeDocument, OwnerUri = Common.OwnerUri };
var executeRequest = GetQueryExecuteResultContextMock(null, null, null);
queryService.HandleExecuteRequest(executeParams, executeRequest.Object).Wait();
@@ -137,13 +218,62 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution
OwnerUri = Common.OwnerUri,
ResultSetIndex = 0,
BatchIndex = 0,
- FilePath = "testwrite_4.json"
+ FilePath = "testwrite_4.json"
};
SaveResultRequestResult result = null;
var saveRequest = GetSaveResultsContextMock(qcr => result = qcr, null);
queryService.ActiveQueries[Common.OwnerUri].Batches[0] = Common.GetBasicExecutedBatch();
queryService.HandleSaveResultsAsJsonRequest(saveParams, saveRequest.Object).Wait();
+
+ // Expect to see a file successfully created in filepath and a success message
+ Assert.Null(result.Messages);
+ Assert.True(File.Exists(saveParams.FilePath));
+ VerifySaveResultsCallCount(saveRequest, Times.Once(), Times.Never());
+ // Delete temp file after test
+ if (File.Exists(saveParams.FilePath))
+ {
+ File.Delete(saveParams.FilePath);
+ }
+ }
+
+ ///
+ /// Test save results to a file as JSON with a selection of cells and correct parameters
+ ///
+ [Fact]
+ public async void SaveResultsAsJsonWithSelectionSuccessTest()
+ {
+ // Set up file for returning the query
+ var fileMock = new Mock();
+ fileMock.SetupGet(file => file.Contents).Returns(Common.StandardQuery);
+ // Set up workspace mock
+ var workspaceService = new Mock>();
+ workspaceService.Setup(service => service.Workspace.GetFile(It.IsAny()))
+ .Returns(fileMock.Object);
+
+ // Execute a query
+ var queryService = await Common.GetPrimedExecutionService(Common.CreateMockFactory(null, false), true, workspaceService.Object);
+ var executeParams = new QueryExecuteParams { QuerySelection = Common.WholeDocument , OwnerUri = Common.OwnerUri };
+ var executeRequest = GetQueryExecuteResultContextMock(null, null, null);
+ queryService.HandleExecuteRequest(executeParams, executeRequest.Object).Wait();
+
+ // Request to save the results as json with correct parameters
+ var saveParams = new SaveResultsAsJsonRequestParams
+ {
+ OwnerUri = Common.OwnerUri,
+ ResultSetIndex = 0,
+ BatchIndex = 0,
+ FilePath = "testwrite_5.json",
+ RowStartIndex = 0,
+ RowEndIndex = 0,
+ ColumnStartIndex = 0,
+ ColumnEndIndex = 0
+ };
+ SaveResultRequestResult result = null;
+ var saveRequest = GetSaveResultsContextMock(qcr => result = qcr, null);
+ queryService.ActiveQueries[Common.OwnerUri].Batches[0] = Common.GetBasicExecutedBatch();
+ queryService.HandleSaveResultsAsJsonRequest(saveParams, saveRequest.Object).Wait();
+
// Expect to see a file successfully created in filepath and a success message
Assert.Null(result.Messages);
Assert.True(File.Exists(saveParams.FilePath));
@@ -160,11 +290,18 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution
/// Test handling exception in saving results to JSON file
///
[Fact]
- public void SaveResultsAsJsonExceptionTest()
+ public async void SaveResultsAsJsonExceptionTest()
{
+ // Set up file for returning the query
+ var fileMock = new Mock();
+ fileMock.SetupGet(file => file.Contents).Returns(Common.StandardQuery);
+ // Set up workspace mock
+ var workspaceService = new Mock>();
+ workspaceService.Setup(service => service.Workspace.GetFile(It.IsAny()))
+ .Returns(fileMock.Object);
// Execute a query
- var queryService = Common.GetPrimedExecutionService(Common.CreateMockFactory(null, false), true);
- var executeParams = new QueryExecuteParams { QueryText = Common.StandardQuery, OwnerUri = Common.OwnerUri };
+ var queryService = await Common.GetPrimedExecutionService(Common.CreateMockFactory(null, false), true, workspaceService.Object);
+ var executeParams = new QueryExecuteParams { QuerySelection = Common.WholeDocument, OwnerUri = Common.OwnerUri };
var executeRequest = GetQueryExecuteResultContextMock(null, null, null);
queryService.HandleExecuteRequest(executeParams, executeRequest.Object).Wait();
@@ -192,11 +329,12 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution
/// Test saving results to JSON file when the requested result set is no longer active
///
[Fact]
- public void SaveResultsAsJsonQueryNotFoundTest()
+ public async void SaveResultsAsJsonQueryNotFoundTest()
{
+ var workspaceService = new Mock>();
// Execute a query
- var queryService = Common.GetPrimedExecutionService(Common.CreateMockFactory(null, false), true);
- var executeParams = new QueryExecuteParams { QueryText = Common.StandardQuery, OwnerUri = Common.OwnerUri };
+ var queryService = await Common.GetPrimedExecutionService(Common.CreateMockFactory(null, false), true, workspaceService.Object);
+ var executeParams = new QueryExecuteParams { QuerySelection = Common.WholeDocument, OwnerUri = Common.OwnerUri };
var executeRequest = GetQueryExecuteResultContextMock(null, null, null);
queryService.HandleExecuteRequest(executeParams, executeRequest.Object).Wait();
diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/SubsetTests.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/SubsetTests.cs
index 1a50dd55..7b57971b 100644
--- a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/SubsetTests.cs
+++ b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/SubsetTests.cs
@@ -4,12 +4,15 @@
//
using System;
+using System.Linq;
using System.Threading.Tasks;
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol;
using Microsoft.SqlTools.ServiceLayer.QueryExecution;
using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts;
using Microsoft.SqlTools.ServiceLayer.SqlContext;
using Microsoft.SqlTools.ServiceLayer.Test.Utility;
+using Microsoft.SqlTools.ServiceLayer.Workspace;
+using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts;
using Moq;
using Xunit;
@@ -17,6 +20,48 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution
{
public class SubsetTests
{
+ #region ResultSet Class Tests
+
+ [Theory]
+ [InlineData(0,2)]
+ [InlineData(0,20)]
+ [InlineData(1,2)]
+ public void ResultSetValidTest(int startRow, int rowCount)
+ {
+ // Setup:
+ // ... I have a batch that has been executed
+ Batch b = Common.GetBasicExecutedBatch();
+
+ // If:
+ // ... I have a result set and I ask for a subset with valid arguments
+ ResultSet rs = b.ResultSets.First();
+ ResultSetSubset subset = rs.GetSubset(startRow, rowCount).Result;
+
+ // Then:
+ // ... I should get the requested number of rows back
+ Assert.Equal(Math.Min(rowCount, Common.StandardTestData.Length), subset.RowCount);
+ Assert.Equal(Math.Min(rowCount, Common.StandardTestData.Length), subset.Rows.Length);
+ }
+
+ [Theory]
+ [InlineData(-1, 2)] // Invalid start index, too low
+ [InlineData(10, 2)] // Invalid start index, too high
+ [InlineData(0, -1)] // Invalid row count, too low
+ [InlineData(0, 0)] // Invalid row count, zero
+ public void ResultSetInvalidParmsTest(int rowStartIndex, int rowCount)
+ {
+ // If:
+ // I have an executed batch with a resultset in it and request invalid result set from it
+ Batch b = Common.GetBasicExecutedBatch();
+ ResultSet rs = b.ResultSets.First();
+
+ // Then:
+ // ... It should throw an exception
+ Assert.ThrowsAsync(() => rs.GetSubset(rowStartIndex, rowCount)).Wait();
+ }
+
+ #endregion
+
#region Batch Class Tests
[Theory]
@@ -37,13 +82,9 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution
}
[Theory]
- [InlineData(-1, 0, 2)] // Invalid result set, too low
- [InlineData(2, 0, 2)] // Invalid result set, too high
- [InlineData(0, -1, 2)] // Invalid start index, too low
- [InlineData(0, 10, 2)] // Invalid start index, too high
- [InlineData(0, 0, -1)] // Invalid row count, too low
- [InlineData(0, 0, 0)] // Invalid row count, zero
- public void BatchSubsetInvalidParamsTest(int resultSetIndex, int rowStartInex, int rowCount)
+ [InlineData(-1)] // Invalid result set, too low
+ [InlineData(2)] // Invalid result set, too high
+ public void BatchSubsetInvalidParamsTest(int resultSetIndex)
{
// If I have an executed batch
Batch b = Common.GetBasicExecutedBatch();
@@ -51,7 +92,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution
// ... And I ask for a subset with an invalid result set index
// Then:
// ... It should throw an exception
- Assert.ThrowsAsync(() => b.GetSubset(resultSetIndex, rowStartInex, rowCount)).Wait();
+ Assert.ThrowsAsync(() => b.GetSubset(resultSetIndex, 0, 2)).Wait();
}
#endregion
@@ -91,11 +132,20 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution
[Fact]
public async Task SubsetServiceValidTest()
{
+
+ // Set up file for returning the query
+ var fileMock = new Mock();
+ fileMock.SetupGet(file => file.Contents).Returns(Common.StandardQuery);
+ // Set up workspace mock
+ var workspaceService = new Mock>();
+ workspaceService.Setup(service => service.Workspace.GetFile(It.IsAny()))
+ .Returns(fileMock.Object);
// If:
// ... I have a query that has results (doesn't matter what)
- var queryService =Common.GetPrimedExecutionService(
- Common.CreateMockFactory(new[] {Common.StandardTestData}, false), true);
- var executeParams = new QueryExecuteParams {QueryText = "Doesn'tMatter", OwnerUri = Common.OwnerUri};
+ var queryService = await Common.GetPrimedExecutionService(
+ Common.CreateMockFactory(new[] {Common.StandardTestData}, false), true,
+ workspaceService.Object);
+ var executeParams = new QueryExecuteParams {QuerySelection = null, OwnerUri = Common.OwnerUri};
var executeRequest = RequestContextMocks.SetupRequestContextMock(null, QueryExecuteCompleteEvent.Type, null, null);
await queryService.HandleExecuteRequest(executeParams, executeRequest.Object);
@@ -115,11 +165,13 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution
}
[Fact]
- public void SubsetServiceMissingQueryTest()
+ public async void SubsetServiceMissingQueryTest()
{
+
+ var workspaceService = new Mock>();
// If:
// ... I ask for a set of results for a file that hasn't executed a query
- var queryService = Common.GetPrimedExecutionService(Common.CreateMockFactory(null, false), true);
+ var queryService = await Common.GetPrimedExecutionService(Common.CreateMockFactory(null, false), true, workspaceService.Object);
var subsetParams = new QueryExecuteSubsetParams { OwnerUri = Common.OwnerUri, RowsCount = 1, ResultSetIndex = 0, RowsStartIndex = 0 };
QueryExecuteSubsetResult result = null;
var subsetRequest = GetQuerySubsetResultContextMock(qesr => result = qesr, null);
@@ -135,13 +187,22 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution
}
[Fact]
- public void SubsetServiceUnexecutedQueryTest()
+ public async void SubsetServiceUnexecutedQueryTest()
{
+
+ // Set up file for returning the query
+ var fileMock = new Mock();
+ fileMock.SetupGet(file => file.Contents).Returns(Common.StandardQuery);
+ // Set up workspace mock
+ var workspaceService = new Mock>();
+ workspaceService.Setup(service => service.Workspace.GetFile(It.IsAny()))
+ .Returns(fileMock.Object);
// If:
// ... I have a query that hasn't finished executing (doesn't matter what)
- var queryService = Common.GetPrimedExecutionService(
- Common.CreateMockFactory(new[] { Common.StandardTestData }, false), true);
- var executeParams = new QueryExecuteParams { QueryText = "Doesn'tMatter", OwnerUri = Common.OwnerUri };
+ var queryService = await Common.GetPrimedExecutionService(
+ Common.CreateMockFactory(new[] { Common.StandardTestData }, false), true,
+ workspaceService.Object);
+ var executeParams = new QueryExecuteParams { QuerySelection = null, OwnerUri = Common.OwnerUri };
var executeRequest = RequestContextMocks.SetupRequestContextMock(null, QueryExecuteCompleteEvent.Type, null, null);
queryService.HandleExecuteRequest(executeParams, executeRequest.Object).Wait();
queryService.ActiveQueries[Common.OwnerUri].HasExecuted = false;
@@ -162,13 +223,16 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution
}
[Fact]
- public void SubsetServiceOutOfRangeSubsetTest()
+ public async void SubsetServiceOutOfRangeSubsetTest()
{
+
+ var workspaceService = new Mock>();
// If:
// ... I have a query that doesn't have any result sets
- var queryService = Common.GetPrimedExecutionService(
- Common.CreateMockFactory(null, false), true);
- var executeParams = new QueryExecuteParams { QueryText = "Doesn'tMatter", OwnerUri = Common.OwnerUri };
+ var queryService = await Common.GetPrimedExecutionService(
+ Common.CreateMockFactory(null, false), true,
+ workspaceService.Object);
+ var executeParams = new QueryExecuteParams { QuerySelection = null, OwnerUri = Common.OwnerUri };
var executeRequest = RequestContextMocks.SetupRequestContextMock(null, QueryExecuteCompleteEvent.Type, null, null);
queryService.HandleExecuteRequest(executeParams, executeRequest.Object).Wait();
@@ -191,7 +255,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution
#region Mocking
- private Mock> GetQuerySubsetResultContextMock(
+ private static Mock> GetQuerySubsetResultContextMock(
Action resultCallback,
Action errorCallback)
{
@@ -218,7 +282,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution
return requestContext;
}
- private void VerifyQuerySubsetCallCount(Mock> mock, Times sendResultCalls,
+ private static void VerifyQuerySubsetCallCount(Mock> mock, Times sendResultCalls,
Times sendErrorCalls)
{
mock.Verify(rc => rc.SendResult(It.IsAny()), sendResultCalls);
diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/SqlContext/SettingsTests.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/SqlContext/SettingsTests.cs
new file mode 100644
index 00000000..fba65a29
--- /dev/null
+++ b/test/Microsoft.SqlTools.ServiceLayer.Test/SqlContext/SettingsTests.cs
@@ -0,0 +1,101 @@
+//
+// 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.SqlContext;
+using Xunit;
+
+namespace Microsoft.SqlTools.ServiceLayer.Test.LanguageServices
+{
+ ///
+ /// Tests for the SqlContext settins
+ ///
+ public class SettingsTests
+ {
+ ///
+ /// Validate that the Language Service default settings are as expected
+ ///
+ [Fact]
+ public void ValidateLanguageServiceDefaults()
+ {
+ var sqlToolsSettings = new SqlToolsSettings();
+ Assert.True(sqlToolsSettings.IsDiagnositicsEnabled);
+ Assert.True(sqlToolsSettings.IsSuggestionsEnabled);
+ Assert.True(sqlToolsSettings.SqlTools.EnableIntellisense);
+ Assert.True(sqlToolsSettings.SqlTools.IntelliSense.EnableDiagnostics);
+ Assert.True(sqlToolsSettings.SqlTools.IntelliSense.EnableSuggestions);
+ Assert.True(sqlToolsSettings.SqlTools.IntelliSense.EnableQuickInfo);
+ Assert.False(sqlToolsSettings.SqlTools.IntelliSense.LowerCaseSuggestions);
+ }
+
+ ///
+ /// Validate that the IsDiagnositicsEnabled flag behavior
+ ///
+ [Fact]
+ public void ValidateIsDiagnosticsEnabled()
+ {
+ var sqlToolsSettings = new SqlToolsSettings();
+
+ // diagnostics is enabled if IntelliSense and Diagnostics flags are set
+ sqlToolsSettings.SqlTools.EnableIntellisense = true;
+ sqlToolsSettings.SqlTools.IntelliSense.EnableDiagnostics = true;
+ Assert.True(sqlToolsSettings.IsDiagnositicsEnabled);
+
+ // diagnostics is disabled if either IntelliSense and Diagnostics flags is not set
+ sqlToolsSettings.SqlTools.EnableIntellisense = false;
+ sqlToolsSettings.SqlTools.IntelliSense.EnableDiagnostics = true;
+ Assert.False(sqlToolsSettings.IsDiagnositicsEnabled);
+
+ sqlToolsSettings.SqlTools.EnableIntellisense = true;
+ sqlToolsSettings.SqlTools.IntelliSense.EnableDiagnostics = false;
+ Assert.False(sqlToolsSettings.IsDiagnositicsEnabled);
+ }
+
+ ///
+ /// Validate that the IsSuggestionsEnabled flag behavior
+ ///
+ [Fact]
+ public void ValidateIsSuggestionsEnabled()
+ {
+ var sqlToolsSettings = new SqlToolsSettings();
+
+ // suggestions is enabled if IntelliSense and Suggestions flags are set
+ sqlToolsSettings.SqlTools.EnableIntellisense = true;
+ sqlToolsSettings.SqlTools.IntelliSense.EnableSuggestions = true;
+ Assert.True(sqlToolsSettings.IsSuggestionsEnabled);
+
+ // suggestions is disabled if either IntelliSense and Suggestions flags is not set
+ sqlToolsSettings.SqlTools.EnableIntellisense = false;
+ sqlToolsSettings.SqlTools.IntelliSense.EnableSuggestions = true;
+ Assert.False(sqlToolsSettings.IsSuggestionsEnabled);
+
+ sqlToolsSettings.SqlTools.EnableIntellisense = true;
+ sqlToolsSettings.SqlTools.IntelliSense.EnableSuggestions = false;
+ Assert.False(sqlToolsSettings.IsSuggestionsEnabled);
+ }
+
+ ///
+ /// Validate that the IsQuickInfoEnabled flag behavior
+ ///
+ [Fact]
+ public void ValidateIsQuickInfoEnabled()
+ {
+ var sqlToolsSettings = new SqlToolsSettings();
+
+ // quick info is enabled if IntelliSense and quick info flags are set
+ sqlToolsSettings.SqlTools.EnableIntellisense = true;
+ sqlToolsSettings.SqlTools.IntelliSense.EnableQuickInfo = true;
+ Assert.True(sqlToolsSettings.IsQuickInfoEnabled);
+
+ // quick info is disabled if either IntelliSense and quick info flags is not set
+ sqlToolsSettings.SqlTools.EnableIntellisense = false;
+ sqlToolsSettings.SqlTools.IntelliSense.EnableQuickInfo = true;
+ Assert.False(sqlToolsSettings.IsQuickInfoEnabled);
+
+ sqlToolsSettings.SqlTools.EnableIntellisense = true;
+ sqlToolsSettings.SqlTools.IntelliSense.EnableQuickInfo = false;
+ Assert.False(sqlToolsSettings.IsQuickInfoEnabled);
+ }
+ }
+}
diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/Utility/TestObjects.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/Utility/TestObjects.cs
index 82ffffd0..67e48786 100644
--- a/test/Microsoft.SqlTools.ServiceLayer.Test/Utility/TestObjects.cs
+++ b/test/Microsoft.SqlTools.ServiceLayer.Test/Utility/TestObjects.cs
@@ -21,6 +21,8 @@ namespace Microsoft.SqlTools.Test.Utility
///
public class TestObjects
{
+ public const string ScriptUri = "file://some/file.sql";
+
///
/// Creates a test connection service
///
@@ -35,11 +37,22 @@ namespace Microsoft.SqlTools.Test.Utility
#endif
}
+ ///
+ /// Creates a test connection info instance.
+ ///
+ public static ConnectionInfo GetTestConnectionInfo()
+ {
+ return new ConnectionInfo(
+ GetTestSqlConnectionFactory(),
+ ScriptUri,
+ GetTestConnectionDetails());
+ }
+
public static ConnectParams GetTestConnectionParams()
{
return new ConnectParams()
{
- OwnerUri = "file://some/file.sql",
+ OwnerUri = ScriptUri,
Connection = GetTestConnectionDetails()
};
}
diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/Utility/TestUtils.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/Utility/TestUtils.cs
index 9a5f8ce1..b2d52180 100644
--- a/test/Microsoft.SqlTools.ServiceLayer.Test/Utility/TestUtils.cs
+++ b/test/Microsoft.SqlTools.ServiceLayer.Test/Utility/TestUtils.cs
@@ -1,5 +1,6 @@
using System;
using System.Runtime.InteropServices;
+using System.Threading;
namespace Microsoft.SqlTools.ServiceLayer.Test.Utility
{
@@ -21,5 +22,23 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.Utility
test();
}
}
+
+ ///
+ /// Wait for a condition to be true for a limited amount of time.
+ ///
+ /// Function that returns a boolean on a condition
+ /// Number of milliseconds to wait between test intervals.
+ /// Number of test intervals to perform before giving up.
+ /// True if the condition was met before the test interval limit.
+ public static bool WaitFor(Func condition, int intervalMilliseconds = 10, int intervalCount = 200)
+ {
+ int count = 0;
+ while (count++ < intervalCount && !condition.Invoke())
+ {
+ Thread.Sleep(intervalMilliseconds);
+ }
+
+ return (count < intervalCount);
+ }
}
}
diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/project.json b/test/Microsoft.SqlTools.ServiceLayer.Test/project.json
index 4e5318f5..ae6e3ea6 100644
--- a/test/Microsoft.SqlTools.ServiceLayer.Test/project.json
+++ b/test/Microsoft.SqlTools.ServiceLayer.Test/project.json
@@ -9,7 +9,7 @@
"System.Runtime.Serialization.Primitives": "4.1.1",
"System.Data.Common": "4.1.0",
"System.Data.SqlClient": "4.1.0",
- "Microsoft.SqlServer.Smo": "140.1.5",
+ "Microsoft.SqlServer.Smo": "140.1.8",
"System.Security.SecureString": "4.0.0",
"System.Collections.Specialized": "4.0.1",
"System.ComponentModel.TypeConverter": "4.1.0",