mirror of
https://github.com/ckaczor/sqltoolsservice.git
synced 2026-01-13 17:23:02 -05:00
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:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -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()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user