Add IntelliSense binding queue (#73)

* Initial code for binding queue

* Fix-up some of the timeout wait code

* Add some initial test code

* Add missing test file

* Update the binding queue tests

* Add more test coverage and refactor a bit.  Disable reliabile connection until we can fix it..it's holding an open data reader connection.

* A few more test updates

* Initial integrate queue with language service.

* Hook up the connected binding queue into al binding calls.

* Cleanup comments and remove dead code

* More missing comments

* Fix build break.  Reenable ReliabileConnection.

* Revert all changes to SqlConnectionFactory

* Resolve merge conflicts

* Cleanup some more of the timeouts and sync code

* Address code review feedback

* Address more code review feedback
This commit is contained in:
Karl Burtram
2016-10-04 14:55:59 -07:00
committed by GitHub
parent 1b8e9c1e86
commit 62525b9c98
18 changed files with 1409 additions and 360 deletions

2
.gitignore vendored
View File

@@ -14,6 +14,7 @@ project.lock.json
*.userosscache
*.sln.docstates
*.exe
scratch.txt
# mergetool conflict files
*.orig
@@ -55,6 +56,7 @@ cross/rootfs/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
test*json
#NUNIT
*.VisualState.xml

View File

@@ -47,6 +47,18 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection
private Dictionary<string, ConnectionInfo> ownerToConnectionMap = new Dictionary<string, ConnectionInfo>();
/// <summary>
/// Map from script URIs to ConnectionInfo objects
/// This is internal for testing access only
/// </summary>
internal Dictionary<string, ConnectionInfo> OwnerToConnectionMap
{
get
{
return this.ownerToConnectionMap;
}
}
/// <summary>
/// Service host object for sending/receiving requests/events.
/// Internal for testing purposes.

View File

@@ -4,6 +4,7 @@
//
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.SqlServer.Management.SqlParser.Binder;
using Microsoft.SqlServer.Management.SqlParser.Intellisense;
using Microsoft.SqlServer.Management.SqlParser.Parser;
@@ -21,6 +22,8 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
/// </summary>
public static class AutoCompleteHelper
{
private static WorkspaceService<SqlToolsSettings> workspaceServiceInstance;
private static readonly string[] DefaultCompletionText = new string[]
{
"absolute",
@@ -421,6 +424,26 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
"zone"
};
/// <summary>
/// Gets or sets the current workspace service instance
/// Setter for internal testing purposes only
/// </summary>
internal static WorkspaceService<SqlToolsSettings> WorkspaceServiceInstance
{
get
{
if (AutoCompleteHelper.workspaceServiceInstance == null)
{
AutoCompleteHelper.workspaceServiceInstance = WorkspaceService<SqlToolsSettings>.Instance;
}
return AutoCompleteHelper.workspaceServiceInstance;
}
set
{
AutoCompleteHelper.workspaceServiceInstance = value;
}
}
/// <summary>
/// Get the default completion list from hard-coded list
/// </summary>
@@ -538,11 +561,14 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
/// </summary>
/// <param name="info"></param>
/// <param name="scriptInfo"></param>
internal static void PrepopulateCommonMetadata(ConnectionInfo info, ScriptParseInfo scriptInfo)
internal static void PrepopulateCommonMetadata(
ConnectionInfo info,
ScriptParseInfo scriptInfo,
ConnectedBindingQueue bindingQueue)
{
if (scriptInfo.IsConnected)
{
var scriptFile = WorkspaceService<SqlToolsSettings>.Instance.Workspace.GetFile(info.OwnerUri);
var scriptFile = AutoCompleteHelper.WorkspaceServiceInstance.Workspace.GetFile(info.OwnerUri);
LanguageService.Instance.ParseAndBind(scriptFile, info);
if (scriptInfo.BuildingMetadataEvent.WaitOne(LanguageService.OnConnectionWaitTimeout))
@@ -551,14 +577,18 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
{
scriptInfo.BuildingMetadataEvent.Reset();
QueueItem queueItem = bindingQueue.QueueBindingOperation(
key: scriptInfo.ConnectionKey,
bindOperation: (bindingContext, cancelToken) =>
{
// parse a simple statement that returns common metadata
ParseResult parseResult = Parser.Parse(
"select ",
scriptInfo.ParseOptions);
bindingContext.ParseOptions);
List<ParseResult> parseResults = new List<ParseResult>();
parseResults.Add(parseResult);
scriptInfo.Binder.Bind(
bindingContext.Binder.Bind(
parseResults,
info.ConnectionDetails.DatabaseName,
BindMode.Batch);
@@ -566,18 +596,18 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
// get the completion list from SQL Parser
var suggestions = Resolver.FindCompletions(
parseResult, 1, 8,
scriptInfo.MetadataDisplayInfoProvider);
bindingContext.MetadataDisplayInfoProvider);
// this forces lazy evaluation of the suggestion metadata
AutoCompleteHelper.ConvertDeclarationsToCompletionItems(suggestions, 1, 8, 8);
parseResult = Parser.Parse(
"exec ",
scriptInfo.ParseOptions);
bindingContext.ParseOptions);
parseResults = new List<ParseResult>();
parseResults.Add(parseResult);
scriptInfo.Binder.Bind(
bindingContext.Binder.Bind(
parseResults,
info.ConnectionDetails.DatabaseName,
BindMode.Batch);
@@ -585,10 +615,14 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
// get the completion list from SQL Parser
suggestions = Resolver.FindCompletions(
parseResult, 1, 6,
scriptInfo.MetadataDisplayInfoProvider);
bindingContext.MetadataDisplayInfoProvider);
// this forces lazy evaluation of the suggestion metadata
AutoCompleteHelper.ConvertDeclarationsToCompletionItems(suggestions, 1, 6, 6);
return Task.FromResult(null as object);
});
queueItem.ItemProcessed.WaitOne();
}
catch
{
@@ -600,5 +634,53 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
}
}
}
/// <summary>
/// Converts a SQL Parser QuickInfo object into a VS Code Hover object
/// </summary>
/// <param name="quickInfo"></param>
/// <param name="row"></param>
/// <param name="startColumn"></param>
/// <param name="endColumn"></param>
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;
}
}
}
}

View File

@@ -0,0 +1,249 @@
//
// 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;
namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
{
/// <summary>
/// Main class for the Binding Queue
/// </summary>
public class BindingQueue<T> where T : IBindingContext, new()
{
private CancellationTokenSource processQueueCancelToken = new CancellationTokenSource();
private ManualResetEvent itemQueuedEvent = new ManualResetEvent(initialState: false);
private object bindingQueueLock = new object();
private LinkedList<QueueItem> bindingQueue = new LinkedList<QueueItem>();
private object bindingContextLock = new object();
private Task queueProcessorTask;
/// <summary>
/// Map from context keys to binding context instances
/// Internal for testing purposes only
/// </summary>
internal Dictionary<string, IBindingContext> BindingContextMap { get; set; }
/// <summary>
/// Constructor for a binding queue instance
/// </summary>
public BindingQueue()
{
this.BindingContextMap = new Dictionary<string, IBindingContext>();
this.queueProcessorTask = StartQueueProcessor();
}
/// <summary>
/// Stops the binding queue by sending cancellation request
/// </summary>
/// <param name="timeout"></param>
public bool StopQueueProcessor(int timeout)
{
this.processQueueCancelToken.Cancel();
return this.queueProcessorTask.Wait(timeout);
}
/// <summary>
/// Queue a binding request item
/// </summary>
public QueueItem QueueBindingOperation(
string key,
Func<IBindingContext, CancellationToken, Task<object>> bindOperation,
Func<IBindingContext, Task<object>> 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;
}
/// <summary>
/// Gets or creates a binding context for the provided context key
/// </summary>
/// <param name="key"></param>
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;
}
}
}
/// <summary>
/// Gets the next pending queue item
/// </summary>
private QueueItem GetNextQueueItem()
{
lock (this.bindingQueueLock)
{
if (this.bindingQueue.Count == 0)
{
return null;
}
QueueItem queueItem = this.bindingQueue.First.Value;
this.bindingQueue.RemoveFirst();
return queueItem;
}
}
/// <summary>
/// Starts the queue processing thread
/// </summary>
private Task StartQueueProcessor()
{
return Task.Factory.StartNew(
ProcessQueue,
this.processQueueCancelToken.Token,
TaskCreationOptions.LongRunning,
TaskScheduler.Default);
}
/// <summary>
/// The core queue processing method
/// </summary>
/// <param name="state"></param>
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;
}
// 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.ResultsTask = Task.Run(() =>
{
var timeoutTask = queueItem.TimeoutOperation(bindingContext);
timeoutTask.ContinueWith((obj) => queueItem.ItemProcessed.Set());
return timeoutTask.Result;
});
continue;
}
// execute the binding operation
CancellationTokenSource cancelToken = new CancellationTokenSource();
queueItem.ResultsTask = queueItem.BindOperation(
bindingContext,
cancelToken.Token);
// set notification events once the binding operation task completes
queueItem.ResultsTask.ContinueWith((obj) =>
{
queueItem.ItemProcessed.Set();
bindingContext.BindingLocked.Set();
});
// check if the binding tasks completed within the binding timeout
if (!queueItem.ResultsTask.Wait(bindTimeout))
{
// if the task didn't complete then call the timeout callback
if (queueItem.TimeoutOperation != null)
{
cancelToken.Cancel();
queueItem.ResultsTask = queueItem.TimeoutOperation(bindingContext);
queueItem.ResultsTask.ContinueWith((obj) => 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();
}
}
}
}
}

View File

@@ -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
{
/// <summary>
/// Class for the binding context for connected sessions
/// </summary>
public class ConnectedBindingContext : IBindingContext
{
private ParseOptions parseOptions;
private ServerConnection serverConnection;
/// <summary>
/// Connected binding context constructor
/// </summary>
public ConnectedBindingContext()
{
this.BindingLocked = new ManualResetEvent(initialState: true);
this.BindingTimeout = ConnectedBindingQueue.DefaultBindingTimeout;
this.MetadataDisplayInfoProvider = new MetadataDisplayInfoProvider();
}
/// <summary>
/// Gets or sets a flag indicating if the binder is connected
/// </summary>
public bool IsConnected { get; set; }
/// <summary>
/// Gets or sets the binding server connection
/// </summary>
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;
}
}
/// <summary>
/// Gets or sets the metadata display info provider
/// </summary>
public MetadataDisplayInfoProvider MetadataDisplayInfoProvider { get; set; }
/// <summary>
/// Gets or sets the SMO metadata provider
/// </summary>
public SmoMetadataProvider SmoMetadataProvider { get; set; }
/// <summary>
/// Gets or sets the binder
/// </summary>
public IBinder Binder { get; set; }
/// <summary>
/// Gets or sets an event to signal if a binding operation is in progress
/// </summary>
public ManualResetEvent BindingLocked { get; set; }
/// <summary>
/// Gets or sets the binding operation timeout in milliseconds
/// </summary>
public int BindingTimeout { get; set; }
/// <summary>
/// Gets the Language Service ServerVersion
/// </summary>
public ServerVersion ServerVersion
{
get
{
return this.ServerConnection != null
? this.ServerConnection.ServerVersion
: null;
}
}
/// <summary>
/// Gets the current DataEngineType
/// </summary>
public DatabaseEngineType DatabaseEngineType
{
get
{
return this.ServerConnection != null
? this.ServerConnection.DatabaseEngineType
: DatabaseEngineType.Standalone;
}
}
/// <summary>
/// Gets the current connections TransactSqlVersion
/// </summary>
public TransactSqlVersion TransactSqlVersion
{
get
{
return this.IsConnected
? GetTransactSqlVersion(this.ServerVersion)
: TransactSqlVersion.Current;
}
}
/// <summary>
/// Gets the current DatabaseCompatibilityLevel
/// </summary>
public DatabaseCompatibilityLevel DatabaseCompatibilityLevel
{
get
{
return this.IsConnected
? GetDatabaseCompatibilityLevel(this.ServerVersion)
: DatabaseCompatibilityLevel.Current;
}
}
/// <summary>
/// Gets the current ParseOptions
/// </summary>
public ParseOptions ParseOptions
{
get
{
if (this.parseOptions == null)
{
this.parseOptions = new ParseOptions(
batchSeparator: LanguageService.DefaultBatchSeperator,
isQuotedIdentifierSet: true,
compatibilityLevel: DatabaseCompatibilityLevel,
transactSqlVersion: TransactSqlVersion);
}
return this.parseOptions;
}
}
/// <summary>
/// Gets the database compatibility level from a server version
/// </summary>
/// <param name="serverVersion"></param>
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;
}
}
/// <summary>
/// Gets the transaction sql version from a server version
/// </summary>
/// <param name="serverVersion"></param>
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;
}
}
}
}

View File

@@ -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
{
/// <summary>
/// ConnectedBindingQueue class for processing online binding requests
/// </summary>
public class ConnectedBindingQueue : BindingQueue<ConnectedBindingContext>
{
internal const int DefaultBindingTimeout = 60000;
internal const int DefaultMinimumConnectionTimeout = 30;
/// <summary>
/// Gets the current settings
/// </summary>
internal SqlToolsSettings CurrentSettings
{
get { return WorkspaceService<SqlToolsSettings>.Instance.CurrentSettings; }
}
/// <summary>
/// Generate a unique key based on the ConnectionInfo object
/// </summary>
/// <param name="connInfo"></param>
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"
);
}
/// <summary>
/// Use a ConnectionInfo item to create a connected binding context
/// </summary>
/// <param name="connInfo"></param>
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;
}
}
}

View File

@@ -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
{
/// <summary>
/// The context used for binding requests
/// </summary>
public interface IBindingContext
{
/// <summary>
/// Gets or sets a flag indicating if the context is connected
/// </summary>
bool IsConnected { get; set; }
/// <summary>
/// Gets or sets the binding server connection
/// </summary>
ServerConnection ServerConnection { get; set; }
/// <summary>
/// Gets or sets the metadata display info provider
/// </summary>
MetadataDisplayInfoProvider MetadataDisplayInfoProvider { get; set; }
/// <summary>
/// Gets or sets the SMO metadata provider
/// </summary>
SmoMetadataProvider SmoMetadataProvider { get; set; }
/// <summary>
/// Gets or sets the binder
/// </summary>
IBinder Binder { get; set; }
/// <summary>
/// Gets or sets an event to signal if a binding operation is in progress
/// </summary>
ManualResetEvent BindingLocked { get; set; }
/// <summary>
/// Gets or sets the binding operation timeout in milliseconds
/// </summary>
int BindingTimeout { get; set; }
/// <summary>
/// Gets or sets the current connection parse options
/// </summary>
ParseOptions ParseOptions { get; }
/// <summary>
/// Gets or sets the current connection server version
/// </summary>
ServerVersion ServerVersion { get; }
/// <summary>
/// Gets or sets the database engine type
/// </summary>
DatabaseEngineType DatabaseEngineType { get; }
/// <summary>
/// Gets or sets the T-SQL version
/// </summary>
TransactSqlVersion TransactSqlVersion { get; }
/// <summary>
/// Gets or sets the database compatibility level
/// </summary>
DatabaseCompatibilityLevel DatabaseCompatibilityLevel { get; }
}
}

View File

@@ -11,12 +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.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;
@@ -38,22 +37,50 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
internal const int DiagnosticParseDelay = 750;
internal const int HoverTimeout = 3000;
internal const int FindCompletionsTimeout = 3000;
internal const int FindCompletionStartTimeout = 50;
internal const int OnConnectionWaitTimeout = 300000;
private static ConnectionService connectionService = null;
private static WorkspaceService<SqlToolsSettings> workspaceServiceInstance;
private object parseMapLock = new object();
private ScriptParseInfo currentCompletionParseInfo;
private ConnectionService connectionService = null;
private ConnectedBindingQueue bindingQueue = new ConnectedBindingQueue();
private ParseOptions defaultParseOptions = new ParseOptions(
batchSeparator: LanguageService.DefaultBatchSeperator,
isQuotedIdentifierSet: true,
compatibilityLevel: DatabaseCompatibilityLevel.Current,
transactSqlVersion: TransactSqlVersion.Current);
/// <summary>
/// Gets or sets the binding queue instance
/// Internal for testing purposes only
/// </summary>
internal ConnectedBindingQueue BindingQueue
{
get
{
return this.bindingQueue;
}
set
{
this.bindingQueue = value;
}
}
/// <summary>
/// Internal for testing purposes only
/// </summary>
internal ConnectionService ConnectionServiceInstance
internal static ConnectionService ConnectionServiceInstance
{
get
{
@@ -96,6 +123,14 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
get { return instance.Value; }
}
private ParseOptions DefaultParseOptions
{
get
{
return this.defaultParseOptions;
}
}
/// <summary>
/// Default, parameterless constructor.
/// </summary>
@@ -109,14 +144,40 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
private static CancellationTokenSource ExistingRequestCancellation { get; set; }
/// <summary>
/// Gets the current settings
/// </summary>
internal SqlToolsSettings CurrentSettings
{
get { return WorkspaceService<SqlToolsSettings>.Instance.CurrentSettings; }
}
/// <summary>
/// Gets or sets the current workspace service instance
/// Setter for internal testing purposes only
/// </summary>
internal static WorkspaceService<SqlToolsSettings> WorkspaceServiceInstance
{
get
{
if (LanguageService.workspaceServiceInstance == null)
{
LanguageService.workspaceServiceInstance = WorkspaceService<SqlToolsSettings>.Instance;
}
return LanguageService.workspaceServiceInstance;
}
set
{
LanguageService.workspaceServiceInstance = value;
}
}
/// <summary>
/// Gets the current workspace instance
/// </summary>
internal Workspace.Workspace CurrentWorkspace
{
get { return WorkspaceService<SqlToolsSettings>.Instance.Workspace; }
get { return LanguageService.WorkspaceServiceInstance.Workspace; }
}
/// <summary>
@@ -181,7 +242,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
/// <param name="textDocumentPosition"></param>
/// <param name="requestContext"></param>
/// <returns></returns>
private static async Task HandleCompletionRequest(
internal static async Task HandleCompletionRequest(
TextDocumentPosition textDocumentPosition,
RequestContext<CompletionItem[]> requestContext)
{
@@ -193,11 +254,11 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
else
{
// get the current list of completion items and return to client
var scriptFile = WorkspaceService<SqlToolsSettings>.Instance.Workspace.GetFile(
var scriptFile = LanguageService.WorkspaceServiceInstance.Workspace.GetFile(
textDocumentPosition.TextDocument.Uri);
ConnectionInfo connInfo;
ConnectionService.Instance.TryFindConnection(
LanguageService.ConnectionServiceInstance.TryFindConnection(
scriptFile.ClientFilePath,
out connInfo);
@@ -339,12 +400,6 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
// update the current settings to reflect any changes
CurrentSettings.Update(newSettings);
// update the script parse info objects if the settings have changed
foreach (var scriptInfo in this.ScriptParseInfoMap.Values)
{
scriptInfo.OnSettingsChanged(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)
@@ -391,7 +446,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
/// </summary>
/// <param name="filePath"></param>
/// <param name="sqlText"></param>
/// <returns></returns>
/// <returns>The ParseResult instance returned from SQL Parser</returns>
public ParseResult ParseAndBind(ScriptFile scriptFile, ConnectionInfo connInfo)
{
// get or create the current parse info object
@@ -403,21 +458,34 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
{
parseInfo.BuildingMetadataEvent.Reset();
if (connInfo == null || !parseInfo.IsConnected)
{
// parse current SQL file contents to retrieve a list of errors
ParseResult parseResult = Parser.IncrementalParse(
scriptFile.Contents,
parseInfo.ParseResult,
parseInfo.ParseOptions);
this.DefaultParseOptions);
parseInfo.ParseResult = parseResult;
if (connInfo != null && parseInfo.IsConnected)
}
else
{
QueueItem queueItem = this.BindingQueue.QueueBindingOperation(
key: parseInfo.ConnectionKey,
bindOperation: (bindingContext, cancelToken) =>
{
try
{
ParseResult parseResult = Parser.IncrementalParse(
scriptFile.Contents,
parseInfo.ParseResult,
bindingContext.ParseOptions);
parseInfo.ParseResult = parseResult;
List<ParseResult> parseResults = new List<ParseResult>();
parseResults.Add(parseResult);
parseInfo.Binder.Bind(
bindingContext.Binder.Bind(
parseResults,
connInfo.ConnectionDetails.DatabaseName,
BindMode.Batch);
@@ -430,7 +498,22 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
{
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 Task.FromResult(null as object);
});
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
{
@@ -455,20 +538,13 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
try
{
scriptInfo.BuildingMetadataEvent.Reset();
ReliableSqlConnection sqlConn = info.SqlConnection as ReliableSqlConnection;
if (sqlConn != null)
{
ServerConnection serverConn = new ServerConnection(sqlConn.GetUnderlyingConnection());
scriptInfo.MetadataProvider = SmoMetadataProvider.CreateConnectedProvider(serverConn);
scriptInfo.Binder = BinderProvider.CreateBinder(scriptInfo.MetadataProvider);
scriptInfo.ServerConnection = serverConn;
scriptInfo.ConnectionKey = this.BindingQueue.AddConnectionContext(info);
scriptInfo.IsConnected = true;
}
}
catch (Exception)
catch (Exception ex)
{
Logger.Write(LogLevel.Error, "Unknown error in OnConnection " + ex.ToString());
scriptInfo.IsConnected = false;
}
finally
@@ -479,8 +555,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
}
}
// populate SMO metadata provider with most common info
AutoCompleteHelper.PrepopulateCommonMetadata(info, scriptInfo);
AutoCompleteHelper.PrepopulateCommonMetadata(info, scriptInfo, this.BindingQueue);
});
}
@@ -555,42 +630,31 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
{
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,
scriptParseInfo.MetadataDisplayInfoProvider);
bindingContext.MetadataDisplayInfoProvider);
// 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 Task.FromResult(
AutoCompleteHelper.ConvertQuickInfoToHover(
quickInfo,
startLine,
startColumn,
endColumn
) as object);
});
return new Hover()
{
Contents = markedStrings,
Range = new Range
{
Start = new Position
{
Line = startLine,
Character = startColumn
},
End = new Position
{
Line = startLine,
Character = endColumn
}
}
};
}
queueItem.ItemProcessed.WaitOne();
return queueItem.GetResultAsT<Hover>();
}
finally
{
@@ -647,7 +711,12 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
&& scriptParseInfo.BuildingMetadataEvent.WaitOne(LanguageService.FindCompletionStartTimeout))
{
scriptParseInfo.BuildingMetadataEvent.Reset();
Task<CompletionItem[]> findCompletionsTask = Task.Run(() => {
QueueItem queueItem = this.BindingQueue.QueueBindingOperation(
key: scriptParseInfo.ConnectionKey,
bindOperation: (bindingContext, cancelToken) =>
{
CompletionItem[] completions = null;
try
{
// get the completion list from SQL Parser
@@ -655,13 +724,13 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
scriptParseInfo.ParseResult,
textDocumentPosition.Position.Line + 1,
textDocumentPosition.Position.Character + 1,
scriptParseInfo.MetadataDisplayInfoProvider);
bindingContext.MetadataDisplayInfoProvider);
// 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(
completions = AutoCompleteHelper.ConvertDeclarationsToCompletionItems(
scriptParseInfo.CurrentSuggestions,
startLine,
startColumn,
@@ -671,14 +740,20 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
{
scriptParseInfo.BuildingMetadataEvent.Set();
}
return Task.FromResult(completions as object);
},
timeoutOperation: (bindingContext) =>
{
return Task.FromResult(
AutoCompleteHelper.GetDefaultCompletionItems(startLine, startColumn, endColumn, useLowerCaseSuggestions) as object);
});
findCompletionsTask.Wait(LanguageService.FindCompletionsTimeout);
if (findCompletionsTask.IsCompleted
&& findCompletionsTask.Result != null
&& findCompletionsTask.Result.Length > 0)
queueItem.ItemProcessed.WaitOne();
var completionItems = queueItem.GetResultAsT<CompletionItem[]>();
if (completionItems != null && completionItems.Length > 0)
{
return findCompletionsTask.Result;
return completionItems;
}
}
@@ -829,7 +904,12 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
#endregion
private void AddOrUpdateScriptParseInfo(string uri, ScriptParseInfo scriptInfo)
/// <summary>
/// Adds a new or updates an existing script parse info instance in local cache
/// </summary>
/// <param name="uri"></param>
/// <param name="scriptInfo"></param>
internal void AddOrUpdateScriptParseInfo(string uri, ScriptParseInfo scriptInfo)
{
lock (this.parseMapLock)
{
@@ -845,7 +925,13 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
}
}
private ScriptParseInfo GetScriptParseInfo(string uri, bool createIfNotExists = false)
/// <summary>
/// Gets a script parse info object for a file from the local cache
/// Internal for testing purposes only
/// </summary>
/// <param name="uri"></param>
/// <param name="createIfNotExists">Creates a new instance if one doesn't exist</param>
internal ScriptParseInfo GetScriptParseInfo(string uri, bool createIfNotExists = false)
{
lock (this.parseMapLock)
{
@@ -857,7 +943,6 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
{
// create a new script parse info object and initialize with the current settings
ScriptParseInfo scriptInfo = new ScriptParseInfo();
scriptInfo.OnSettingsChanged(this.CurrentSettings);
this.ScriptParseInfoMap.Add(uri, scriptInfo);
return scriptInfo;
}
@@ -874,9 +959,6 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
{
if (this.ScriptParseInfoMap.ContainsKey(uri))
{
var scriptInfo = this.ScriptParseInfoMap[uri];
scriptInfo.ServerConnection.Disconnect();
scriptInfo.ServerConnection = null;
return this.ScriptParseInfoMap.Remove(uri);
}
else

View File

@@ -0,0 +1,67 @@
//
// 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
{
/// <summary>
/// Class that stores the state of a binding queue request item
/// </summary>
public class QueueItem
{
/// <summary>
/// QueueItem constructor
/// </summary>
public QueueItem()
{
this.ItemProcessed = new ManualResetEvent(initialState: false);
}
/// <summary>
/// Gets or sets the queue item key
/// </summary>
public string Key { get; set; }
/// <summary>
/// Gets or sets the bind operation callback method
/// </summary>
public Func<IBindingContext, CancellationToken, Task<object>> BindOperation { get; set; }
/// <summary>
/// Gets or sets the timeout operation to call if the bind operation doesn't finish within timeout period
/// </summary>
public Func<IBindingContext, Task<object>> TimeoutOperation { get; set; }
/// <summary>
/// Gets or sets an event to signal when this queue item has been processed
/// </summary>
public ManualResetEvent ItemProcessed { get; set; }
/// <summary>
/// Gets or sets the task that was used to execute this queue item.
/// This allows the queuer to retrieve the execution result.
/// </summary>
public Task<object> ResultsTask { get; set; }
/// <summary>
/// Gets or sets the binding operation timeout in milliseconds
/// </summary>
public int? BindingTimeout { get; set; }
/// <summary>
/// Converts the result of the execution task to type T
/// </summary>
public T GetResultAsT<T>() where T : class
{
var task = this.ResultsTask;
return (task != null && task.IsCompleted && task.Result != null)
? task.Result as T
: null;
}
}
}

View File

@@ -3,17 +3,10 @@
// 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;
using Microsoft.SqlTools.ServiceLayer.SqlContext;
namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
{
@@ -24,16 +17,6 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
{
private ManualResetEvent buildingMetadataEvent = new ManualResetEvent(initialState: true);
private ParseOptions parseOptions = new ParseOptions();
private ServerConnection serverConnection;
private Lazy<MetadataDisplayInfoProvider> metadataDisplayInfoProvider = new Lazy<MetadataDisplayInfoProvider>(() =>
{
var infoProvider = new MetadataDisplayInfoProvider();
return infoProvider;
});
/// <summary>
/// Event which tells if MetadataProvider is built fully or not
/// </summary>
@@ -48,181 +31,18 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
public bool IsConnected { get; set; }
/// <summary>
/// Gets or sets the LanguageService SMO ServerConnection
/// Gets or sets the binding queue connection context key
/// </summary>
public ServerConnection ServerConnection
{
get
{
return this.serverConnection;
}
set
{
this.serverConnection = value;
this.parseOptions = new ParseOptions(
batchSeparator: LanguageService.DefaultBatchSeperator,
isQuotedIdentifierSet: true,
compatibilityLevel: DatabaseCompatibilityLevel,
transactSqlVersion: TransactSqlVersion);
}
}
/// <summary>
/// Gets the Language Service ServerVersion
/// </summary>
public ServerVersion ServerVersion
{
get
{
return this.ServerConnection != null
? this.ServerConnection.ServerVersion
: null;
}
}
/// <summary>
/// Gets the current DataEngineType
/// </summary>
public DatabaseEngineType DatabaseEngineType
{
get
{
return this.ServerConnection != null
? this.ServerConnection.DatabaseEngineType
: DatabaseEngineType.Standalone;
}
}
/// <summary>
/// Gets the current connections TransactSqlVersion
/// </summary>
public TransactSqlVersion TransactSqlVersion
{
get
{
return this.IsConnected
? GetTransactSqlVersion(this.ServerVersion)
: TransactSqlVersion.Current;
}
}
/// <summary>
/// Gets the current DatabaseCompatibilityLevel
/// </summary>
public DatabaseCompatibilityLevel DatabaseCompatibilityLevel
{
get
{
return this.IsConnected
? GetDatabaseCompatibilityLevel(this.ServerVersion)
: DatabaseCompatibilityLevel.Current;
}
}
/// <summary>
/// Gets the current ParseOptions
/// </summary>
public ParseOptions ParseOptions
{
get
{
return this.parseOptions;
}
}
/// <summary>
/// Gets or sets the SMO binder for schema-aware intellisense
/// </summary>
public IBinder Binder { get; set; }
public string ConnectionKey { get; set; }
/// <summary>
/// Gets or sets the previous SQL parse result
/// </summary>
public ParseResult ParseResult { get; set; }
/// <summary>
/// Gets or set the SMO metadata provider that's bound to the current connection
/// </summary>
public SmoMetadataProvider MetadataProvider { get; set; }
/// <summary>
/// Gets or sets the SMO metadata display info provider
/// </summary>
public MetadataDisplayInfoProvider MetadataDisplayInfoProvider
{
get
{
return this.metadataDisplayInfoProvider.Value;
}
}
/// <summary>
/// Gets or sets the current autocomplete suggestion list
/// </summary>
public IEnumerable<Declaration> CurrentSuggestions { get; set; }
/// <summary>
/// Update parse settings if the current configuration has changed
/// </summary>
/// <param name="settings"></param>
public void OnSettingsChanged(SqlToolsSettings settings)
{
this.MetadataDisplayInfoProvider.BuiltInCasing =
settings.SqlTools.IntelliSense.LowerCaseSuggestions.Value
? CasingStyle.Lowercase
: CasingStyle.Uppercase;
}
/// <summary>
/// Gets the database compatibility level from a server version
/// </summary>
/// <param name="serverVersion"></param>
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;
}
}
/// <summary>
/// Gets the transaction sql version from a server version
/// </summary>
/// <param name="serverVersion"></param>
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;
}
}
}
}

View File

@@ -36,8 +36,9 @@ namespace Microsoft.SqlTools.ServiceLayer.Workspace.Contracts
/// <summary>
/// Gets or sets the path which the editor client uses to identify this file.
/// Setter for testing purposes only
/// virtual to allow mocking.
/// </summary>
public string ClientFilePath { get; internal set; }
public virtual string ClientFilePath { get; internal set; }
/// <summary>
/// Gets or sets a boolean that determines whether

View File

@@ -9,7 +9,7 @@
"Newtonsoft.Json": "9.0.1",
"System.Data.Common": "4.1.0",
"System.Data.SqlClient": "4.1.0",
"Microsoft.SqlServer.Smo": "140.1.7",
"Microsoft.SqlServer.Smo": "140.1.8",
"System.Security.SecureString": "4.0.0",
"System.Collections.Specialized": "4.0.1",
"System.ComponentModel.TypeConverter": "4.1.0",

View File

@@ -0,0 +1,124 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.SqlServer.Management.SqlParser.Binder;
using Microsoft.SqlServer.Management.SqlParser.MetadataProvider;
using Microsoft.SqlServer.Management.SqlParser.Parser;
using Microsoft.SqlTools.ServiceLayer.Connection;
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol;
using Microsoft.SqlTools.ServiceLayer.LanguageServices;
using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts;
using Microsoft.SqlTools.ServiceLayer.SqlContext;
using Microsoft.SqlTools.ServiceLayer.Test.QueryExecution;
using Microsoft.SqlTools.ServiceLayer.Workspace;
using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts;
using Microsoft.SqlTools.Test.Utility;
using Moq;
using Xunit;
namespace Microsoft.SqlTools.ServiceLayer.Test.LanguageServices
{
/// <summary>
/// Tests for the language service autocomplete component
/// </summary>
public class AutocompleteTests
{
private const int TaskTimeout = 60000;
private readonly string testScriptUri = TestObjects.ScriptUri;
private readonly string testConnectionKey = "testdbcontextkey";
private Mock<ConnectedBindingQueue> bindingQueue;
private Mock<WorkspaceService<SqlToolsSettings>> workspaceService;
private Mock<RequestContext<CompletionItem[]>> requestContext;
private Mock<IBinder> binder;
private TextDocumentPosition textDocument;
private void InitializeTestObjects()
{
// initial cursor position in the script file
textDocument = new TextDocumentPosition
{
TextDocument = new TextDocumentIdentifier {Uri = this.testScriptUri},
Position = new Position
{
Line = 0,
Character = 0
}
};
// default settings are stored in the workspace service
WorkspaceService<SqlToolsSettings>.Instance.CurrentSettings = new SqlToolsSettings();
// set up file for returning the query
var fileMock = new Mock<ScriptFile>();
fileMock.SetupGet(file => file.Contents).Returns(Common.StandardQuery);
fileMock.SetupGet(file => file.ClientFilePath).Returns(this.testScriptUri);
// set up workspace mock
workspaceService = new Mock<WorkspaceService<SqlToolsSettings>>();
workspaceService.Setup(service => service.Workspace.GetFile(It.IsAny<string>()))
.Returns(fileMock.Object);
// setup binding queue mock
bindingQueue = new Mock<ConnectedBindingQueue>();
bindingQueue.Setup(q => q.AddConnectionContext(It.IsAny<ConnectionInfo>()))
.Returns(this.testConnectionKey);
// inject mock instances into the Language Service
LanguageService.WorkspaceServiceInstance = workspaceService.Object;
LanguageService.ConnectionServiceInstance = TestObjects.GetTestConnectionService();
ConnectionInfo connectionInfo = TestObjects.GetTestConnectionInfo();
LanguageService.ConnectionServiceInstance.OwnerToConnectionMap.Add(this.testScriptUri, connectionInfo);
LanguageService.Instance.BindingQueue = bindingQueue.Object;
// setup the mock for SendResult
requestContext = new Mock<RequestContext<CompletionItem[]>>();
requestContext.Setup(rc => rc.SendResult(It.IsAny<CompletionItem[]>()))
.Returns(Task.FromResult(0));
// setup the IBinder mock
binder = new Mock<IBinder>();
binder.Setup(b => b.Bind(
It.IsAny<IEnumerable<ParseResult>>(),
It.IsAny<string>(),
It.IsAny<BindMode>()));
var testScriptParseInfo = new ScriptParseInfo();
LanguageService.Instance.AddOrUpdateScriptParseInfo(this.testScriptUri, testScriptParseInfo);
testScriptParseInfo.IsConnected = true;
testScriptParseInfo.ConnectionKey = LanguageService.Instance.BindingQueue.AddConnectionContext(connectionInfo);
// setup the binding context object
ConnectedBindingContext bindingContext = new ConnectedBindingContext();
bindingContext.Binder = binder.Object;
bindingContext.MetadataDisplayInfoProvider = new MetadataDisplayInfoProvider();
LanguageService.Instance.BindingQueue.BindingContextMap.Add(testScriptParseInfo.ConnectionKey, bindingContext);
}
/// <summary>
/// Tests the primary completion list event handler
/// </summary>
[Fact]
public void GetCompletionsHandlerTest()
{
InitializeTestObjects();
// request the completion list
Task handleCompletion = LanguageService.HandleCompletionRequest(textDocument, requestContext.Object);
handleCompletion.Wait(TaskTimeout);
// verify that send result was called with a completion array
requestContext.Verify(m => m.SendResult(It.IsAny<CompletionItem[]>()), Times.Once());
}
}
}

View File

@@ -0,0 +1,202 @@
//
// 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 System.Threading.Tasks;
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;
using Microsoft.SqlTools.ServiceLayer.LanguageServices;
using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts;
using Xunit;
namespace Microsoft.SqlTools.ServiceLayer.Test.LanguageServices
{
/// <summary>
/// Test class for the test binding context
/// </summary>
public class TestBindingContext : IBindingContext
{
public TestBindingContext()
{
this.BindingLocked = new ManualResetEvent(initialState: true);
this.BindingTimeout = 3000;
}
public bool IsConnected { get; set; }
public ServerConnection ServerConnection { get; set; }
public MetadataDisplayInfoProvider MetadataDisplayInfoProvider { get; set; }
public SmoMetadataProvider SmoMetadataProvider { get; set; }
public IBinder Binder { get; set; }
public ManualResetEvent BindingLocked { get; set; }
public int BindingTimeout { get; set; }
public ParseOptions ParseOptions { get; }
public ServerVersion ServerVersion { get; }
public DatabaseEngineType DatabaseEngineType { get; }
public TransactSqlVersion TransactSqlVersion { get; }
public DatabaseCompatibilityLevel DatabaseCompatibilityLevel { get; }
}
/// <summary>
/// Tests for the Binding Queue
/// </summary>
public class BindingQueueTests
{
private int bindCallCount = 0;
private int timeoutCallCount = 0;
private int bindCallbackDelay = 0;
private bool isCancelationRequested = false;
private IBindingContext bindingContext = null;
private BindingQueue<TestBindingContext> bindingQueue = null;
private void InitializeTestSettings()
{
this.bindCallCount = 0;
this.timeoutCallCount = 0;
this.bindCallbackDelay = 10;
this.isCancelationRequested = false;
this.bindingContext = GetMockBindingContext();
this.bindingQueue = new BindingQueue<TestBindingContext>();
}
private IBindingContext GetMockBindingContext()
{
return new TestBindingContext();
}
/// <summary>
/// Test bind operation callback
/// </summary>
private Task<object> TestBindOperation(
IBindingContext bindContext,
CancellationToken cancelToken)
{
return Task.Run(() =>
{
cancelToken.WaitHandle.WaitOne(this.bindCallbackDelay);
this.isCancelationRequested = cancelToken.IsCancellationRequested;
if (!this.isCancelationRequested)
{
++this.bindCallCount;
}
return new CompletionItem[0] as object;
});
}
/// <summary>
/// Test callback for the bind timeout operation
/// </summary>
private Task<object> TestTimeoutOperation(
IBindingContext bindingContext)
{
++this.timeoutCallCount;
return Task.FromResult(new CompletionItem[0] as object);
}
/// <summary>
/// Runs for a few seconds to allow the queue to pump any requests
/// </summary>
private void WaitForQueue(int delay = 5000)
{
int step = 50;
int steps = delay / step + 1;
for (int i = 0; i < steps; ++i)
{
Thread.Sleep(step);
}
}
/// <summary>
/// Queues a single task
/// </summary>
[Fact]
public void QueueOneBindingOperationTest()
{
InitializeTestSettings();
this.bindingQueue.QueueBindingOperation(
key: "testkey",
bindOperation: TestBindOperation,
timeoutOperation: TestTimeoutOperation);
WaitForQueue();
this.bindingQueue.StopQueueProcessor(15000);
Assert.True(this.bindCallCount == 1);
Assert.True(this.timeoutCallCount == 0);
Assert.False(this.isCancelationRequested);
}
/// <summary>
/// Queue a 100 short tasks
/// </summary>
[Fact]
public void Queue100BindingOperationTest()
{
InitializeTestSettings();
for (int i = 0; i < 100; ++i)
{
this.bindingQueue.QueueBindingOperation(
key: "testkey",
bindOperation: TestBindOperation,
timeoutOperation: TestTimeoutOperation);
}
WaitForQueue();
this.bindingQueue.StopQueueProcessor(15000);
Assert.True(this.bindCallCount == 100);
Assert.True(this.timeoutCallCount == 0);
Assert.False(this.isCancelationRequested);
}
/// <summary>
/// Queue an task with a long operation causing a timeout
/// </summary>
[Fact]
public void QueueWithTimeout()
{
InitializeTestSettings();
this.bindCallbackDelay = 10000;
this.bindingQueue.QueueBindingOperation(
key: "testkey",
bindOperation: TestBindOperation,
timeoutOperation: TestTimeoutOperation);
WaitForQueue(this.bindCallbackDelay + 2000);
this.bindingQueue.StopQueueProcessor(15000);
Assert.True(this.bindCallCount == 0);
Assert.True(this.timeoutCallCount == 1);
Assert.True(this.isCancelationRequested);
}
}
}

View File

@@ -33,6 +33,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.LanguageServices
{
#region "Diagnostics tests"
/// <summary>
/// Verify that the latest SqlParser (2016 as of this writing) is used by default
/// </summary>
@@ -154,12 +155,12 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.LanguageServices
InitializeTestServices();
Assert.True(LanguageService.Instance.Context != null);
Assert.True(LanguageService.Instance.ConnectionServiceInstance != null);
Assert.True(LanguageService.ConnectionServiceInstance != null);
Assert.True(LanguageService.Instance.CurrentSettings != null);
Assert.True(LanguageService.Instance.CurrentWorkspace != null);
LanguageService.Instance.ConnectionServiceInstance = null;
Assert.True(LanguageService.Instance.ConnectionServiceInstance == null);
LanguageService.ConnectionServiceInstance = null;
Assert.True(LanguageService.ConnectionServiceInstance == null);
}
/// <summary>
@@ -178,6 +179,18 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.LanguageServices
Connection = TestObjects.GetTestConnectionDetails()
});
// set up file for returning the query
var fileMock = new Mock<ScriptFile>();
fileMock.SetupGet(file => file.Contents).Returns(Common.StandardQuery);
fileMock.SetupGet(file => file.ClientFilePath).Returns(ownerUri);
// set up workspace mock
var workspaceService = new Mock<WorkspaceService<SqlToolsSettings>>();
workspaceService.Setup(service => service.Workspace.GetFile(It.IsAny<string>()))
.Returns(fileMock.Object);
AutoCompleteHelper.WorkspaceServiceInstance = workspaceService.Object;
ConnectionInfo connInfo = null;
connectionService.TryFindConnection(ownerUri, out connInfo);
@@ -212,7 +225,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.LanguageServices
ScriptParseInfo scriptInfo = new ScriptParseInfo();
scriptInfo.IsConnected = true;
AutoCompleteHelper.PrepopulateCommonMetadata(connInfo, scriptInfo);
AutoCompleteHelper.PrepopulateCommonMetadata(connInfo, scriptInfo, null);
}
private string GetTestSqlFile()

View File

@@ -253,12 +253,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution
var metadataProvider = SmoMetadataProvider.CreateConnectedProvider(srvConn);
var binder = BinderProvider.CreateBinder(metadataProvider);
LanguageService.Instance.ScriptParseInfoMap.Add(textDocument.TextDocument.Uri,
new ScriptParseInfo
{
Binder = binder,
MetadataProvider = metadataProvider
});
LanguageService.Instance.ScriptParseInfoMap.Add(textDocument.TextDocument.Uri, new ScriptParseInfo());
scriptFile = new ScriptFile {ClientFilePath = textDocument.TextDocument.Uri};

View File

@@ -21,6 +21,8 @@ namespace Microsoft.SqlTools.Test.Utility
/// </summary>
public class TestObjects
{
public const string ScriptUri = "file://some/file.sql";
/// <summary>
/// Creates a test connection service
/// </summary>
@@ -42,7 +44,7 @@ namespace Microsoft.SqlTools.Test.Utility
{
return new ConnectionInfo(
GetTestSqlConnectionFactory(),
"file://some/file.sql",
ScriptUri,
GetTestConnectionDetails());
}
@@ -50,7 +52,7 @@ namespace Microsoft.SqlTools.Test.Utility
{
return new ConnectParams()
{
OwnerUri = "file://some/file.sql",
OwnerUri = ScriptUri,
Connection = GetTestConnectionDetails()
};
}

View File

@@ -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.7",
"Microsoft.SqlServer.Smo": "140.1.8",
"System.Security.SecureString": "4.0.0",
"System.Collections.Specialized": "4.0.1",
"System.ComponentModel.TypeConverter": "4.1.0",