mirror of
https://github.com/ckaczor/sqltoolsservice.git
synced 2026-02-04 17:24:56 -05:00
Merge pull request #25 from Microsoft/feature/queryBatchProcessing
Feature: Batch Separator Support for Query Execution
This commit is contained in:
263
src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Batch.cs
Normal file
263
src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Batch.cs
Normal file
@@ -0,0 +1,263 @@
|
||||
//
|
||||
// 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.Data;
|
||||
using System.Data.Common;
|
||||
using System.Data.SqlClient;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
{
|
||||
/// <summary>
|
||||
/// This class represents a batch within a query
|
||||
/// </summary>
|
||||
public class Batch
|
||||
{
|
||||
private const string RowsAffectedFormat = "({0} row(s) affected)";
|
||||
|
||||
#region Properties
|
||||
/// <summary>
|
||||
/// The text of batch that will be executed
|
||||
/// </summary>
|
||||
public string BatchText { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not this batch has an error
|
||||
/// </summary>
|
||||
public bool HasError { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not this batch has been executed, regardless of success or failure
|
||||
/// </summary>
|
||||
public bool HasExecuted { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Internal representation of the messages so we can modify internally
|
||||
/// </summary>
|
||||
private List<string> resultMessages;
|
||||
|
||||
/// <summary>
|
||||
/// Messages that have come back from the server
|
||||
/// </summary>
|
||||
public IEnumerable<string> ResultMessages
|
||||
{
|
||||
get { return resultMessages; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal representation of the result sets so we can modify internally
|
||||
/// </summary>
|
||||
private List<ResultSet> resultSets;
|
||||
|
||||
/// <summary>
|
||||
/// The result sets of the batch execution
|
||||
/// </summary>
|
||||
public IEnumerable<ResultSet> ResultSets
|
||||
{
|
||||
get { return resultSets; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Property for generating a set result set summaries from the result sets
|
||||
/// </summary>
|
||||
public ResultSetSummary[] ResultSummaries
|
||||
{
|
||||
get
|
||||
{
|
||||
return ResultSets.Select((set, index) => new ResultSetSummary()
|
||||
{
|
||||
ColumnInfo = set.Columns,
|
||||
Id = index,
|
||||
RowCount = set.Rows.Count
|
||||
}).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The 0-indexed line number that this batch started on
|
||||
/// </summary>
|
||||
internal int StartLine { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
public Batch(string batchText, int startLine)
|
||||
{
|
||||
// Sanity check for input
|
||||
if (string.IsNullOrEmpty(batchText))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(batchText), "Query text cannot be null");
|
||||
}
|
||||
|
||||
// Initialize the internal state
|
||||
BatchText = batchText;
|
||||
StartLine = startLine - 1; // -1 to make sure that the line number of the batch is 0-indexed, since SqlParser gives 1-indexed line numbers
|
||||
HasExecuted = false;
|
||||
resultSets = new List<ResultSet>();
|
||||
resultMessages = new List<string>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes this batch and captures any server messages that are returned.
|
||||
/// </summary>
|
||||
/// <param name="conn">The connection to use to execute the batch</param>
|
||||
/// <param name="cancellationToken">Token for cancelling the execution</param>
|
||||
public async Task Execute(DbConnection conn, CancellationToken cancellationToken)
|
||||
{
|
||||
// Sanity check to make sure we haven't already run this batch
|
||||
if (HasExecuted)
|
||||
{
|
||||
throw new InvalidOperationException("Batch has already executed.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Register the message listener to *this instance* of the batch
|
||||
// Note: This is being done to associate messages with batches
|
||||
SqlConnection sqlConn = conn as SqlConnection;
|
||||
if (sqlConn != null)
|
||||
{
|
||||
sqlConn.InfoMessage += StoreDbMessage;
|
||||
}
|
||||
|
||||
// Create a command that we'll use for executing the query
|
||||
using (DbCommand command = conn.CreateCommand())
|
||||
{
|
||||
command.CommandText = BatchText;
|
||||
command.CommandType = CommandType.Text;
|
||||
|
||||
// Execute the command to get back a reader
|
||||
using (DbDataReader reader = await command.ExecuteReaderAsync(cancellationToken))
|
||||
{
|
||||
do
|
||||
{
|
||||
// Skip this result set if there aren't any rows
|
||||
if (!reader.HasRows && reader.FieldCount == 0)
|
||||
{
|
||||
// Create a message with the number of affected rows -- IF the query affects rows
|
||||
resultMessages.Add(reader.RecordsAffected >= 0
|
||||
? string.Format(RowsAffectedFormat, reader.RecordsAffected)
|
||||
: "Command(s) completed successfully.");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Read until we hit the end of the result set
|
||||
ResultSet resultSet = new ResultSet();
|
||||
while (await reader.ReadAsync(cancellationToken))
|
||||
{
|
||||
resultSet.AddRow(reader);
|
||||
}
|
||||
|
||||
// Read off the column schema information
|
||||
if (reader.CanGetColumnSchema())
|
||||
{
|
||||
resultSet.Columns = reader.GetColumnSchema().ToArray();
|
||||
}
|
||||
|
||||
// Add the result set to the results of the query
|
||||
resultSets.Add(resultSet);
|
||||
|
||||
// Add a message for the number of rows the query returned
|
||||
resultMessages.Add(string.Format(RowsAffectedFormat, resultSet.Rows.Count));
|
||||
} while (await reader.NextResultAsync(cancellationToken));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (DbException dbe)
|
||||
{
|
||||
HasError = true;
|
||||
UnwrapDbException(dbe);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
HasError = true;
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Remove the message event handler from the connection
|
||||
SqlConnection sqlConn = conn as SqlConnection;
|
||||
if (sqlConn != null)
|
||||
{
|
||||
sqlConn.InfoMessage -= StoreDbMessage;
|
||||
}
|
||||
|
||||
// Mark that we have executed
|
||||
HasExecuted = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a subset of the rows from a result set of the batch
|
||||
/// </summary>
|
||||
/// <param name="resultSetIndex">The index for selecting the result set</param>
|
||||
/// <param name="startRow">The starting row of the results</param>
|
||||
/// <param name="rowCount">How many rows to retrieve</param>
|
||||
/// <returns>A subset of results</returns>
|
||||
public ResultSetSubset GetSubset(int resultSetIndex, int startRow, int rowCount)
|
||||
{
|
||||
// Sanity check to make sure we have valid numbers
|
||||
if (resultSetIndex < 0 || resultSetIndex >= resultSets.Count)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(resultSetIndex), "Result set index cannot be less than 0" +
|
||||
"or greater than the number of result sets");
|
||||
}
|
||||
|
||||
// Retrieve the result set
|
||||
return resultSets[resultSetIndex].GetSubset(startRow, rowCount);
|
||||
}
|
||||
|
||||
#region Private Helpers
|
||||
|
||||
/// <summary>
|
||||
/// Delegate handler for storing messages that are returned from the server
|
||||
/// NOTE: Only messages that are below a certain severity will be returned via this
|
||||
/// mechanism. Anything above that level will trigger an exception.
|
||||
/// </summary>
|
||||
/// <param name="sender">Object that fired the event</param>
|
||||
/// <param name="args">Arguments from the event</param>
|
||||
private void StoreDbMessage(object sender, SqlInfoMessageEventArgs args)
|
||||
{
|
||||
resultMessages.Add(args.Message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to convert a <see cref="DbException"/> to a <see cref="SqlException"/> that
|
||||
/// contains much more info about Sql Server errors. The exception is then unwrapped and
|
||||
/// messages are formatted and stored in <see cref="ResultMessages"/>. If the exception
|
||||
/// cannot be converted to SqlException, the message is written to the messages list.
|
||||
/// </summary>
|
||||
/// <param name="dbe">The exception to unwrap</param>
|
||||
private void UnwrapDbException(DbException dbe)
|
||||
{
|
||||
SqlException se = dbe as SqlException;
|
||||
if (se != null)
|
||||
{
|
||||
foreach (var error in se.Errors)
|
||||
{
|
||||
SqlError sqlError = error as SqlError;
|
||||
if (sqlError != null)
|
||||
{
|
||||
int lineNumber = sqlError.LineNumber + StartLine;
|
||||
string message = String.Format("Msg {0}, Level {1}, State {2}, Line {3}{4}{5}",
|
||||
sqlError.Number, sqlError.Class, sqlError.State, lineNumber,
|
||||
Environment.NewLine, sqlError.Message);
|
||||
resultMessages.Add(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
resultMessages.Add(dbe.Message);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// Summary of a batch within a query
|
||||
/// </summary>
|
||||
public class BatchSummary
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether or not the batch was successful. True indicates errors, false indicates success
|
||||
/// </summary>
|
||||
public bool HasError { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the result set within the query results
|
||||
/// </summary>
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Any messages that came back from the server during execution of the batch
|
||||
/// </summary>
|
||||
public string[] Messages { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The summaries of the result sets inside the batch
|
||||
/// </summary>
|
||||
public ResultSetSummary[] ResultSetSummaries { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -17,20 +17,10 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts
|
||||
/// </summary>
|
||||
public string OwnerUri { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Any messages that came back from the server during execution of the query
|
||||
/// </summary>
|
||||
public string[] Messages { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not the query was successful. True indicates errors, false indicates success
|
||||
/// </summary>
|
||||
public bool HasError { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Summaries of the result sets that were returned with the query
|
||||
/// </summary>
|
||||
public ResultSetSummary[] ResultSetSummaries { get; set; }
|
||||
public BatchSummary[] BatchSummaries { get; set; }
|
||||
}
|
||||
|
||||
public class QueryExecuteCompleteEvent
|
||||
|
||||
@@ -17,6 +17,11 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts
|
||||
/// </summary>
|
||||
public string OwnerUri { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Index of the batch to get the results from
|
||||
/// </summary>
|
||||
public int BatchIndex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Index of the result set to get the results from
|
||||
/// </summary>
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts
|
||||
public class ResultSetSummary
|
||||
{
|
||||
/// <summary>
|
||||
/// The ID of the result set within the query results
|
||||
/// The ID of the result set within the batch results
|
||||
/// </summary>
|
||||
public int Id { get; set; }
|
||||
|
||||
|
||||
@@ -4,15 +4,14 @@
|
||||
//
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Data.Common;
|
||||
using System.Data.SqlClient;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.SqlServer.Management.SqlParser.Parser;
|
||||
using Microsoft.SqlTools.ServiceLayer.Connection;
|
||||
using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts;
|
||||
using Microsoft.SqlTools.ServiceLayer.SqlContext;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
{
|
||||
@@ -21,10 +20,35 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
/// </summary>
|
||||
public class Query : IDisposable
|
||||
{
|
||||
private const string RowsAffectedFormat = "({0} row(s) affected)";
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// The batches underneath this query
|
||||
/// </summary>
|
||||
internal Batch[] Batches { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The summaries of the batches underneath this query
|
||||
/// </summary>
|
||||
public BatchSummary[] BatchSummaries
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!HasExecuted)
|
||||
{
|
||||
throw new InvalidOperationException("Query has not been executed.");
|
||||
}
|
||||
|
||||
return Batches.Select((batch, index) => new BatchSummary
|
||||
{
|
||||
Id = index,
|
||||
HasError = batch.HasError,
|
||||
Messages = batch.ResultMessages.ToArray(),
|
||||
ResultSetSummaries = batch.ResultSummaries
|
||||
}).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancellation token source, used for cancelling async db actions
|
||||
/// </summary>
|
||||
@@ -34,49 +58,34 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
/// The connection info associated with the file editor owner URI, used to create a new
|
||||
/// connection upon execution of the query
|
||||
/// </summary>
|
||||
public ConnectionInfo EditorConnection { get; set; }
|
||||
private ConnectionInfo EditorConnection { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not the query has an error
|
||||
/// </summary>
|
||||
public bool HasError { get; set; }
|
||||
private bool HasExecuteBeenCalled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not the query has completed executed, regardless of success or failure
|
||||
/// </summary>
|
||||
public bool HasExecuted { get; set; }
|
||||
/// <remarks>
|
||||
/// Don't touch the setter unless you're doing unit tests!
|
||||
/// </remarks>
|
||||
public bool HasExecuted
|
||||
{
|
||||
get { return Batches.Length == 0 ? HasExecuteBeenCalled : Batches.All(b => b.HasExecuted); }
|
||||
internal set
|
||||
{
|
||||
HasExecuteBeenCalled = value;
|
||||
foreach (var batch in Batches)
|
||||
{
|
||||
batch.HasExecuted = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The text of the query to execute
|
||||
/// </summary>
|
||||
public string QueryText { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Messages that have come back from the server
|
||||
/// </summary>
|
||||
public List<string> ResultMessages { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The result sets of the query execution
|
||||
/// </summary>
|
||||
public List<ResultSet> ResultSets { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Property for generating a set result set summaries from the result sets
|
||||
/// </summary>
|
||||
public ResultSetSummary[] ResultSummary
|
||||
{
|
||||
get
|
||||
{
|
||||
return ResultSets.Select((set, index) => new ResultSetSummary
|
||||
{
|
||||
ColumnInfo = set.Columns,
|
||||
Id = index,
|
||||
RowCount = set.Rows.Count
|
||||
}).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
@@ -84,10 +93,11 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
/// </summary>
|
||||
/// <param name="queryText">The text of the query to execute</param>
|
||||
/// <param name="connection">The information of the connection to use to execute the query</param>
|
||||
public Query(string queryText, ConnectionInfo connection)
|
||||
/// <param name="settings">Settings for how to execute the query, from the user</param>
|
||||
public Query(string queryText, ConnectionInfo connection, QueryExecutionSettings settings)
|
||||
{
|
||||
// Sanity check for input
|
||||
if (String.IsNullOrEmpty(queryText))
|
||||
if (string.IsNullOrEmpty(queryText))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(queryText), "Query text cannot be null");
|
||||
}
|
||||
@@ -95,14 +105,24 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
{
|
||||
throw new ArgumentNullException(nameof(connection), "Connection cannot be null");
|
||||
}
|
||||
if (settings == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(settings), "Settings cannot be null");
|
||||
}
|
||||
|
||||
// Initialize the internal state
|
||||
QueryText = queryText;
|
||||
EditorConnection = connection;
|
||||
HasExecuted = false;
|
||||
ResultSets = new List<ResultSet>();
|
||||
ResultMessages = new List<string>();
|
||||
cancellationSource = new CancellationTokenSource();
|
||||
|
||||
// Process the query into batches
|
||||
ParseResult parseResult = Parser.Parse(queryText, new ParseOptions
|
||||
{
|
||||
BatchSeparator = settings.BatchSeparator
|
||||
});
|
||||
// NOTE: We only want to process batches that have statements (ie, ignore comments and empty lines)
|
||||
Batches = parseResult.Script.Batches.Where(b => b.Statements.Count > 0)
|
||||
.Select(b => new Batch(b.Sql, b.StartLocation.LineNumber)).ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -110,98 +130,38 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
/// </summary>
|
||||
public async Task Execute()
|
||||
{
|
||||
// Sanity check to make sure we haven't already run this query
|
||||
if (HasExecuted)
|
||||
// Mark that we've internally executed
|
||||
HasExecuteBeenCalled = true;
|
||||
|
||||
// Don't actually execute if there aren't any batches to execute
|
||||
if (Batches.Length == 0)
|
||||
{
|
||||
throw new InvalidOperationException("Query has already executed.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a connection from the connection details
|
||||
try
|
||||
// Open up a connection for querying the database
|
||||
string connectionString = ConnectionService.BuildConnectionString(EditorConnection.ConnectionDetails);
|
||||
using (DbConnection conn = EditorConnection.Factory.CreateSqlConnection(connectionString))
|
||||
{
|
||||
string connectionString = ConnectionService.BuildConnectionString(EditorConnection.ConnectionDetails);
|
||||
using (DbConnection conn = EditorConnection.Factory.CreateSqlConnection(connectionString))
|
||||
await conn.OpenAsync();
|
||||
|
||||
// We need these to execute synchronously, otherwise the user will be very unhappy
|
||||
foreach (Batch b in Batches)
|
||||
{
|
||||
// If we have the message listener, bind to it
|
||||
SqlConnection sqlConn = conn as SqlConnection;
|
||||
if (sqlConn != null)
|
||||
{
|
||||
sqlConn.InfoMessage += StoreDbMessage;
|
||||
}
|
||||
|
||||
await conn.OpenAsync(cancellationSource.Token);
|
||||
|
||||
// Create a command that we'll use for executing the query
|
||||
using (DbCommand command = conn.CreateCommand())
|
||||
{
|
||||
command.CommandText = QueryText;
|
||||
command.CommandType = CommandType.Text;
|
||||
|
||||
// Execute the command to get back a reader
|
||||
using (DbDataReader reader = await command.ExecuteReaderAsync(cancellationSource.Token))
|
||||
{
|
||||
do
|
||||
{
|
||||
// Skip this result set if there aren't any rows
|
||||
if (!reader.HasRows && reader.FieldCount == 0)
|
||||
{
|
||||
// Create a message with the number of affected rows -- IF the query affects rows
|
||||
ResultMessages.Add(reader.RecordsAffected >= 0
|
||||
? string.Format(RowsAffectedFormat, reader.RecordsAffected)
|
||||
: "Command Executed Successfully");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Read until we hit the end of the result set
|
||||
ResultSet resultSet = new ResultSet();
|
||||
while (await reader.ReadAsync(cancellationSource.Token))
|
||||
{
|
||||
resultSet.AddRow(reader);
|
||||
}
|
||||
|
||||
// Read off the column schema information
|
||||
if (reader.CanGetColumnSchema())
|
||||
{
|
||||
resultSet.Columns = reader.GetColumnSchema().ToArray();
|
||||
}
|
||||
|
||||
// Add the result set to the results of the query
|
||||
ResultSets.Add(resultSet);
|
||||
|
||||
// Add a message for the number of rows the query returned
|
||||
ResultMessages.Add(string.Format(RowsAffectedFormat, resultSet.Rows.Count));
|
||||
|
||||
} while (await reader.NextResultAsync(cancellationSource.Token));
|
||||
}
|
||||
}
|
||||
await b.Execute(conn, cancellationSource.Token);
|
||||
}
|
||||
}
|
||||
catch (DbException dbe)
|
||||
{
|
||||
HasError = true;
|
||||
UnwrapDbException(dbe);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
HasError = true;
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Mark that we have executed
|
||||
HasExecuted = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a subset of the result sets
|
||||
/// </summary>
|
||||
/// <param name="batchIndex">The index for selecting the batch item</param>
|
||||
/// <param name="resultSetIndex">The index for selecting the result set</param>
|
||||
/// <param name="startRow">The starting row of the results</param>
|
||||
/// <param name="rowCount">How many rows to retrieve</param>
|
||||
/// <returns>A subset of results</returns>
|
||||
public ResultSetSubset GetSubset(int resultSetIndex, int startRow, int rowCount)
|
||||
public ResultSetSubset GetSubset(int batchIndex, int resultSetIndex, int startRow, int rowCount)
|
||||
{
|
||||
// Sanity check that the results are available
|
||||
if (!HasExecuted)
|
||||
@@ -209,30 +169,14 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
throw new InvalidOperationException("The query has not completed, yet.");
|
||||
}
|
||||
|
||||
// Sanity check to make sure we have valid numbers
|
||||
if (resultSetIndex < 0 || resultSetIndex >= ResultSets.Count)
|
||||
// Sanity check to make sure that the batch is within bounds
|
||||
if (batchIndex < 0 || batchIndex >= Batches.Length)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(resultSetIndex), "Result set index cannot be less than 0" +
|
||||
throw new ArgumentOutOfRangeException(nameof(batchIndex), "Result set index cannot be less than 0" +
|
||||
"or greater than the number of result sets");
|
||||
}
|
||||
ResultSet targetResultSet = ResultSets[resultSetIndex];
|
||||
if (startRow < 0 || startRow >= targetResultSet.Rows.Count)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(startRow), "Start row cannot be less than 0 " +
|
||||
"or greater than the number of rows in the resultset");
|
||||
}
|
||||
if (rowCount <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(rowCount), "Row count must be a positive integer");
|
||||
}
|
||||
|
||||
// Retrieve the subset of the results as per the request
|
||||
object[][] rows = targetResultSet.Rows.Skip(startRow).Take(rowCount).ToArray();
|
||||
return new ResultSetSubset
|
||||
{
|
||||
Rows = rows,
|
||||
RowCount = rows.Length
|
||||
};
|
||||
return Batches[batchIndex].GetSubset(resultSetIndex, startRow, rowCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -250,48 +194,6 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
cancellationSource.Cancel();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delegate handler for storing messages that are returned from the server
|
||||
/// NOTE: Only messages that are below a certain severity will be returned via this
|
||||
/// mechanism. Anything above that level will trigger an exception.
|
||||
/// </summary>
|
||||
/// <param name="sender">Object that fired the event</param>
|
||||
/// <param name="args">Arguments from the event</param>
|
||||
private void StoreDbMessage(object sender, SqlInfoMessageEventArgs args)
|
||||
{
|
||||
ResultMessages.Add(args.Message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to convert a <see cref="DbException"/> to a <see cref="SqlException"/> that
|
||||
/// contains much more info about Sql Server errors. The exception is then unwrapped and
|
||||
/// messages are formatted and stored in <see cref="ResultMessages"/>. If the exception
|
||||
/// cannot be converted to SqlException, the message is written to the messages list.
|
||||
/// </summary>
|
||||
/// <param name="dbe">The exception to unwrap</param>
|
||||
private void UnwrapDbException(DbException dbe)
|
||||
{
|
||||
SqlException se = dbe as SqlException;
|
||||
if (se != null)
|
||||
{
|
||||
foreach (var error in se.Errors)
|
||||
{
|
||||
SqlError sqlError = error as SqlError;
|
||||
if (sqlError != null)
|
||||
{
|
||||
string message = String.Format("Msg {0}, Level {1}, State {2}, Line {3}{4}{5}",
|
||||
sqlError.Number, sqlError.Class, sqlError.State, sqlError.LineNumber,
|
||||
Environment.NewLine, sqlError.Message);
|
||||
ResultMessages.Add(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ResultMessages.Add(dbe.Message);
|
||||
}
|
||||
}
|
||||
|
||||
#region IDisposable Implementation
|
||||
|
||||
private bool disposed;
|
||||
|
||||
@@ -10,6 +10,8 @@ using Microsoft.SqlTools.ServiceLayer.Connection;
|
||||
using Microsoft.SqlTools.ServiceLayer.Hosting;
|
||||
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol;
|
||||
using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts;
|
||||
using Microsoft.SqlTools.ServiceLayer.SqlContext;
|
||||
using Microsoft.SqlTools.ServiceLayer.Workspace;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
{
|
||||
@@ -60,6 +62,8 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
private readonly Lazy<ConcurrentDictionary<string, Query>> queries =
|
||||
new Lazy<ConcurrentDictionary<string, Query>>(() => new ConcurrentDictionary<string, Query>());
|
||||
|
||||
private SqlToolsSettings Settings { get { return WorkspaceService<SqlToolsSettings>.Instance.CurrentSettings; } }
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
@@ -81,6 +85,13 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
Dispose();
|
||||
return Task.FromResult(0);
|
||||
});
|
||||
|
||||
// Register a handler for when the configuration changes
|
||||
WorkspaceService<SqlToolsSettings>.Instance.RegisterConfigChangeCallback((oldSettings, newSettings, eventContext) =>
|
||||
{
|
||||
Settings.QueryExecutionSettings.Update(newSettings.QueryExecutionSettings);
|
||||
return Task.FromResult(0);
|
||||
});
|
||||
}
|
||||
|
||||
#region Request Handlers
|
||||
@@ -123,7 +134,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
var result = new QueryExecuteSubsetResult
|
||||
{
|
||||
Message = null,
|
||||
ResultSubset = query.GetSubset(
|
||||
ResultSubset = query.GetSubset(subsetParams.BatchIndex,
|
||||
subsetParams.ResultSetIndex, subsetParams.RowsStartIndex, subsetParams.RowsCount)
|
||||
};
|
||||
await requestContext.SendResult(result);
|
||||
@@ -251,8 +262,11 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
ActiveQueries.TryRemove(executeParams.OwnerUri, out oldQuery);
|
||||
}
|
||||
|
||||
// Retrieve the current settings for executing the query with
|
||||
QueryExecutionSettings settings = WorkspaceService<SqlToolsSettings>.Instance.CurrentSettings.QueryExecutionSettings;
|
||||
|
||||
// If we can't add the query now, it's assumed the query is in progress
|
||||
Query newQuery = new Query(executeParams.QueryText, connectionInfo);
|
||||
Query newQuery = new Query(executeParams.QueryText, connectionInfo, settings);
|
||||
if (!ActiveQueries.TryAdd(executeParams.OwnerUri, newQuery))
|
||||
{
|
||||
await requestContext.SendResult(new QueryExecuteResult
|
||||
@@ -292,10 +306,8 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
await Task.WhenAll(executeTask);
|
||||
QueryExecuteCompleteParams eventParams = new QueryExecuteCompleteParams
|
||||
{
|
||||
HasError = query.HasError,
|
||||
Messages = query.ResultMessages.ToArray(),
|
||||
OwnerUri = executeParams.OwnerUri,
|
||||
ResultSetSummaries = query.ResultSummary
|
||||
BatchSummaries = query.BatchSummaries
|
||||
};
|
||||
await requestContext.SendEvent(QueryExecuteCompleteEvent.Type, eventParams);
|
||||
}
|
||||
|
||||
@@ -3,8 +3,11 @@
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data.Common;
|
||||
using System.Linq;
|
||||
using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
{
|
||||
@@ -33,5 +36,33 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
}
|
||||
Rows.Add(row.ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a subset of the rows from the result set
|
||||
/// </summary>
|
||||
/// <param name="startRow">The starting row of the results</param>
|
||||
/// <param name="rowCount">How many rows to retrieve</param>
|
||||
/// <returns>A subset of results</returns>
|
||||
public ResultSetSubset GetSubset(int startRow, int rowCount)
|
||||
{
|
||||
// Sanity check to make sure that the row and the row count are within bounds
|
||||
if (startRow < 0 || startRow >= Rows.Count)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(startRow), "Start row cannot be less than 0 " +
|
||||
"or greater than the number of rows in the resultset");
|
||||
}
|
||||
if (rowCount <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(rowCount), "Row count must be a positive integer");
|
||||
}
|
||||
|
||||
// Retrieve the subset of the results as per the request
|
||||
object[][] rows = Rows.Skip(startRow).Take(rowCount).ToArray();
|
||||
return new ResultSetSubset
|
||||
{
|
||||
Rows = rows,
|
||||
RowCount = rows.Length
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.SqlContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Collection of settings related to the execution of queries
|
||||
/// </summary>
|
||||
public class QueryExecutionSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// Default value for batch separator (de facto standard as per SSMS)
|
||||
/// </summary>
|
||||
private const string DefaultBatchSeparator = "GO";
|
||||
|
||||
private string batchSeparator;
|
||||
|
||||
/// <summary>
|
||||
/// The configured batch separator, will use a default if a value was not configured
|
||||
/// </summary>
|
||||
public string BatchSeparator
|
||||
{
|
||||
get { return batchSeparator ?? DefaultBatchSeparator; }
|
||||
set { batchSeparator = value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the current settings with the new settings
|
||||
/// </summary>
|
||||
/// <param name="newSettings">The new settings</param>
|
||||
public void Update(QueryExecutionSettings newSettings)
|
||||
{
|
||||
BatchSeparator = newSettings.BatchSeparator;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ namespace Microsoft.SqlTools.ServiceLayer.SqlContext
|
||||
public SqlToolsSettings()
|
||||
{
|
||||
this.ScriptAnalysis = new ScriptAnalysisSettings();
|
||||
this.QueryExecutionSettings = new QueryExecutionSettings();
|
||||
}
|
||||
|
||||
public bool EnableProfileLoading { get; set; }
|
||||
@@ -31,6 +32,8 @@ namespace Microsoft.SqlTools.ServiceLayer.SqlContext
|
||||
this.ScriptAnalysis.Update(settings.ScriptAnalysis, workspaceRootPath);
|
||||
}
|
||||
}
|
||||
|
||||
public QueryExecutionSettings QueryExecutionSettings { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -44,6 +44,8 @@ namespace Microsoft.SqlTools.ServiceLayer.Workspace
|
||||
ConfigChangeCallbacks = new List<ConfigChangeCallback>();
|
||||
TextDocChangeCallbacks = new List<TextDocChangeCallback>();
|
||||
TextDocOpenCallbacks = new List<TextDocOpenCallback>();
|
||||
|
||||
CurrentSettings = new TConfig();
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -52,7 +54,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Workspace
|
||||
|
||||
public Workspace Workspace { get; private set; }
|
||||
|
||||
public TConfig CurrentSettings { get; private set; }
|
||||
public TConfig CurrentSettings { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Delegate for callbacks that occur when the configuration for the workspace changes
|
||||
@@ -101,7 +103,6 @@ namespace Microsoft.SqlTools.ServiceLayer.Workspace
|
||||
{
|
||||
// Create a workspace that will handle state for the session
|
||||
Workspace = new Workspace();
|
||||
CurrentSettings = new TConfig();
|
||||
|
||||
// Register the handlers for when changes to the workspae occur
|
||||
serviceHost.SetEventHandler(DidChangeTextDocumentNotification.Type, HandleDidChangeTextDocumentNotification);
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"Newtonsoft.Json": "9.0.1",
|
||||
"Microsoft.SqlServer.SqlParser": "140.1.4",
|
||||
"Microsoft.SqlServer.SqlParser": "140.1.5",
|
||||
"System.Data.Common": "4.1.0",
|
||||
"System.Data.SqlClient": "4.1.0"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user