mirror of
https://github.com/ckaczor/sqltoolsservice.git
synced 2026-01-14 09:59:48 -05:00
Update the ScriptingService to expose new scripting JSON-RPC APIs that use the SqlScriptPublishModel for script generation. The SqlScriptPublishModel is the model behind the SSMS scripting wizard. To enable scripting for CLI tools, we've ported SqlScriptPublishModel to .NET Core. The SqlScriptPublishModel wraps the SMO scripting APIs for .sql script generation. 1) Added three new requests to the ScriptingService: ScriptingRequest, ScriptingListObjectsRequest, ScriptingCancelRequest. 2) Generating scripts are long running operations, so the ScriptingRequest and ScriptingListObjectsRequest kick off a long running scripting task and return immediately. 3) Long running scripting task reports progress and completion, and can be cancelled by a ScriptingCancelRequest request. 4) Bumped the SMO nuget package to 140.17049.0. This new version contains a signed SSMS_Rel build of SMO with the SqlScriptPublishModel. 5) For testing, adding the Northwind database schema TODO (in later pull requests) 1) Integrate the new ScriptingService APIs with the ConnectionService 2) Integrate with the metadata support recently added
485 lines
17 KiB
C#
485 lines
17 KiB
C#
//
|
|
// 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.Runtime.CompilerServices;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.SqlTools.Hosting.Protocol.Contracts;
|
|
using Microsoft.SqlTools.ServiceLayer.Connection.Contracts;
|
|
using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts;
|
|
using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts;
|
|
using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts.ExecuteRequests;
|
|
using Microsoft.SqlTools.ServiceLayer.Scripting.Contracts;
|
|
using Microsoft.SqlTools.ServiceLayer.SqlContext;
|
|
using Microsoft.SqlTools.ServiceLayer.TestDriver.Driver;
|
|
using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts;
|
|
using Xunit;
|
|
|
|
namespace Microsoft.SqlTools.ServiceLayer.Test.Common
|
|
{
|
|
/// <summary>
|
|
/// Service class to execute SQL tools commands using the test driver or calling the service classed directly
|
|
/// </summary>
|
|
public sealed class TestServiceDriverProvider : IDisposable
|
|
{
|
|
private bool isRunning = false;
|
|
private TestConnectionProfileService testConnectionService;
|
|
|
|
public TestServiceDriverProvider()
|
|
{
|
|
Driver = new ServiceTestDriver();
|
|
Driver.Start().Wait();
|
|
this.isRunning = true;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (this.isRunning)
|
|
{
|
|
WaitForExit();
|
|
}
|
|
}
|
|
|
|
public void WaitForExit()
|
|
{
|
|
try
|
|
{
|
|
this.isRunning = false;
|
|
Driver.Stop().Wait();
|
|
Console.WriteLine("Successfully killed process.");
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Console.WriteLine($"Exception while waiting for service exit: {e.Message}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The driver object used to read/write data to the service
|
|
/// </summary>
|
|
public ServiceTestDriver Driver
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
public TestConnectionProfileService TestConnectionService
|
|
{
|
|
get
|
|
{
|
|
return (testConnectionService = testConnectionService ?? TestConnectionProfileService.Instance);
|
|
}
|
|
}
|
|
|
|
private object fileLock = new Object();
|
|
|
|
/// <summary>
|
|
/// Request a new connection to be created
|
|
/// </summary>
|
|
/// <returns>True if the connection completed successfully</returns>
|
|
public async Task<bool> Connect(string ownerUri, ConnectParams connectParams, int timeout = 15000)
|
|
{
|
|
connectParams.OwnerUri = ownerUri;
|
|
var connectResult = await Driver.SendRequest(ConnectionRequest.Type, connectParams);
|
|
if (connectResult)
|
|
{
|
|
var completeEvent = await Driver.WaitForEvent(ConnectionCompleteNotification.Type, timeout);
|
|
return !string.IsNullOrEmpty(completeEvent.ConnectionId);
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Request a new connection to be created for given query
|
|
/// </summary>
|
|
public async Task<bool> ConnectForQuery(TestServerType serverType, string query, string ownerUri, string databaseName = null, int timeout = 15000)
|
|
{
|
|
if (!string.IsNullOrEmpty(query))
|
|
{
|
|
WriteToFile(ownerUri, query);
|
|
}
|
|
|
|
return await Connect(serverType, ownerUri, databaseName, timeout);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Request a new connection to be created for url
|
|
/// </summary>
|
|
public async Task<bool> Connect(TestServerType serverType, string ownerUri, string databaseName = null, int timeout = 15000)
|
|
{
|
|
|
|
var connectParams = GetConnectionParameters(serverType, databaseName);
|
|
|
|
bool connected = await Connect(ownerUri, connectParams, timeout);
|
|
Assert.True(connected, "Connection is successful");
|
|
Console.WriteLine($"Connection to {connectParams.Connection.ServerName} is successful");
|
|
|
|
return connected;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Request a disconnect
|
|
/// </summary>
|
|
public async Task<bool> Disconnect(string ownerUri)
|
|
{
|
|
var disconnectParams = new DisconnectParams();
|
|
disconnectParams.OwnerUri = ownerUri;
|
|
|
|
var disconnectResult = await Driver.SendRequest(DisconnectRequest.Type, disconnectParams);
|
|
return disconnectResult;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Request a cancel connect
|
|
/// </summary>
|
|
public async Task<bool> CancelConnect(string ownerUri)
|
|
{
|
|
var cancelParams = new CancelConnectParams();
|
|
cancelParams.OwnerUri = ownerUri;
|
|
|
|
return await Driver.SendRequest(CancelConnectRequest.Type, cancelParams);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Request a cancel connect
|
|
/// </summary>
|
|
public async Task<ListDatabasesResponse> ListDatabases(string ownerUri)
|
|
{
|
|
var listParams = new ListDatabasesParams();
|
|
listParams.OwnerUri = ownerUri;
|
|
|
|
return await Driver.SendRequest(ListDatabasesRequest.Type, listParams);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Request the active SQL script is parsed for errors
|
|
/// </summary>
|
|
public async Task<SubsetResult> RequestQueryExecuteSubset(SubsetParams subsetParams)
|
|
{
|
|
return await Driver.SendRequest(SubsetRequest.Type, subsetParams);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Request the active SQL script is parsed for errors
|
|
/// </summary>
|
|
public async Task RequestOpenDocumentNotification(DidOpenTextDocumentNotification openParams)
|
|
{
|
|
await Driver.SendEvent(DidOpenTextDocumentNotification.Type, openParams);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Request a configuration change notification
|
|
/// </summary>
|
|
public async Task RequestChangeConfigurationNotification(DidChangeConfigurationParams<SqlToolsSettings> configParams)
|
|
{
|
|
await Driver.SendEvent(DidChangeConfigurationNotification<SqlToolsSettings>.Type, configParams);
|
|
}
|
|
|
|
/// <summary>
|
|
/// /// Request the active SQL script is parsed for errors
|
|
/// </summary>
|
|
public async Task RequestChangeTextDocumentNotification(DidChangeTextDocumentParams changeParams)
|
|
{
|
|
await Driver.SendEvent(DidChangeTextDocumentNotification.Type, changeParams);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Request completion item resolve to look-up additional info
|
|
/// </summary>
|
|
public async Task<CompletionItem> RequestResolveCompletion(CompletionItem item)
|
|
{
|
|
var result = await Driver.SendRequest(CompletionResolveRequest.Type, item);
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns database connection parameters for given server type
|
|
/// </summary>
|
|
public ConnectParams GetConnectionParameters(TestServerType serverType, string databaseName = null)
|
|
{
|
|
return TestConnectionService.GetConnectionParameters(serverType, databaseName);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Request a list of completion items for a position in a block of text
|
|
/// </summary>
|
|
public async Task<CompletionItem[]> RequestCompletion(string ownerUri, string text, int line, int character)
|
|
{
|
|
// Write the text to a backing file
|
|
lock (fileLock)
|
|
{
|
|
System.IO.File.WriteAllText(ownerUri, text);
|
|
}
|
|
|
|
var completionParams = new TextDocumentPosition();
|
|
completionParams.TextDocument = new TextDocumentIdentifier();
|
|
completionParams.TextDocument.Uri = ownerUri;
|
|
completionParams.Position = new Position();
|
|
completionParams.Position.Line = line;
|
|
completionParams.Position.Character = character;
|
|
|
|
var result = await Driver.SendRequest(CompletionRequest.Type, completionParams);
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Request a a hover tooltop
|
|
/// </summary>
|
|
public async Task<Hover> RequestHover(string ownerUri, string text, int line, int character)
|
|
{
|
|
// Write the text to a backing file
|
|
lock (fileLock)
|
|
{
|
|
System.IO.File.WriteAllText(ownerUri, text);
|
|
}
|
|
|
|
var completionParams = new TextDocumentPosition
|
|
{
|
|
TextDocument = new TextDocumentIdentifier { Uri = ownerUri },
|
|
Position = new Position
|
|
{
|
|
Line = line,
|
|
Character = character
|
|
}
|
|
};
|
|
|
|
var result = await Driver.SendRequest(HoverRequest.Type, completionParams);
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Request definition( peek definition/go to definition) for a sql object in a sql string
|
|
/// </summary>
|
|
public async Task<Location[]> RequestDefinition(string ownerUri, string text, int line, int character)
|
|
{
|
|
// Write the text to a backing file
|
|
lock (fileLock)
|
|
{
|
|
System.IO.File.WriteAllText(ownerUri, text);
|
|
}
|
|
|
|
var definitionParams = new TextDocumentPosition();
|
|
definitionParams.TextDocument = new TextDocumentIdentifier();
|
|
definitionParams.TextDocument.Uri = ownerUri;
|
|
definitionParams.Position = new Position();
|
|
definitionParams.Position.Line = line;
|
|
definitionParams.Position.Character = character;
|
|
|
|
// Send definition request
|
|
var result = await Driver.SendRequest(DefinitionRequest.Type, definitionParams);
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Run a query using a given connection bound to a URI
|
|
/// </summary>
|
|
public async Task<QueryCompleteParams> RunQueryAndWaitToComplete(string ownerUri, string query, int timeoutMilliseconds = 5000)
|
|
{
|
|
// Write the query text to a backing file
|
|
WriteToFile(ownerUri, query);
|
|
|
|
var queryParams = new ExecuteDocumentSelectionParams
|
|
{
|
|
OwnerUri = ownerUri,
|
|
QuerySelection = null
|
|
};
|
|
|
|
var result = await Driver.SendRequest(ExecuteDocumentSelectionRequest.Type, queryParams);
|
|
if (result != null)
|
|
{
|
|
var eventResult = await Driver.WaitForEvent(QueryCompleteEvent.Type, timeoutMilliseconds);
|
|
return eventResult;
|
|
}
|
|
else
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Run a query using a given connection bound to a URI. This method only waits for the initial response from query
|
|
/// execution (QueryExecuteResult). It is up to the caller to wait for the QueryCompleteEvent if they are interested.
|
|
/// </summary>
|
|
public async Task<ExecuteRequestResult> RunQueryAsync(string ownerUri, string query, int timeoutMilliseconds = 5000)
|
|
{
|
|
WriteToFile(ownerUri, query);
|
|
|
|
var queryParams = new ExecuteDocumentSelectionParams
|
|
{
|
|
OwnerUri = ownerUri,
|
|
QuerySelection = null
|
|
};
|
|
|
|
return await Driver.SendRequest(ExecuteDocumentSelectionRequest.Type, queryParams);
|
|
}
|
|
|
|
public async Task RunQuery(TestServerType serverType, string databaseName, string query)
|
|
{
|
|
using (SelfCleaningTempFile queryTempFile = new SelfCleaningTempFile())
|
|
{
|
|
await ConnectForQuery(serverType, query, queryTempFile.FilePath, databaseName);
|
|
var queryResult = await CalculateRunTime(() => RunQueryAndWaitToComplete(queryTempFile.FilePath, query, 50000), false);
|
|
Assert.NotNull(queryResult);
|
|
Assert.NotNull(queryResult.BatchSummaries);
|
|
|
|
await Disconnect(queryTempFile.FilePath);
|
|
}
|
|
}
|
|
|
|
public async Task<T> CalculateRunTime<T>(Func<Task<T>> testToRun, bool printResult, [CallerMemberName] string testName = "")
|
|
{
|
|
TestTimer timer = new TestTimer() { PrintResult = printResult };
|
|
T result = await testToRun();
|
|
timer.EndAndPrint(testName);
|
|
|
|
return result;
|
|
}
|
|
|
|
public async Task ExecuteWithTimeout(TestTimer timer, int timeout, Func<Task<bool>> repeatedCode,
|
|
TimeSpan? delay = null, [CallerMemberName] string testName = "")
|
|
{
|
|
while (true)
|
|
{
|
|
if (await repeatedCode())
|
|
{
|
|
timer.EndAndPrint(testName);
|
|
break;
|
|
}
|
|
if (timer.TotalMilliSecondsUntilNow >= timeout)
|
|
{
|
|
Assert.True(false, $"{testName} timed out after {timeout} milliseconds");
|
|
break;
|
|
}
|
|
if (delay.HasValue)
|
|
{
|
|
await Task.Delay(delay.Value);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Request to cancel an executing query
|
|
/// </summary>
|
|
public async Task<QueryCancelResult> CancelQuery(string ownerUri)
|
|
{
|
|
var cancelParams = new QueryCancelParams { OwnerUri = ownerUri };
|
|
|
|
var result = await Driver.SendRequest(QueryCancelRequest.Type, cancelParams);
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Request to save query results as CSV
|
|
/// </summary>
|
|
public async Task<SaveResultRequestResult> SaveAsCsv(string ownerUri, string filename, int batchIndex, int resultSetIndex)
|
|
{
|
|
var saveParams = new SaveResultsAsCsvRequestParams
|
|
{
|
|
OwnerUri = ownerUri,
|
|
BatchIndex = batchIndex,
|
|
ResultSetIndex = resultSetIndex,
|
|
FilePath = filename
|
|
};
|
|
|
|
var result = await Driver.SendRequest(SaveResultsAsCsvRequest.Type, saveParams);
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Request to save query results as JSON
|
|
/// </summary>
|
|
public async Task<SaveResultRequestResult> SaveAsJson(string ownerUri, string filename, int batchIndex, int resultSetIndex)
|
|
{
|
|
var saveParams = new SaveResultsAsJsonRequestParams
|
|
{
|
|
OwnerUri = ownerUri,
|
|
BatchIndex = batchIndex,
|
|
ResultSetIndex = resultSetIndex,
|
|
FilePath = filename
|
|
};
|
|
|
|
var result = await Driver.SendRequest(SaveResultsAsJsonRequest.Type, saveParams);
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Request a subset of results from a query
|
|
/// </summary>
|
|
public async Task<SubsetResult> ExecuteSubset(string ownerUri, int batchIndex, int resultSetIndex, int rowStartIndex, int rowCount)
|
|
{
|
|
var subsetParams = new SubsetParams();
|
|
subsetParams.OwnerUri = ownerUri;
|
|
subsetParams.BatchIndex = batchIndex;
|
|
subsetParams.ResultSetIndex = resultSetIndex;
|
|
subsetParams.RowsStartIndex = rowStartIndex;
|
|
subsetParams.RowsCount = rowCount;
|
|
|
|
var result = await Driver.SendRequest(SubsetRequest.Type, subsetParams);
|
|
return result;
|
|
}
|
|
|
|
public async Task<ScriptingListObjectsResult> ListScriptingObjects(ScriptingListObjectsParams parameters)
|
|
{
|
|
return await Driver.SendRequest(ScriptingListObjectsRequest.Type, parameters);
|
|
}
|
|
|
|
public async Task<ScriptingResult> Script(ScriptingParams parameters)
|
|
{
|
|
return await Driver.SendRequest(ScriptingRequest.Type, parameters);
|
|
}
|
|
|
|
public async Task<ScriptingCancelResult> CancelScript(string operationId)
|
|
{
|
|
return await Driver.SendRequest(ScriptingCancelRequest.Type, new ScriptingCancelParams { OperationId = operationId });
|
|
}
|
|
|
|
/// <summary>
|
|
/// Waits for a message to be returned by the service
|
|
/// </summary>
|
|
/// <returns>A message from the service layer</returns>
|
|
public async Task<MessageParams> WaitForMessage()
|
|
{
|
|
return await Driver.WaitForEvent(MessageEvent.Type);
|
|
}
|
|
|
|
public void WriteToFile(string ownerUri, string query)
|
|
{
|
|
lock (fileLock)
|
|
{
|
|
System.IO.File.WriteAllText(ownerUri, query);
|
|
}
|
|
}
|
|
|
|
public bool TryGetEvent<T>(EventType<T> eventType, out T value)
|
|
{
|
|
value = default(T);
|
|
|
|
try
|
|
{
|
|
Task<T> t = this.Driver.WaitForEvent(eventType, TimeSpan.Zero);
|
|
value = t.Result;
|
|
return true;
|
|
}
|
|
catch (Exception)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public void AssertEventNotQueued<T>(EventType<T> eventType)
|
|
{
|
|
T temp;
|
|
if (TryGetEvent(eventType, out temp))
|
|
{
|
|
Assert.True(false, string.Format("Event of type {0} was found in the queue.", eventType.GetType().FullName, temp.ToString()));
|
|
}
|
|
}
|
|
}
|
|
}
|