From 62525b9c98ba9f5c8dab28357e7cfe595f96346e Mon Sep 17 00:00:00 2001 From: Karl Burtram Date: Tue, 4 Oct 2016 14:55:59 -0700 Subject: [PATCH] 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 --- .gitignore | 2 + .../Connection/ConnectionService.cs | 12 + .../LanguageServices/AutoCompleteHelper.cs | 148 ++++++-- .../LanguageServices/BindingQueue.cs | 249 +++++++++++++ .../ConnectedBindingContext.cs | 208 +++++++++++ .../LanguageServices/ConnectedBindingQueue.cs | 109 ++++++ .../LanguageServices/IBindingContext.cs | 81 +++++ .../LanguageServices/LanguageService.cs | 338 +++++++++++------- .../LanguageServices/QueueItem.cs | 67 ++++ .../LanguageServices/ScriptParseInfo.cs | 184 +--------- .../Workspace/Contracts/ScriptFile.cs | 3 +- .../project.json | 2 +- .../LanguageServer/AutocompleteTests.cs | 124 +++++++ .../LanguageServer/BindingQueueTests.cs | 202 +++++++++++ .../LanguageServer/LanguageServiceTests.cs | 25 +- .../QueryExecution/Common.cs | 7 +- .../Utility/TestObjects.cs | 6 +- .../project.json | 2 +- 18 files changed, 1409 insertions(+), 360 deletions(-) create mode 100644 src/Microsoft.SqlTools.ServiceLayer/LanguageServices/BindingQueue.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/LanguageServices/ConnectedBindingContext.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/LanguageServices/ConnectedBindingQueue.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/LanguageServices/IBindingContext.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/LanguageServices/QueueItem.cs create mode 100644 test/Microsoft.SqlTools.ServiceLayer.Test/LanguageServer/AutocompleteTests.cs create mode 100644 test/Microsoft.SqlTools.ServiceLayer.Test/LanguageServer/BindingQueueTests.cs diff --git a/.gitignore b/.gitignore index 497b0f36..97e1cc41 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/ConnectionService.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/ConnectionService.cs index c948e728..80707a55 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Connection/ConnectionService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/ConnectionService.cs @@ -47,6 +47,18 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection private Dictionary ownerToConnectionMap = new Dictionary(); + /// + /// Map from script URIs to ConnectionInfo objects + /// This is internal for testing access only + /// + internal Dictionary OwnerToConnectionMap + { + get + { + return this.ownerToConnectionMap; + } + } + /// /// Service host object for sending/receiving requests/events. /// Internal for testing purposes. diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteHelper.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteHelper.cs index 85846738..c0244226 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteHelper.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteHelper.cs @@ -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 /// public static class AutoCompleteHelper { + private static WorkspaceService workspaceServiceInstance; + private static readonly string[] DefaultCompletionText = new string[] { "absolute", @@ -421,6 +424,26 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices "zone" }; + /// + /// Gets or sets the current workspace service instance + /// Setter for internal testing purposes only + /// + internal static WorkspaceService WorkspaceServiceInstance + { + get + { + if (AutoCompleteHelper.workspaceServiceInstance == null) + { + AutoCompleteHelper.workspaceServiceInstance = WorkspaceService.Instance; + } + return AutoCompleteHelper.workspaceServiceInstance; + } + set + { + AutoCompleteHelper.workspaceServiceInstance = value; + } + } + /// /// Get the default completion list from hard-coded list /// @@ -538,11 +561,14 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices /// /// /// - internal static void PrepopulateCommonMetadata(ConnectionInfo info, ScriptParseInfo scriptInfo) + internal static void PrepopulateCommonMetadata( + ConnectionInfo info, + ScriptParseInfo scriptInfo, + ConnectedBindingQueue bindingQueue) { if (scriptInfo.IsConnected) { - var scriptFile = WorkspaceService.Instance.Workspace.GetFile(info.OwnerUri); + var scriptFile = AutoCompleteHelper.WorkspaceServiceInstance.Workspace.GetFile(info.OwnerUri); LanguageService.Instance.ParseAndBind(scriptFile, info); if (scriptInfo.BuildingMetadataEvent.WaitOne(LanguageService.OnConnectionWaitTimeout)) @@ -551,44 +577,52 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices { scriptInfo.BuildingMetadataEvent.Reset(); - // parse a simple statement that returns common metadata - ParseResult parseResult = Parser.Parse( - "select ", - scriptInfo.ParseOptions); + QueueItem queueItem = bindingQueue.QueueBindingOperation( + key: scriptInfo.ConnectionKey, + bindOperation: (bindingContext, cancelToken) => + { + // parse a simple statement that returns common metadata + ParseResult parseResult = Parser.Parse( + "select ", + bindingContext.ParseOptions); - List parseResults = new List(); - parseResults.Add(parseResult); - scriptInfo.Binder.Bind( - parseResults, - info.ConnectionDetails.DatabaseName, - BindMode.Batch); + List parseResults = new List(); + parseResults.Add(parseResult); + bindingContext.Binder.Bind( + parseResults, + info.ConnectionDetails.DatabaseName, + BindMode.Batch); - // get the completion list from SQL Parser - var suggestions = Resolver.FindCompletions( - parseResult, 1, 8, - scriptInfo.MetadataDisplayInfoProvider); + // get the completion list from SQL Parser + var suggestions = Resolver.FindCompletions( + parseResult, 1, 8, + bindingContext.MetadataDisplayInfoProvider); - // this forces lazy evaluation of the suggestion metadata - AutoCompleteHelper.ConvertDeclarationsToCompletionItems(suggestions, 1, 8, 8); + // this forces lazy evaluation of the suggestion metadata + AutoCompleteHelper.ConvertDeclarationsToCompletionItems(suggestions, 1, 8, 8); - parseResult = Parser.Parse( - "exec ", - scriptInfo.ParseOptions); + parseResult = Parser.Parse( + "exec ", + bindingContext.ParseOptions); - parseResults = new List(); - parseResults.Add(parseResult); - scriptInfo.Binder.Bind( - parseResults, - info.ConnectionDetails.DatabaseName, - BindMode.Batch); + parseResults = new List(); + parseResults.Add(parseResult); + bindingContext.Binder.Bind( + parseResults, + info.ConnectionDetails.DatabaseName, + BindMode.Batch); - // get the completion list from SQL Parser - suggestions = Resolver.FindCompletions( - parseResult, 1, 6, - scriptInfo.MetadataDisplayInfoProvider); + // get the completion list from SQL Parser + suggestions = Resolver.FindCompletions( + parseResult, 1, 6, + bindingContext.MetadataDisplayInfoProvider); - // this forces lazy evaluation of the suggestion metadata - AutoCompleteHelper.ConvertDeclarationsToCompletionItems(suggestions, 1, 6, 6); + // this forces lazy evaluation of the suggestion metadata + AutoCompleteHelper.ConvertDeclarationsToCompletionItems(suggestions, 1, 6, 6); + return Task.FromResult(null as object); + }); + + queueItem.ItemProcessed.WaitOne(); } catch { @@ -600,5 +634,53 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices } } } + + + /// + /// Converts a SQL Parser QuickInfo object into a VS Code Hover object + /// + /// + /// + /// + /// + internal static Hover ConvertQuickInfoToHover( + Babel.CodeObjectQuickInfo quickInfo, + int row, + int startColumn, + int endColumn) + { + // convert from the parser format to the VS Code wire format + var markedStrings = new MarkedString[1]; + if (quickInfo != null) + { + markedStrings[0] = new MarkedString() + { + Language = "SQL", + Value = quickInfo.Text + }; + + return new Hover() + { + Contents = markedStrings, + Range = new Range + { + Start = new Position + { + Line = row, + Character = startColumn + }, + End = new Position + { + Line = row, + Character = endColumn + } + } + }; + } + else + { + return null; + } + } } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/BindingQueue.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/BindingQueue.cs new file mode 100644 index 00000000..4417cda6 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/BindingQueue.cs @@ -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 +{ + /// + /// Main class for the Binding Queue + /// + public class BindingQueue where T : IBindingContext, new() + { + private CancellationTokenSource processQueueCancelToken = new CancellationTokenSource(); + + private ManualResetEvent itemQueuedEvent = new ManualResetEvent(initialState: false); + + private object bindingQueueLock = new object(); + + private LinkedList bindingQueue = new LinkedList(); + + private object bindingContextLock = new object(); + + private Task queueProcessorTask; + + /// + /// Map from context keys to binding context instances + /// Internal for testing purposes only + /// + internal Dictionary BindingContextMap { get; set; } + + /// + /// Constructor for a binding queue instance + /// + public BindingQueue() + { + this.BindingContextMap = new Dictionary(); + + this.queueProcessorTask = StartQueueProcessor(); + } + + /// + /// Stops the binding queue by sending cancellation request + /// + /// + public bool StopQueueProcessor(int timeout) + { + this.processQueueCancelToken.Cancel(); + return this.queueProcessorTask.Wait(timeout); + } + + /// + /// Queue a binding request item + /// + public QueueItem QueueBindingOperation( + string key, + Func> bindOperation, + Func> timeoutOperation = null, + int? bindingTimeout = null) + { + // don't add null operations to the binding queue + if (bindOperation == null) + { + return null; + } + + QueueItem queueItem = new QueueItem() + { + Key = key, + BindOperation = bindOperation, + TimeoutOperation = timeoutOperation, + BindingTimeout = bindingTimeout + }; + + lock (this.bindingQueueLock) + { + this.bindingQueue.AddLast(queueItem); + } + + this.itemQueuedEvent.Set(); + + return queueItem; + } + + /// + /// Gets or creates a binding context for the provided context key + /// + /// + protected IBindingContext GetOrCreateBindingContext(string key) + { + // use a default binding context for disconnected requests + if (string.IsNullOrWhiteSpace(key)) + { + key = "disconnected_binding_context"; + } + + lock (this.bindingContextLock) + { + if (!this.BindingContextMap.ContainsKey(key)) + { + this.BindingContextMap.Add(key, new T()); + } + + return this.BindingContextMap[key]; + } + } + + private bool HasPendingQueueItems + { + get + { + lock (this.bindingQueueLock) + { + return this.bindingQueue.Count > 0; + } + } + } + + /// + /// Gets the next pending queue item + /// + private QueueItem GetNextQueueItem() + { + lock (this.bindingQueueLock) + { + if (this.bindingQueue.Count == 0) + { + return null; + } + + QueueItem queueItem = this.bindingQueue.First.Value; + this.bindingQueue.RemoveFirst(); + return queueItem; + } + } + + /// + /// Starts the queue processing thread + /// + private Task StartQueueProcessor() + { + return Task.Factory.StartNew( + ProcessQueue, + this.processQueueCancelToken.Token, + TaskCreationOptions.LongRunning, + TaskScheduler.Default); + } + + /// + /// The core queue processing method + /// + /// + private void ProcessQueue() + { + CancellationToken token = this.processQueueCancelToken.Token; + WaitHandle[] waitHandles = new WaitHandle[2] + { + this.itemQueuedEvent, + token.WaitHandle + }; + + while (true) + { + // wait for with an item to be queued or the a cancellation request + WaitHandle.WaitAny(waitHandles); + if (token.IsCancellationRequested) + { + break; + } + + try + { + // dispatch all pending queue items + while (this.HasPendingQueueItems) + { + QueueItem queueItem = GetNextQueueItem(); + if (queueItem == null) + { + continue; + } + + IBindingContext bindingContext = GetOrCreateBindingContext(queueItem.Key); + if (bindingContext == null) + { + queueItem.ItemProcessed.Set(); + continue; + } + + // 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(); + } + } + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/ConnectedBindingContext.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/ConnectedBindingContext.cs new file mode 100644 index 00000000..8851abe1 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/ConnectedBindingContext.cs @@ -0,0 +1,208 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Threading; +using Microsoft.SqlServer.Management.Common; +using Microsoft.SqlServer.Management.SmoMetadataProvider; +using Microsoft.SqlServer.Management.SqlParser.Binder; +using Microsoft.SqlServer.Management.SqlParser.Common; +using Microsoft.SqlServer.Management.SqlParser.MetadataProvider; +using Microsoft.SqlServer.Management.SqlParser.Parser; + +namespace Microsoft.SqlTools.ServiceLayer.LanguageServices +{ + /// + /// Class for the binding context for connected sessions + /// + public class ConnectedBindingContext : IBindingContext + { + private ParseOptions parseOptions; + + private ServerConnection serverConnection; + + /// + /// Connected binding context constructor + /// + public ConnectedBindingContext() + { + this.BindingLocked = new ManualResetEvent(initialState: true); + this.BindingTimeout = ConnectedBindingQueue.DefaultBindingTimeout; + this.MetadataDisplayInfoProvider = new MetadataDisplayInfoProvider(); + } + + /// + /// Gets or sets a flag indicating if the binder is connected + /// + public bool IsConnected { get; set; } + + /// + /// Gets or sets the binding server connection + /// + public ServerConnection ServerConnection + { + get + { + return this.serverConnection; + } + set + { + this.serverConnection = value; + + // reset the parse options so the get recreated for the current connection + this.parseOptions = null; + } + } + + /// + /// Gets or sets the metadata display info provider + /// + public MetadataDisplayInfoProvider MetadataDisplayInfoProvider { get; set; } + + /// + /// Gets or sets the SMO metadata provider + /// + public SmoMetadataProvider SmoMetadataProvider { get; set; } + + /// + /// Gets or sets the binder + /// + public IBinder Binder { get; set; } + + /// + /// Gets or sets an event to signal if a binding operation is in progress + /// + public ManualResetEvent BindingLocked { get; set; } + + /// + /// Gets or sets the binding operation timeout in milliseconds + /// + public int BindingTimeout { get; set; } + + /// + /// Gets the Language Service ServerVersion + /// + public ServerVersion ServerVersion + { + get + { + return this.ServerConnection != null + ? this.ServerConnection.ServerVersion + : null; + } + } + + /// + /// Gets the current DataEngineType + /// + public DatabaseEngineType DatabaseEngineType + { + get + { + return this.ServerConnection != null + ? this.ServerConnection.DatabaseEngineType + : DatabaseEngineType.Standalone; + } + } + + /// + /// Gets the current connections TransactSqlVersion + /// + public TransactSqlVersion TransactSqlVersion + { + get + { + return this.IsConnected + ? GetTransactSqlVersion(this.ServerVersion) + : TransactSqlVersion.Current; + } + } + + /// + /// Gets the current DatabaseCompatibilityLevel + /// + public DatabaseCompatibilityLevel DatabaseCompatibilityLevel + { + get + { + return this.IsConnected + ? GetDatabaseCompatibilityLevel(this.ServerVersion) + : DatabaseCompatibilityLevel.Current; + } + } + + /// + /// Gets the current ParseOptions + /// + public ParseOptions ParseOptions + { + get + { + if (this.parseOptions == null) + { + this.parseOptions = new ParseOptions( + batchSeparator: LanguageService.DefaultBatchSeperator, + isQuotedIdentifierSet: true, + compatibilityLevel: DatabaseCompatibilityLevel, + transactSqlVersion: TransactSqlVersion); + } + return this.parseOptions; + } + } + + + /// + /// Gets the database compatibility level from a server version + /// + /// + private static DatabaseCompatibilityLevel GetDatabaseCompatibilityLevel(ServerVersion serverVersion) + { + int versionMajor = Math.Max(serverVersion.Major, 8); + + switch (versionMajor) + { + case 8: + return DatabaseCompatibilityLevel.Version80; + case 9: + return DatabaseCompatibilityLevel.Version90; + case 10: + return DatabaseCompatibilityLevel.Version100; + case 11: + return DatabaseCompatibilityLevel.Version110; + case 12: + return DatabaseCompatibilityLevel.Version120; + case 13: + return DatabaseCompatibilityLevel.Version130; + default: + return DatabaseCompatibilityLevel.Current; + } + } + + /// + /// Gets the transaction sql version from a server version + /// + /// + private static TransactSqlVersion GetTransactSqlVersion(ServerVersion serverVersion) + { + int versionMajor = Math.Max(serverVersion.Major, 9); + + switch (versionMajor) + { + case 9: + case 10: + // In case of 10.0 we still use Version 10.5 as it is the closest available. + return TransactSqlVersion.Version105; + case 11: + return TransactSqlVersion.Version110; + case 12: + return TransactSqlVersion.Version120; + case 13: + return TransactSqlVersion.Version130; + default: + return TransactSqlVersion.Current; + } + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/ConnectedBindingQueue.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/ConnectedBindingQueue.cs new file mode 100644 index 00000000..c99f0cc6 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/ConnectedBindingQueue.cs @@ -0,0 +1,109 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Data.SqlClient; +using Microsoft.SqlServer.Management.Common; +using Microsoft.SqlServer.Management.SmoMetadataProvider; +using Microsoft.SqlServer.Management.SqlParser.Binder; +using Microsoft.SqlServer.Management.SqlParser.MetadataProvider; +using Microsoft.SqlTools.ServiceLayer.Connection; +using Microsoft.SqlTools.ServiceLayer.Connection.Contracts; +using Microsoft.SqlTools.ServiceLayer.SqlContext; +using Microsoft.SqlTools.ServiceLayer.Workspace; + +namespace Microsoft.SqlTools.ServiceLayer.LanguageServices +{ + /// + /// ConnectedBindingQueue class for processing online binding requests + /// + public class ConnectedBindingQueue : BindingQueue + { + internal const int DefaultBindingTimeout = 60000; + + internal const int DefaultMinimumConnectionTimeout = 30; + + /// + /// Gets the current settings + /// + internal SqlToolsSettings CurrentSettings + { + get { return WorkspaceService.Instance.CurrentSettings; } + } + + /// + /// Generate a unique key based on the ConnectionInfo object + /// + /// + private string GetConnectionContextKey(ConnectionInfo connInfo) + { + ConnectionDetails details = connInfo.ConnectionDetails; + return string.Format("{0}_{1}_{2}_{3}", + details.ServerName ?? "NULL", + details.DatabaseName ?? "NULL", + details.UserName ?? "NULL", + details.AuthenticationType ?? "NULL" + ); + } + + /// + /// Use a ConnectionInfo item to create a connected binding context + /// + /// + public virtual string AddConnectionContext(ConnectionInfo connInfo) + { + if (connInfo == null) + { + return string.Empty; + } + + // lookup the current binding context + string connectionKey = GetConnectionContextKey(connInfo); + IBindingContext bindingContext = this.GetOrCreateBindingContext(connectionKey); + + try + { + // increase the connection timeout to at least 30 seconds and and build connection string + // enable PersistSecurityInfo to handle issues in SMO where the connection context is lost in reconnections + int? originalTimeout = connInfo.ConnectionDetails.ConnectTimeout; + bool? originalPersistSecurityInfo = connInfo.ConnectionDetails.PersistSecurityInfo; + connInfo.ConnectionDetails.ConnectTimeout = Math.Max(DefaultMinimumConnectionTimeout, originalTimeout ?? 0); + connInfo.ConnectionDetails.PersistSecurityInfo = true; + string connectionString = ConnectionService.BuildConnectionString(connInfo.ConnectionDetails); + connInfo.ConnectionDetails.ConnectTimeout = originalTimeout; + connInfo.ConnectionDetails.PersistSecurityInfo = originalPersistSecurityInfo; + + // open a dedicated binding server connection + SqlConnection sqlConn = new SqlConnection(connectionString); + if (sqlConn != null) + { + sqlConn.Open(); + + // populate the binding context to work with the SMO metadata provider + ServerConnection serverConn = new ServerConnection(sqlConn); + bindingContext.SmoMetadataProvider = SmoMetadataProvider.CreateConnectedProvider(serverConn); + bindingContext.MetadataDisplayInfoProvider = new MetadataDisplayInfoProvider(); + bindingContext.MetadataDisplayInfoProvider.BuiltInCasing = + this.CurrentSettings.SqlTools.IntelliSense.LowerCaseSuggestions.Value + ? CasingStyle.Lowercase : CasingStyle.Uppercase; + bindingContext.Binder = BinderProvider.CreateBinder(bindingContext.SmoMetadataProvider); + bindingContext.ServerConnection = serverConn; + bindingContext.BindingTimeout = ConnectedBindingQueue.DefaultBindingTimeout; + bindingContext.IsConnected = true; + } + } + catch (Exception) + { + bindingContext.IsConnected = false; + } + finally + { + bindingContext.BindingLocked.Set(); + } + + return connectionKey; + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/IBindingContext.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/IBindingContext.cs new file mode 100644 index 00000000..c83a28d7 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/IBindingContext.cs @@ -0,0 +1,81 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Threading; +using Microsoft.SqlServer.Management.Common; +using Microsoft.SqlServer.Management.SmoMetadataProvider; +using Microsoft.SqlServer.Management.SqlParser.Binder; +using Microsoft.SqlServer.Management.SqlParser.Common; +using Microsoft.SqlServer.Management.SqlParser.MetadataProvider; +using Microsoft.SqlServer.Management.SqlParser.Parser; + +namespace Microsoft.SqlTools.ServiceLayer.LanguageServices +{ + /// + /// The context used for binding requests + /// + public interface IBindingContext + { + /// + /// Gets or sets a flag indicating if the context is connected + /// + bool IsConnected { get; set; } + + /// + /// Gets or sets the binding server connection + /// + ServerConnection ServerConnection { get; set; } + + /// + /// Gets or sets the metadata display info provider + /// + MetadataDisplayInfoProvider MetadataDisplayInfoProvider { get; set; } + + /// + /// Gets or sets the SMO metadata provider + /// + SmoMetadataProvider SmoMetadataProvider { get; set; } + + /// + /// Gets or sets the binder + /// + IBinder Binder { get; set; } + + /// + /// Gets or sets an event to signal if a binding operation is in progress + /// + ManualResetEvent BindingLocked { get; set; } + + /// + /// Gets or sets the binding operation timeout in milliseconds + /// + int BindingTimeout { get; set; } + + /// + /// Gets or sets the current connection parse options + /// + ParseOptions ParseOptions { get; } + + /// + /// Gets or sets the current connection server version + /// + ServerVersion ServerVersion { get; } + + /// + /// Gets or sets the database engine type + /// + DatabaseEngineType DatabaseEngineType { get; } + + /// + /// Gets or sets the T-SQL version + /// + TransactSqlVersion TransactSqlVersion { get; } + + /// + /// Gets or sets the database compatibility level + /// + DatabaseCompatibilityLevel DatabaseCompatibilityLevel { get; } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs index b0c2440c..fe196f7d 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs @@ -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,26 +37,54 @@ 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 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); + + /// + /// Gets or sets the binding queue instance + /// Internal for testing purposes only + /// + internal ConnectedBindingQueue BindingQueue + { + get + { + return this.bindingQueue; + } + set + { + this.bindingQueue = value; + } + } /// /// Internal for testing purposes only /// - internal ConnectionService ConnectionServiceInstance + internal static ConnectionService ConnectionServiceInstance { get { - if(connectionService == null) + if (connectionService == null) { connectionService = ConnectionService.Instance; } @@ -96,6 +123,14 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices get { return instance.Value; } } + private ParseOptions DefaultParseOptions + { + get + { + return this.defaultParseOptions; + } + } + /// /// Default, parameterless constructor. /// @@ -109,14 +144,40 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices private static CancellationTokenSource ExistingRequestCancellation { get; set; } + /// + /// Gets the current settings + /// internal SqlToolsSettings CurrentSettings { get { return WorkspaceService.Instance.CurrentSettings; } } + /// + /// Gets or sets the current workspace service instance + /// Setter for internal testing purposes only + /// + internal static WorkspaceService WorkspaceServiceInstance + { + get + { + if (LanguageService.workspaceServiceInstance == null) + { + LanguageService.workspaceServiceInstance = WorkspaceService.Instance; + } + return LanguageService.workspaceServiceInstance; + } + set + { + LanguageService.workspaceServiceInstance = value; + } + } + + /// + /// Gets the current workspace instance + /// internal Workspace.Workspace CurrentWorkspace { - get { return WorkspaceService.Instance.Workspace; } + get { return LanguageService.WorkspaceServiceInstance.Workspace; } } /// @@ -181,7 +242,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices /// /// /// - private static async Task HandleCompletionRequest( + internal static async Task HandleCompletionRequest( TextDocumentPosition textDocumentPosition, RequestContext 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.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 /// /// /// - /// + /// The ParseResult instance returned from SQL Parser public ParseResult ParseAndBind(ScriptFile scriptFile, ConnectionInfo connInfo) { // get or create the current parse info object @@ -403,34 +458,62 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices { parseInfo.BuildingMetadataEvent.Reset(); - // parse current SQL file contents to retrieve a list of errors - ParseResult parseResult = Parser.IncrementalParse( - scriptFile.Contents, - parseInfo.ParseResult, - parseInfo.ParseOptions); - - parseInfo.ParseResult = parseResult; - - if (connInfo != null && parseInfo.IsConnected) + if (connInfo == null || !parseInfo.IsConnected) { - try - { - List parseResults = new List(); - parseResults.Add(parseResult); - parseInfo.Binder.Bind( - parseResults, - connInfo.ConnectionDetails.DatabaseName, - BindMode.Batch); - } - catch (ConnectionException) - { - Logger.Write(LogLevel.Error, "Hit connection exception while binding - disposing binder object..."); - } - catch (SqlParserInternalBinderError) - { - Logger.Write(LogLevel.Error, "Hit connection exception while binding - disposing binder object..."); - } + // parse current SQL file contents to retrieve a list of errors + ParseResult parseResult = Parser.IncrementalParse( + scriptFile.Contents, + parseInfo.ParseResult, + this.DefaultParseOptions); + + parseInfo.ParseResult = parseResult; } + else + { + QueueItem queueItem = this.BindingQueue.QueueBindingOperation( + key: parseInfo.ConnectionKey, + bindOperation: (bindingContext, cancelToken) => + { + try + { + ParseResult parseResult = Parser.IncrementalParse( + scriptFile.Contents, + parseInfo.ParseResult, + bindingContext.ParseOptions); + + parseInfo.ParseResult = parseResult; + + List parseResults = new List(); + parseResults.Add(parseResult); + bindingContext.Binder.Bind( + parseResults, + connInfo.ConnectionDetails.DatabaseName, + BindMode.Batch); + } + catch (ConnectionException) + { + Logger.Write(LogLevel.Error, "Hit connection exception while binding - disposing binder object..."); + } + catch (SqlParserInternalBinderError) + { + Logger.Write(LogLevel.Error, "Hit connection exception while binding - disposing binder object..."); + } + catch (Exception ex) + { + Logger.Write(LogLevel.Error, "Unknown exception during parsing " + ex.ToString()); + } + + return 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 { @@ -447,28 +530,21 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices /// public async Task UpdateLanguageServiceOnConnection(ConnectionInfo info) { - await Task.Run( () => + await Task.Run(() => { ScriptParseInfo scriptInfo = GetScriptParseInfo(info.OwnerUri, createIfNotExists: true); if (scriptInfo.BuildingMetadataEvent.WaitOne(LanguageService.OnConnectionWaitTimeout)) { 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.IsConnected = true; - } + scriptInfo.BuildingMetadataEvent.Reset(); + 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 @@ -477,10 +553,9 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices // (Tell Language Service that I am ready with Metadata Provider Object) scriptInfo.BuildingMetadataEvent.Set(); } - } + } - // populate SMO metadata provider with most common info - AutoCompleteHelper.PrepopulateCommonMetadata(info, scriptInfo); + AutoCompleteHelper.PrepopulateCommonMetadata(info, scriptInfo, this.BindingQueue); }); } @@ -556,41 +631,30 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices scriptParseInfo.BuildingMetadataEvent.Reset(); try { - // get the current quick info text - Babel.CodeObjectQuickInfo quickInfo = Resolver.GetQuickInfo( - scriptParseInfo.ParseResult, - startLine + 1, - endColumn + 1, - scriptParseInfo.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 new Hover() - { - Contents = markedStrings, - Range = new Range - { - Start = new Position - { - Line = startLine, - Character = startColumn - }, - End = new Position - { - Line = startLine, - Character = endColumn - } - } - }; - } + QueueItem queueItem = this.BindingQueue.QueueBindingOperation( + key: scriptParseInfo.ConnectionKey, + bindingTimeout: LanguageService.HoverTimeout, + bindOperation: (bindingContext, cancelToken) => + { + // get the current quick info text + Babel.CodeObjectQuickInfo quickInfo = Resolver.GetQuickInfo( + scriptParseInfo.ParseResult, + startLine + 1, + endColumn + 1, + bindingContext.MetadataDisplayInfoProvider); + + // convert from the parser format to the VS Code wire format + return Task.FromResult( + AutoCompleteHelper.ConvertQuickInfoToHover( + quickInfo, + startLine, + startColumn, + endColumn + ) as object); + }); + + queueItem.ItemProcessed.WaitOne(); + return queueItem.GetResultAsT(); } finally { @@ -646,39 +710,50 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices if (scriptParseInfo.IsConnected && scriptParseInfo.BuildingMetadataEvent.WaitOne(LanguageService.FindCompletionStartTimeout)) { - scriptParseInfo.BuildingMetadataEvent.Reset(); - Task findCompletionsTask = Task.Run(() => { - try + scriptParseInfo.BuildingMetadataEvent.Reset(); + + QueueItem queueItem = this.BindingQueue.QueueBindingOperation( + key: scriptParseInfo.ConnectionKey, + bindOperation: (bindingContext, cancelToken) => { - // get the completion list from SQL Parser - scriptParseInfo.CurrentSuggestions = Resolver.FindCompletions( - scriptParseInfo.ParseResult, - textDocumentPosition.Position.Line + 1, - textDocumentPosition.Position.Character + 1, - scriptParseInfo.MetadataDisplayInfoProvider); + CompletionItem[] completions = null; + try + { + // get the completion list from SQL Parser + scriptParseInfo.CurrentSuggestions = Resolver.FindCompletions( + scriptParseInfo.ParseResult, + textDocumentPosition.Position.Line + 1, + textDocumentPosition.Position.Character + 1, + bindingContext.MetadataDisplayInfoProvider); - // cache the current script parse info object to resolve completions later - this.currentCompletionParseInfo = scriptParseInfo; + // cache the current script parse info object to resolve completions later + this.currentCompletionParseInfo = scriptParseInfo; - // convert the suggestion list to the VS Code format - return AutoCompleteHelper.ConvertDeclarationsToCompletionItems( - scriptParseInfo.CurrentSuggestions, - startLine, - startColumn, - endColumn); - } - finally + // convert the suggestion list to the VS Code format + completions = AutoCompleteHelper.ConvertDeclarationsToCompletionItems( + scriptParseInfo.CurrentSuggestions, + startLine, + startColumn, + endColumn); + } + finally + { + scriptParseInfo.BuildingMetadataEvent.Set(); + } + + return Task.FromResult(completions as object); + }, + timeoutOperation: (bindingContext) => { - scriptParseInfo.BuildingMetadataEvent.Set(); - } - }); - - findCompletionsTask.Wait(LanguageService.FindCompletionsTimeout); - if (findCompletionsTask.IsCompleted - && findCompletionsTask.Result != null - && findCompletionsTask.Result.Length > 0) + return Task.FromResult( + AutoCompleteHelper.GetDefaultCompletionItems(startLine, startColumn, endColumn, useLowerCaseSuggestions) as object); + }); + + queueItem.ItemProcessed.WaitOne(); + var completionItems = queueItem.GetResultAsT(); + 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) + /// + /// Adds a new or updates an existing script parse info instance in local cache + /// + /// + /// + 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) + /// + /// Gets a script parse info object for a file from the local cache + /// Internal for testing purposes only + /// + /// + /// Creates a new instance if one doesn't exist + internal ScriptParseInfo GetScriptParseInfo(string uri, bool createIfNotExists = false) { lock (this.parseMapLock) { @@ -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; } @@ -873,10 +958,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices lock (this.parseMapLock) { if (this.ScriptParseInfoMap.ContainsKey(uri)) - { - var scriptInfo = this.ScriptParseInfoMap[uri]; - scriptInfo.ServerConnection.Disconnect(); - scriptInfo.ServerConnection = null; + { return this.ScriptParseInfoMap.Remove(uri); } else diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/QueueItem.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/QueueItem.cs new file mode 100644 index 00000000..2ec25e1a --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/QueueItem.cs @@ -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 +{ + /// + /// Class that stores the state of a binding queue request item + /// + public class QueueItem + { + /// + /// QueueItem constructor + /// + public QueueItem() + { + this.ItemProcessed = new ManualResetEvent(initialState: false); + } + + /// + /// Gets or sets the queue item key + /// + public string Key { get; set; } + + /// + /// Gets or sets the bind operation callback method + /// + public Func> BindOperation { get; set; } + + /// + /// Gets or sets the timeout operation to call if the bind operation doesn't finish within timeout period + /// + public Func> TimeoutOperation { get; set; } + + /// + /// Gets or sets an event to signal when this queue item has been processed + /// + public ManualResetEvent ItemProcessed { get; set; } + + /// + /// Gets or sets the task that was used to execute this queue item. + /// This allows the queuer to retrieve the execution result. + /// + public Task ResultsTask { get; set; } + + /// + /// Gets or sets the binding operation timeout in milliseconds + /// + public int? BindingTimeout { get; set; } + + /// + /// Converts the result of the execution task to type T + /// + public T GetResultAsT() where T : class + { + var task = this.ResultsTask; + return (task != null && task.IsCompleted && task.Result != null) + ? task.Result as T + : null; + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/ScriptParseInfo.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/ScriptParseInfo.cs index 08f8a07e..2c56d497 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/ScriptParseInfo.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/ScriptParseInfo.cs @@ -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 = new Lazy(() => - { - var infoProvider = new MetadataDisplayInfoProvider(); - return infoProvider; - }); - /// /// Event which tells if MetadataProvider is built fully or not /// @@ -48,181 +31,18 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices public bool IsConnected { get; set; } /// - /// Gets or sets the LanguageService SMO ServerConnection + /// Gets or sets the binding queue connection context key /// - public ServerConnection ServerConnection - { - get - { - return this.serverConnection; - } - set - { - this.serverConnection = value; - this.parseOptions = new ParseOptions( - batchSeparator: LanguageService.DefaultBatchSeperator, - isQuotedIdentifierSet: true, - compatibilityLevel: DatabaseCompatibilityLevel, - transactSqlVersion: TransactSqlVersion); - } - } - - /// - /// Gets the Language Service ServerVersion - /// - public ServerVersion ServerVersion - { - get - { - return this.ServerConnection != null - ? this.ServerConnection.ServerVersion - : null; - } - } - - /// - /// Gets the current DataEngineType - /// - public DatabaseEngineType DatabaseEngineType - { - get - { - return this.ServerConnection != null - ? this.ServerConnection.DatabaseEngineType - : DatabaseEngineType.Standalone; - } - } - - /// - /// Gets the current connections TransactSqlVersion - /// - public TransactSqlVersion TransactSqlVersion - { - get - { - return this.IsConnected - ? GetTransactSqlVersion(this.ServerVersion) - : TransactSqlVersion.Current; - } - } - - /// - /// Gets the current DatabaseCompatibilityLevel - /// - public DatabaseCompatibilityLevel DatabaseCompatibilityLevel - { - get - { - return this.IsConnected - ? GetDatabaseCompatibilityLevel(this.ServerVersion) - : DatabaseCompatibilityLevel.Current; - } - } - - /// - /// Gets the current ParseOptions - /// - public ParseOptions ParseOptions - { - get - { - return this.parseOptions; - } - } - - /// - /// Gets or sets the SMO binder for schema-aware intellisense - /// - public IBinder Binder { get; set; } + public string ConnectionKey { get; set; } /// /// Gets or sets the previous SQL parse result /// public ParseResult ParseResult { get; set; } - - /// - /// Gets or set the SMO metadata provider that's bound to the current connection - /// - public SmoMetadataProvider MetadataProvider { get; set; } - - /// - /// Gets or sets the SMO metadata display info provider - /// - public MetadataDisplayInfoProvider MetadataDisplayInfoProvider - { - get - { - return this.metadataDisplayInfoProvider.Value; - } - } /// /// Gets or sets the current autocomplete suggestion list /// public IEnumerable CurrentSuggestions { get; set; } - - /// - /// Update parse settings if the current configuration has changed - /// - /// - public void OnSettingsChanged(SqlToolsSettings settings) - { - this.MetadataDisplayInfoProvider.BuiltInCasing = - settings.SqlTools.IntelliSense.LowerCaseSuggestions.Value - ? CasingStyle.Lowercase - : CasingStyle.Uppercase; - } - - /// - /// Gets the database compatibility level from a server version - /// - /// - private static DatabaseCompatibilityLevel GetDatabaseCompatibilityLevel(ServerVersion serverVersion) - { - int versionMajor = Math.Max(serverVersion.Major, 8); - - switch (versionMajor) - { - case 8: - return DatabaseCompatibilityLevel.Version80; - case 9: - return DatabaseCompatibilityLevel.Version90; - case 10: - return DatabaseCompatibilityLevel.Version100; - case 11: - return DatabaseCompatibilityLevel.Version110; - case 12: - return DatabaseCompatibilityLevel.Version120; - case 13: - return DatabaseCompatibilityLevel.Version130; - default: - return DatabaseCompatibilityLevel.Current; - } - } - - /// - /// Gets the transaction sql version from a server version - /// - /// - private static TransactSqlVersion GetTransactSqlVersion(ServerVersion serverVersion) - { - int versionMajor = Math.Max(serverVersion.Major, 9); - - switch (versionMajor) - { - case 9: - case 10: - // In case of 10.0 we still use Version 10.5 as it is the closest available. - return TransactSqlVersion.Version105; - case 11: - return TransactSqlVersion.Version110; - case 12: - return TransactSqlVersion.Version120; - case 13: - return TransactSqlVersion.Version130; - default: - return TransactSqlVersion.Current; - } - } } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/Workspace/Contracts/ScriptFile.cs b/src/Microsoft.SqlTools.ServiceLayer/Workspace/Contracts/ScriptFile.cs index 4eaff942..279f9b7f 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Workspace/Contracts/ScriptFile.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Workspace/Contracts/ScriptFile.cs @@ -36,8 +36,9 @@ namespace Microsoft.SqlTools.ServiceLayer.Workspace.Contracts /// /// Gets or sets the path which the editor client uses to identify this file. /// Setter for testing purposes only + /// virtual to allow mocking. /// - public string ClientFilePath { get; internal set; } + public virtual string ClientFilePath { get; internal set; } /// /// Gets or sets a boolean that determines whether diff --git a/src/Microsoft.SqlTools.ServiceLayer/project.json b/src/Microsoft.SqlTools.ServiceLayer/project.json index 53c489e7..aa33787e 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/project.json +++ b/src/Microsoft.SqlTools.ServiceLayer/project.json @@ -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", diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/LanguageServer/AutocompleteTests.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/LanguageServer/AutocompleteTests.cs new file mode 100644 index 00000000..a9e346cd --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/LanguageServer/AutocompleteTests.cs @@ -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 +{ + /// + /// Tests for the language service autocomplete component + /// + public class AutocompleteTests + { + private const int TaskTimeout = 60000; + + private readonly string testScriptUri = TestObjects.ScriptUri; + + private readonly string testConnectionKey = "testdbcontextkey"; + + private Mock bindingQueue; + + private Mock> workspaceService; + + private Mock> requestContext; + + private Mock 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.Instance.CurrentSettings = new SqlToolsSettings(); + + // set up file for returning the query + var fileMock = new Mock(); + fileMock.SetupGet(file => file.Contents).Returns(Common.StandardQuery); + fileMock.SetupGet(file => file.ClientFilePath).Returns(this.testScriptUri); + + // set up workspace mock + workspaceService = new Mock>(); + workspaceService.Setup(service => service.Workspace.GetFile(It.IsAny())) + .Returns(fileMock.Object); + + // setup binding queue mock + bindingQueue = new Mock(); + bindingQueue.Setup(q => q.AddConnectionContext(It.IsAny())) + .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.Setup(rc => rc.SendResult(It.IsAny())) + .Returns(Task.FromResult(0)); + + // setup the IBinder mock + binder = new Mock(); + binder.Setup(b => b.Bind( + It.IsAny>(), + It.IsAny(), + It.IsAny())); + + 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); + } + + /// + /// Tests the primary completion list event handler + /// + [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()), Times.Once()); + } + } +} diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/LanguageServer/BindingQueueTests.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/LanguageServer/BindingQueueTests.cs new file mode 100644 index 00000000..5d79e292 --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/LanguageServer/BindingQueueTests.cs @@ -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 +{ + + /// + /// Test class for the test binding context + /// + 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; } + } + + /// + /// Tests for the Binding Queue + /// + 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 bindingQueue = null; + + private void InitializeTestSettings() + { + this.bindCallCount = 0; + this.timeoutCallCount = 0; + this.bindCallbackDelay = 10; + this.isCancelationRequested = false; + this.bindingContext = GetMockBindingContext(); + this.bindingQueue = new BindingQueue(); + } + + private IBindingContext GetMockBindingContext() + { + return new TestBindingContext(); + } + + /// + /// Test bind operation callback + /// + private Task 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; + }); + } + + /// + /// Test callback for the bind timeout operation + /// + private Task TestTimeoutOperation( + IBindingContext bindingContext) + { + ++this.timeoutCallCount; + return Task.FromResult(new CompletionItem[0] as object); + } + + /// + /// Runs for a few seconds to allow the queue to pump any requests + /// + private void WaitForQueue(int delay = 5000) + { + int step = 50; + int steps = delay / step + 1; + for (int i = 0; i < steps; ++i) + { + Thread.Sleep(step); + } + } + + /// + /// Queues a single task + /// + [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); + } + + /// + /// Queue a 100 short tasks + /// + [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); + } + + /// + /// Queue an task with a long operation causing a timeout + /// + [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); + } + } +} diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/LanguageServer/LanguageServiceTests.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/LanguageServer/LanguageServiceTests.cs index 35214369..1a925a88 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/LanguageServer/LanguageServiceTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/LanguageServer/LanguageServiceTests.cs @@ -33,6 +33,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.LanguageServices { #region "Diagnostics tests" + /// /// Verify that the latest SqlParser (2016 as of this writing) is used by default /// @@ -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); } /// @@ -167,7 +168,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.LanguageServices /// [Fact] public async void UpdateLanguageServiceOnConnection() - { + { string ownerUri = "file://my/sample/file.sql"; var connectionService = TestObjects.GetTestConnectionService(); var connectionResult = @@ -177,7 +178,19 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.LanguageServices OwnerUri = ownerUri, Connection = TestObjects.GetTestConnectionDetails() }); - + + // set up file for returning the query + var fileMock = new Mock(); + fileMock.SetupGet(file => file.Contents).Returns(Common.StandardQuery); + fileMock.SetupGet(file => file.ClientFilePath).Returns(ownerUri); + + // set up workspace mock + var workspaceService = new Mock>(); + workspaceService.Setup(service => service.Workspace.GetFile(It.IsAny())) + .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() diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Common.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Common.cs index 1dcf28f9..78b708d9 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Common.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Common.cs @@ -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}; diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/Utility/TestObjects.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/Utility/TestObjects.cs index 3474c4df..67e48786 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/Utility/TestObjects.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/Utility/TestObjects.cs @@ -21,6 +21,8 @@ namespace Microsoft.SqlTools.Test.Utility /// public class TestObjects { + public const string ScriptUri = "file://some/file.sql"; + /// /// Creates a test connection service /// @@ -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() }; } diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/project.json b/test/Microsoft.SqlTools.ServiceLayer.Test/project.json index 5dac7a2e..ae6e3ea6 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/project.json +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/project.json @@ -9,7 +9,7 @@ "System.Runtime.Serialization.Primitives": "4.1.1", "System.Data.Common": "4.1.0", "System.Data.SqlClient": "4.1.0", - "Microsoft.SqlServer.Smo": "140.1.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",