mirror of
https://github.com/ckaczor/sqltoolsservice.git
synced 2026-01-24 01:25:42 -05:00
Added new Kusto ServiceLayer (#1009)
* Copy smoModel some rename * Copy entire service layer * Building copy * Fixing some references * Launch profile * Resolve namespace issues * Compiling tests. Correct manifest. * Fixing localization resources * ReliableKustoClient * Some trimming of extra code and Kusto code * Kusto client creation in bindingContent * Removing Smo and new Kusto classes * More trimming * Kusto schema hookup * Solidying DataSource abstraction * Solidifying further * Latest refatoring * More refactoring * Building and launching Kusto service layer * Working model which enumerates databases * Refactoring to pass IDataSource to all tree nodes * Removing some dependencies on the context * Working with tables and schema * Comment checkin * Refactoring to give out select script * Query created and sent back to ADS * Fix query generation * Fix listing of databases * Tunneling the query through. * Successful query execution * Return only results table * Deleting Cms * Delete DacFx * Delete SchemaCompare and TaskServices * Change build definition to not stop at launch * Fix error after merge * Save Kusto results in different formats (#935) * save results as csv etc * some fixes Co-authored-by: Monica Gupta <mogupt@microsoft.com> * 2407 Added OrderBy clause in KustoDataSource > GetDatabaseMetaData and GetColumnMetadata (#959) * 2405 Defaulted Options when setting ServerInfo in ConnectionService > GetConnectionCompleteParams (#965) * 2747 Fixed IsUnknownType error for Kusto (#989) * 2747 Removed unused directives in Kusto > DbColumnWrapper. Refactored IsUnknownType to handle null DataTypeName * 2747 Reverted IsUnknownType change in DbColumnWrapper. Changed DataTypeName to get calue from ColumnType. Refactored SafeGetValue to type check before hard casting to reduce case exceptions. * Added EmbeddedResourceUseDependentUponConvention to Microsoft.Kusto.ServiceLayer.csproj. Also renamed DACfx to match Microsoft.SqlTools.ServiceLayer. Added to compile Exclude="**/obj/**/*.cs" * Srahman cleanup sql code (#992) * Removed Management and Security Service Code. * Remove FileBrowser service * Comment why we are using SqlServer library * Remove SQL specific type definitions * clean up formatter service (#996) Co-authored-by: Monica Gupta <mogupt@microsoft.com> * Code clean up and Kusto intellisense (#994) * Code clean up and Kusto intellisense * Addressed few comments * Addressed few comments * addressed comments Co-authored-by: Monica Gupta <mogupt@microsoft.com> * Return multiple tables for Kusto * Changes required for Kusto manage dashboard (#1039) * Changes required for manage dashboard * Addressed comments Co-authored-by: Monica Gupta <mogupt@microsoft.com> * 2728 Kusto function support (#1038) * loc update (#914) * loc update * loc updates * 2728 moved ColumnInfo and KustoResultsReader to separate files. Added Folder and Function to TreeNode.cs * 2728 Added FunctionInfo. Added Folder to ColumnInfo. Removed partial class from KustoResultsReader. Set Function.IsAlwaysLeaf=true in TreeNode.cs. In KustoDataSource changed tableMetadata type to TableMetaData. Added folder and function dictionaries. Refactored GetSchema function. Renamed GenerateColumnMetadataKey to GenerateMetadataKey * 2728 Added FunctionInfo. Added Folder to ColumnInfo. Removed partial class from KustoResultsReader. Set Function.IsAlwaysLeaf=true in TreeNode.cs. In KustoDataSource changed tableMetadata type to TableMetaData. Added folder and function dictionaries. Refactored GetSchema function. Renamed GenerateColumnMetadataKey to GenerateMetadataKey * 2728 Created new SqlConnection within using block. Refactored KustoDataSource > columnmetadata to sort on get instead of insert. * 2728 Added GetFunctionInfo function to KustoDataSource. * 2728 Reverted change to Microsoft.Kusto.ServiceLayer.csproj from merge * 2728 Reverted change to SqlTools.ServiceLayer\Localization\transXliff * 2728 Reverted change to sr.de.xlf and sr.zh-hans.xlf * 2728 Refactored KustoDataSource Function folders to support subfolders * 2728 Refactored KustoDataSource to use urn for folders, functions, and tables instead of name. * Merge remote-tracking branch 'origin/main' into feature-ADE # Conflicts: # Packages.props * 2728 Moved metadata files into Metadata subdirectory. Added GenerateAlterFunction to IDataSource and DataSourceBase. * 2728 Added summary information to SafeAdd in SystemExtensions. Renamed local variable in SetTableMetadata * 2728 Moved SafeAdd from SystemExtensions to KustoQueryUtils. Added check when getting database schema to return existing records before querying again. Added AddRange function to KustoQueryUtils. Created SetFolderMetadataForFunctions method. * 2728 Added DatabaseKeyPrefix to only return tables to a database for the dashboard. Added logic to store all database tables within the tableMetadata dictionary for the dashboard. * 2728 Created TableInfo and moved info objects into Models directory. Refactored KustoDataSource to lazy load columns for tables. Refactored logic to load tables using cslschema instead of schema. * 2728 Renamed LoadColumnSchema to GetTableSchema to be consistent. Co-authored-by: khoiph1 <khoiph@microsoft.com> * Addressed comments Co-authored-by: Shafiq Rahman <srahman@microsoft.com> Co-authored-by: Monica Gupta <mogupt@microsoft.com> Co-authored-by: Justin M <63619224+JustinMDotNet@users.noreply.github.com> Co-authored-by: rkselfhost <rkselfhost@outlook.com> Co-authored-by: khoiph1 <khoiph@microsoft.com>
This commit is contained in:
649
src/Microsoft.Kusto.ServiceLayer/QueryExecution/Batch.cs
Normal file
649
src/Microsoft.Kusto.ServiceLayer/QueryExecution/Batch.cs
Normal file
@@ -0,0 +1,649 @@
|
||||
//
|
||||
// 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.Diagnostics;
|
||||
using System.Data.SqlClient;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Kusto.ServiceLayer.Connection;
|
||||
using Microsoft.Kusto.ServiceLayer.QueryExecution.Contracts;
|
||||
using Microsoft.Kusto.ServiceLayer.QueryExecution.DataStorage;
|
||||
using Microsoft.SqlTools.Utility;
|
||||
using System.Globalization;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Microsoft.Kusto.ServiceLayer.QueryExecution
|
||||
{
|
||||
/// <summary>
|
||||
/// This class represents a batch within a query
|
||||
/// </summary>
|
||||
public class Batch : IDisposable
|
||||
{
|
||||
#region Member Variables
|
||||
/// <summary>
|
||||
/// For IDisposable implementation, whether or not this has been disposed
|
||||
/// </summary>
|
||||
private bool disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Local time when the execution and retrieval of files is finished
|
||||
/// </summary>
|
||||
private DateTime executionEndTime;
|
||||
|
||||
/// <summary>
|
||||
/// Local time when the execution starts, specifically when the object is created
|
||||
/// </summary>
|
||||
private DateTime executionStartTime;
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not any messages have been sent
|
||||
/// </summary>
|
||||
private bool messagesSent;
|
||||
|
||||
/// <summary>
|
||||
/// Factory for creating readers/writers for the output of the batch
|
||||
/// </summary>
|
||||
private readonly IFileStreamFactory outputFileFactory;
|
||||
|
||||
/// <summary>
|
||||
/// Internal representation of the result sets so we can modify internally
|
||||
/// </summary>
|
||||
private readonly List<ResultSet> resultSets;
|
||||
|
||||
/// <summary>
|
||||
/// Special action which this batch performed
|
||||
/// </summary>
|
||||
private readonly SpecialAction specialAction;
|
||||
|
||||
/// <summary>
|
||||
/// Flag indicating whether a separate KeyInfo query should be run
|
||||
/// to get the full ColumnSchema metadata.
|
||||
/// </summary>
|
||||
private readonly bool getFullColumnSchema;
|
||||
|
||||
#endregion
|
||||
|
||||
internal Batch(string batchText, SelectionData selection, int ordinalId,
|
||||
IFileStreamFactory outputFileFactory, int executionCount = 1, bool getFullColumnSchema = false)
|
||||
{
|
||||
// Sanity check for input
|
||||
Validate.IsNotNullOrEmptyString(nameof(batchText), batchText);
|
||||
Validate.IsNotNull(nameof(outputFileFactory), outputFileFactory);
|
||||
Validate.IsGreaterThan(nameof(ordinalId), ordinalId, 0);
|
||||
|
||||
// Initialize the internal state
|
||||
BatchText = batchText;
|
||||
Selection = selection;
|
||||
executionStartTime = DateTime.Now;
|
||||
HasExecuted = false;
|
||||
Id = ordinalId;
|
||||
resultSets = new List<ResultSet>();
|
||||
this.outputFileFactory = outputFileFactory;
|
||||
specialAction = new SpecialAction();
|
||||
BatchExecutionCount = executionCount > 0 ? executionCount : 1;
|
||||
|
||||
this.getFullColumnSchema = getFullColumnSchema;
|
||||
}
|
||||
|
||||
#region Events
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronous handler for when batches are completed
|
||||
/// </summary>
|
||||
/// <param name="batch">The batch that completed</param>
|
||||
public delegate Task BatchAsyncEventHandler(Batch batch);
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronous handler for when a message is emitted by the sql connection
|
||||
/// </summary>
|
||||
/// <param name="message">The message that was emitted</param>
|
||||
public delegate Task BatchAsyncMessageHandler(ResultMessage message);
|
||||
|
||||
/// <summary>
|
||||
/// Event that will be called when the batch has completed execution
|
||||
/// </summary>
|
||||
public event BatchAsyncEventHandler BatchCompletion;
|
||||
|
||||
/// <summary>
|
||||
/// Event that will be called when a message has been emitted
|
||||
/// </summary>
|
||||
public event BatchAsyncMessageHandler BatchMessageSent;
|
||||
|
||||
/// <summary>
|
||||
/// Event to call when the batch has started execution
|
||||
/// </summary>
|
||||
public event BatchAsyncEventHandler BatchStart;
|
||||
|
||||
/// <summary>
|
||||
/// Event that will be called when the resultset has completed execution. It will not be
|
||||
/// called from the Batch but from the ResultSet instance.
|
||||
/// </summary>
|
||||
public event ResultSet.ResultSetAsyncEventHandler ResultSetCompletion;
|
||||
|
||||
/// <summary>
|
||||
/// Event that will be called when the resultSet first becomes available. This is as soon as we start reading the results. It will not be
|
||||
/// called from the Batch but from the ResultSet instance.
|
||||
/// </summary>
|
||||
public event ResultSet.ResultSetAsyncEventHandler ResultSetAvailable;
|
||||
|
||||
/// <summary>
|
||||
/// Event that will be called when additional rows in the result set are available (rowCount available has increased). It will not be
|
||||
/// called from the Batch but from the ResultSet instance.
|
||||
/// </summary>
|
||||
public event ResultSet.ResultSetAsyncEventHandler ResultSetUpdated;
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// The text of batch that will be executed
|
||||
/// </summary>
|
||||
public string BatchText { get; set; }
|
||||
|
||||
public int BatchExecutionCount { get; private set; }
|
||||
/// <summary>
|
||||
/// Localized timestamp for when the execution completed.
|
||||
/// Stored in UTC ISO 8601 format; should be localized before displaying to any user
|
||||
/// </summary>
|
||||
public string ExecutionEndTimeStamp => executionEndTime.ToString("o");
|
||||
|
||||
/// <summary>
|
||||
/// Localized timestamp for how long it took for the execution to complete
|
||||
/// </summary>
|
||||
public string ExecutionElapsedTime
|
||||
{
|
||||
get
|
||||
{
|
||||
TimeSpan elapsedTime = executionEndTime - executionStartTime;
|
||||
return elapsedTime.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Localized timestamp for when the execution began.
|
||||
/// Stored in UTC ISO 8601 format; should be localized before displaying to any user
|
||||
/// </summary>
|
||||
public string ExecutionStartTimeStamp => executionStartTime.ToString("o");
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not this batch encountered an error that halted execution
|
||||
/// </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>
|
||||
/// Ordinal of the batch in the query
|
||||
/// </summary>
|
||||
public int Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The result sets of the batch execution
|
||||
/// </summary>
|
||||
public IList<ResultSet> ResultSets => resultSets;
|
||||
|
||||
/// <summary>
|
||||
/// Property for generating a set result set summaries from the result sets
|
||||
/// </summary>
|
||||
public ResultSetSummary[] ResultSummaries
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (resultSets)
|
||||
{
|
||||
return resultSets.Select(set => set.Summary).ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="BatchSummary"/> based on the batch instance
|
||||
/// </summary>
|
||||
public BatchSummary Summary
|
||||
{
|
||||
get
|
||||
{
|
||||
// Batch summary with information available at start
|
||||
BatchSummary summary = new BatchSummary
|
||||
{
|
||||
Id = Id,
|
||||
Selection = Selection,
|
||||
ExecutionStart = ExecutionStartTimeStamp,
|
||||
HasError = HasError
|
||||
};
|
||||
|
||||
// Add on extra details if we finished executing it
|
||||
if (HasExecuted)
|
||||
{
|
||||
summary.ResultSetSummaries = ResultSummaries;
|
||||
summary.ExecutionEnd = ExecutionEndTimeStamp;
|
||||
summary.ExecutionElapsed = ExecutionElapsedTime;
|
||||
summary.SpecialAction = ProcessResultSetSpecialActions();
|
||||
}
|
||||
|
||||
return summary;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The range from the file that is this batch
|
||||
/// </summary>
|
||||
internal SelectionData Selection { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
|
||||
/// <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(ReliableDataSourceConnection conn, CancellationToken cancellationToken)
|
||||
{
|
||||
// Sanity check to make sure we haven't already run this batch
|
||||
if (HasExecuted)
|
||||
{
|
||||
throw new InvalidOperationException("Batch has already executed.");
|
||||
}
|
||||
|
||||
// Notify that we've started execution
|
||||
if (BatchStart != null)
|
||||
{
|
||||
await BatchStart(this);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await DoExecute(conn, cancellationToken);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
// Cancellation isn't considered an error condition
|
||||
await SendMessage(SR.QueryServiceQueryCancelled, false);
|
||||
throw;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
HasError = true;
|
||||
await SendMessage(SR.QueryServiceQueryFailed(e.Message), true);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Mark that we have executed
|
||||
HasExecuted = true;
|
||||
executionEndTime = DateTime.Now;
|
||||
|
||||
// Fire an event to signify that the batch has completed
|
||||
if (BatchCompletion != null)
|
||||
{
|
||||
await BatchCompletion(this);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private async Task DoExecute(ReliableDataSourceConnection conn, CancellationToken cancellationToken)
|
||||
{
|
||||
bool canContinue = true;
|
||||
int timesLoop = this.BatchExecutionCount;
|
||||
|
||||
await SendMessageIfExecutingMultipleTimes(SR.EE_ExecutionInfo_InitializingLoop, false);
|
||||
|
||||
executionStartTime = DateTime.Now;
|
||||
|
||||
while (canContinue && timesLoop > 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ExecuteOnce(conn, cancellationToken);
|
||||
}
|
||||
catch (DbException dbe)
|
||||
{
|
||||
HasError = true;
|
||||
canContinue = await UnwrapDbException(dbe);
|
||||
if (canContinue)
|
||||
{
|
||||
// If it's a multi-batch, we notify the user that we're ignoring a single failure.
|
||||
await SendMessageIfExecutingMultipleTimes(SR.EE_BatchExecutionError_Ignoring, false);
|
||||
}
|
||||
}
|
||||
timesLoop--;
|
||||
}
|
||||
|
||||
await SendMessageIfExecutingMultipleTimes(string.Format(CultureInfo.CurrentCulture, SR.EE_ExecutionInfo_FinalizingLoop, this.BatchExecutionCount), false);
|
||||
}
|
||||
|
||||
private async Task SendMessageIfExecutingMultipleTimes(string message, bool isError)
|
||||
{
|
||||
if (IsExecutingMultipleTimes())
|
||||
{
|
||||
await SendMessage(message, isError);
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsExecutingMultipleTimes()
|
||||
{
|
||||
return this.BatchExecutionCount > 1;
|
||||
}
|
||||
|
||||
private async Task ExecuteOnce(ReliableDataSourceConnection conn, CancellationToken cancellationToken)
|
||||
{
|
||||
// Make sure we haven't cancelled yet
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
ConnectionService.EnsureConnectionIsOpen(conn);
|
||||
|
||||
// Execute the command to get back a reader
|
||||
using (IDataReader reader = await conn.GetUnderlyingConnection().ExecuteQueryAsync(BatchText, cancellationToken, conn.Database))
|
||||
{
|
||||
do
|
||||
{
|
||||
// Verify that the cancellation token hasn't been canceled
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// This resultset has results (i.e. SELECT/etc queries)
|
||||
ResultSet resultSet = new ResultSet(resultSets.Count, Id, outputFileFactory);
|
||||
resultSet.ResultAvailable += ResultSetAvailable;
|
||||
resultSet.ResultUpdated += ResultSetUpdated;
|
||||
resultSet.ResultCompletion += ResultSetCompletion;
|
||||
|
||||
// Add the result set to the results of the query
|
||||
lock (resultSets)
|
||||
{
|
||||
resultSets.Add(resultSet);
|
||||
}
|
||||
|
||||
// Read until we hit the end of the result set
|
||||
await resultSet.ReadResultToEnd(reader, cancellationToken);
|
||||
|
||||
} while (reader.NextResult());
|
||||
|
||||
// If there were no messages, for whatever reason (NO COUNT set, messages
|
||||
// were emitted, records returned), output a "successful" message
|
||||
if (!messagesSent)
|
||||
{
|
||||
await SendMessage(SR.QueryServiceCompletedSuccessfully, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <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 Task<ResultSetSubset> GetSubset(int resultSetIndex, long startRow, int rowCount)
|
||||
{
|
||||
ResultSet targetResultSet;
|
||||
lock (resultSets)
|
||||
{
|
||||
// Sanity check to make sure we have valid numbers
|
||||
if (resultSetIndex < 0 || resultSetIndex >= resultSets.Count)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(resultSetIndex),
|
||||
SR.QueryServiceSubsetResultSetOutOfRange);
|
||||
}
|
||||
|
||||
targetResultSet = resultSets[resultSetIndex];
|
||||
}
|
||||
|
||||
// Retrieve the result set
|
||||
return targetResultSet.GetSubset(startRow, rowCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates an execution plan
|
||||
/// </summary>
|
||||
/// <param name="resultSetIndex">The index for selecting the result set</param>
|
||||
/// <returns>An execution plan object</returns>
|
||||
public Task<ExecutionPlan> GetExecutionPlan(int resultSetIndex)
|
||||
{
|
||||
ResultSet targetResultSet;
|
||||
lock (resultSets)
|
||||
{
|
||||
// Sanity check to make sure we have valid numbers
|
||||
if (resultSetIndex < 0 || resultSetIndex >= resultSets.Count)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(resultSetIndex),
|
||||
SR.QueryServiceSubsetResultSetOutOfRange);
|
||||
}
|
||||
|
||||
targetResultSet = resultSets[resultSetIndex];
|
||||
}
|
||||
|
||||
// Retrieve the result set
|
||||
return targetResultSet.GetExecutionPlan();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves a result to a file format selected by the user
|
||||
/// </summary>
|
||||
/// <param name="saveParams">Parameters for the save as request</param>
|
||||
/// <param name="fileFactory">
|
||||
/// Factory for creating the reader/writer pair for outputing to the selected format
|
||||
/// </param>
|
||||
/// <param name="successHandler">Delegate to call when request successfully completes</param>
|
||||
/// <param name="failureHandler">Delegate to call if the request fails</param>
|
||||
public void SaveAs(SaveResultsRequestParams saveParams, IFileStreamFactory fileFactory,
|
||||
ResultSet.SaveAsAsyncEventHandler successHandler, ResultSet.SaveAsFailureAsyncEventHandler failureHandler)
|
||||
{
|
||||
// Get the result set to save
|
||||
ResultSet resultSet;
|
||||
lock (resultSets)
|
||||
{
|
||||
// Sanity check to make sure we have a valid result set
|
||||
if (saveParams.ResultSetIndex < 0 || saveParams.ResultSetIndex >= resultSets.Count)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(saveParams.BatchIndex), SR.QueryServiceSubsetResultSetOutOfRange);
|
||||
}
|
||||
|
||||
|
||||
resultSet = resultSets[saveParams.ResultSetIndex];
|
||||
}
|
||||
resultSet.SaveAs(saveParams, fileFactory, successHandler, failureHandler);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Helpers
|
||||
|
||||
private async Task SendMessage(string message, bool isError)
|
||||
{
|
||||
// If the message event is null, this is a no-op
|
||||
if (BatchMessageSent == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// State that we've sent any message, and send it
|
||||
messagesSent = true;
|
||||
await BatchMessageSent(new ResultMessage(message, isError, Id));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handler for when the StatementCompleted event is fired for this batch's command. This
|
||||
/// will be executed ONLY when there is a rowcount to report. If this event is not fired
|
||||
/// either NOCOUNT has been set or the command doesn't affect records.
|
||||
/// </summary>
|
||||
/// <param name="sender">Sender of the event</param>
|
||||
/// <param name="args">Arguments for the event</param>
|
||||
internal void StatementCompletedHandler(object sender, StatementCompletedEventArgs args)
|
||||
{
|
||||
// Add a message for the number of rows the query returned
|
||||
string message = args.RecordCount == 1
|
||||
? SR.QueryServiceAffectedOneRow
|
||||
: SR.QueryServiceAffectedRows(args.RecordCount);
|
||||
SendMessage(message, false).Wait();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delegate handler for storing messages that are returned from the server
|
||||
/// </summary>
|
||||
/// <param name="sender">Object that fired the event</param>
|
||||
/// <param name="args">Arguments from the event</param>
|
||||
private async void ServerMessageHandler(object sender, SqlInfoMessageEventArgs args)
|
||||
{
|
||||
foreach (SqlError error in args.Errors)
|
||||
{
|
||||
await HandleSqlErrorMessage(error.Number, error.Class, error.State, error.LineNumber, error.Procedure, error.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle a single SqlError's error message by processing and displaying it. The arguments come from the error being handled
|
||||
/// </summary>
|
||||
internal async Task HandleSqlErrorMessage(int errorNumber, byte errorClass, byte state, int lineNumber, string procedure, string message)
|
||||
{
|
||||
// Did the database context change (error code 5701)?
|
||||
if (errorNumber == 5701)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string detailedMessage;
|
||||
if (string.IsNullOrEmpty(procedure))
|
||||
{
|
||||
detailedMessage = string.Format("Msg {0}, Level {1}, State {2}, Line {3}{4}{5}",
|
||||
errorNumber, errorClass, state, lineNumber + (Selection != null ? Selection.StartLine : 0),
|
||||
Environment.NewLine, message);
|
||||
}
|
||||
else
|
||||
{
|
||||
detailedMessage = string.Format("Msg {0}, Level {1}, State {2}, Procedure {3}, Line {4}{5}{6}",
|
||||
errorNumber, errorClass, state, procedure, lineNumber,
|
||||
Environment.NewLine, message);
|
||||
}
|
||||
|
||||
bool isError;
|
||||
if (errorClass > 10)
|
||||
{
|
||||
isError = true;
|
||||
}
|
||||
else if (errorClass > 0 && errorNumber > 0)
|
||||
{
|
||||
isError = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
isError = false;
|
||||
detailedMessage = null;
|
||||
}
|
||||
|
||||
if (detailedMessage != null)
|
||||
{
|
||||
await SendMessage(detailedMessage, isError);
|
||||
}
|
||||
else
|
||||
{
|
||||
await SendMessage(message, isError);
|
||||
}
|
||||
|
||||
if (isError)
|
||||
{
|
||||
this.HasError = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to convert an <see cref="Exception"/> to a <see cref="SqlException"/> that
|
||||
/// contains much more info about Sql Server errors. The exception is then unwrapped and
|
||||
/// messages are formatted and sent to the extension. 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>
|
||||
/// <returns>true is exception can be ignored when in a loop, false otherwise</returns>
|
||||
private async Task<bool> UnwrapDbException(Exception dbe)
|
||||
{
|
||||
bool canIgnore = true;
|
||||
SqlException se = dbe as SqlException;
|
||||
if (se != null)
|
||||
{
|
||||
var errors = se.Errors.Cast<SqlError>().ToList();
|
||||
|
||||
// Detect user cancellation errors
|
||||
if (errors.Any(error => error.Class == 11 && error.Number == 0))
|
||||
{
|
||||
// User cancellation error, add the single message
|
||||
await SendMessage(SR.QueryServiceQueryCancelled, false);
|
||||
canIgnore = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Not a user cancellation error, add all
|
||||
foreach (var error in errors)
|
||||
{
|
||||
int lineNumber = error.LineNumber + (Selection != null ? Selection.StartLine : 0);
|
||||
string message = string.Format("Msg {0}, Level {1}, State {2}, Line {3}{4}{5}",
|
||||
error.Number, error.Class, error.State, lineNumber,
|
||||
Environment.NewLine, error.Message);
|
||||
await SendMessage(message, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await SendMessage(dbe.Message, true);
|
||||
}
|
||||
return canIgnore;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Aggregates all result sets in the batch into a single special action
|
||||
/// </summary>
|
||||
private SpecialAction ProcessResultSetSpecialActions()
|
||||
{
|
||||
foreach (ResultSet resultSet in resultSets)
|
||||
{
|
||||
specialAction.CombineSpecialAction(resultSet.Summary.SpecialAction);
|
||||
}
|
||||
|
||||
return specialAction;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IDisposable Implementation
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
lock (resultSets)
|
||||
{
|
||||
foreach (ResultSet r in resultSets)
|
||||
{
|
||||
r.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
disposed = true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
namespace Microsoft.Kusto.ServiceLayer.QueryExecution.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// Summary of a batch within a query
|
||||
/// </summary>
|
||||
public class BatchSummary
|
||||
{
|
||||
/// <summary>
|
||||
/// Localized timestamp for how long it took for the execution to complete
|
||||
/// </summary>
|
||||
public string ExecutionElapsed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Localized timestamp for when the execution completed.
|
||||
/// </summary>
|
||||
public string ExecutionEnd { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Localized timestamp for when the execution started.
|
||||
/// </summary>
|
||||
public string ExecutionStart { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not the batch encountered an error that halted execution
|
||||
/// </summary>
|
||||
public bool HasError { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the result set within the query results
|
||||
/// </summary>
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The selection from the file for this batch
|
||||
/// </summary>
|
||||
public SelectionData Selection { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The summaries of the result sets inside the batch
|
||||
/// </summary>
|
||||
public ResultSetSummary[] ResultSetSummaries { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The special action of the batch
|
||||
/// </summary>
|
||||
public SpecialAction SpecialAction { get; set; }
|
||||
|
||||
public override string ToString() => $"Batch Id:'{Id}', Elapsed:'{ExecutionElapsed}', HasError:'{HasError}'";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using Microsoft.SqlTools.Utility;
|
||||
|
||||
namespace Microsoft.Kusto.ServiceLayer.QueryExecution.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// Class used for internally passing results from a cell around.
|
||||
/// </summary>
|
||||
public class DbCellValue
|
||||
{
|
||||
/// <summary>
|
||||
/// Display value for the cell, suitable to be passed back to the client
|
||||
/// </summary>
|
||||
public string DisplayValue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not the cell is NULL
|
||||
/// </summary>
|
||||
public bool IsNull { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Culture invariant display value for the cell, this value can later be used by the client to convert back to the original value.
|
||||
/// </summary>
|
||||
public string InvariantCultureDisplayValue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The raw object for the cell, for use internally
|
||||
/// </summary>
|
||||
internal object RawObject { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The internal ID for the row. Should be used when directly referencing the row for edit
|
||||
/// or other purposes.
|
||||
/// </summary>
|
||||
public long RowId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Copies the values of this DbCellValue into another DbCellValue (or child object)
|
||||
/// </summary>
|
||||
/// <param name="other">The DbCellValue (or child) that will receive the values</param>
|
||||
public virtual void CopyTo(DbCellValue other)
|
||||
{
|
||||
Validate.IsNotNull(nameof(other), other);
|
||||
|
||||
other.DisplayValue = DisplayValue;
|
||||
other.InvariantCultureDisplayValue = InvariantCultureDisplayValue;
|
||||
other.IsNull = IsNull;
|
||||
other.RawObject = RawObject;
|
||||
other.RowId = RowId;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,352 @@
|
||||
//
|
||||
// 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.Diagnostics;
|
||||
|
||||
namespace Microsoft.Kusto.ServiceLayer.QueryExecution.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// Wrapper around a DbColumn, which provides extra functionality, but can be used as a
|
||||
/// regular DbColumn
|
||||
/// </summary>
|
||||
public class DbColumnWrapper : DbColumn
|
||||
{
|
||||
#region Constants
|
||||
|
||||
/// <summary>
|
||||
/// All types supported by the server, stored as a hash set to provide O(1) lookup
|
||||
/// </summary>
|
||||
private static readonly HashSet<string> AllServerDataTypes = new HashSet<string>
|
||||
{
|
||||
"bigint",
|
||||
"binary",
|
||||
"bit",
|
||||
"char",
|
||||
"datetime",
|
||||
"decimal",
|
||||
"float",
|
||||
"image",
|
||||
"int",
|
||||
"money",
|
||||
"nchar",
|
||||
"ntext",
|
||||
"nvarchar",
|
||||
"real",
|
||||
"uniqueidentifier",
|
||||
"smalldatetime",
|
||||
"smallint",
|
||||
"smallmoney",
|
||||
"text",
|
||||
"timestamp",
|
||||
"tinyint",
|
||||
"varbinary",
|
||||
"varchar",
|
||||
"sql_variant",
|
||||
"xml",
|
||||
"date",
|
||||
"time",
|
||||
"datetimeoffset",
|
||||
"datetime2"
|
||||
};
|
||||
|
||||
private const string SqlXmlDataTypeName = "xml";
|
||||
private const string DbTypeXmlDataTypeName = "DBTYPE_XML";
|
||||
private const string UnknownTypeName = "unknown";
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for a DbColumnWrapper
|
||||
/// </summary>
|
||||
/// <remarks>Most of this logic is taken from SSMS ColumnInfo class</remarks>
|
||||
/// <param name="column">The column we're wrapping around</param>
|
||||
public DbColumnWrapper(DataRow row)
|
||||
{
|
||||
// Set all the fields for the base
|
||||
AllowDBNull = SafeGetValue<bool?>(row, "AllowDBNull");
|
||||
BaseCatalogName = SafeGetValue<string>(row, "BaseCatalogName");
|
||||
BaseColumnName = SafeGetValue<string>(row,"BaseColumnName");
|
||||
BaseSchemaName = SafeGetValue<string>(row,"BaseSchemaName");
|
||||
BaseServerName = SafeGetValue<string>(row,"BaseServerName");
|
||||
BaseTableName = SafeGetValue<string>(row, "BaseTableName");
|
||||
ColumnOrdinal = SafeGetValue<int?>(row, "ColumnOrdinal");
|
||||
ColumnSize = SafeGetValue<int?>(row, "ColumnSize");
|
||||
IsAliased = SafeGetValue<bool?>(row, "IsAliased");
|
||||
IsAutoIncrement = SafeGetValue<bool?>(row, "IsAutoIncrement");
|
||||
IsExpression = SafeGetValue<bool?>(row, "IsExpression");
|
||||
IsHidden = SafeGetValue<bool?>(row, "IsHidden");
|
||||
IsIdentity = SafeGetValue<bool?>(row, "IsIdentity");
|
||||
IsKey = SafeGetValue<bool?>(row, "IsKey");
|
||||
IsLong = SafeGetValue<bool?>(row, "IsLong");
|
||||
IsReadOnly = SafeGetValue<bool?>(row, "IsReadOnly");
|
||||
IsUnique = SafeGetValue<bool?>(row, "IsUnique");
|
||||
NumericPrecision = SafeGetValue<int?>(row, "NumericPrecision");
|
||||
NumericScale = SafeGetValue<int?>(row, "NumericScale");
|
||||
UdtAssemblyQualifiedName = SafeGetValue<string>(row, "UdtAssemblyQualifiedName");
|
||||
DataType = SafeGetValue<Type>(row, "DataType");
|
||||
DataTypeName = SafeGetValue<string>(row, "ColumnType");
|
||||
ColumnName = SafeGetValue<string>(row, "ColumnName");
|
||||
}
|
||||
|
||||
private T SafeGetValue<T>(DataRow row, string attribName)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (row[attribName] is T value)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore exceptions
|
||||
}
|
||||
|
||||
return default(T);
|
||||
}
|
||||
|
||||
public DbColumnWrapper(ColumnInfo columnInfo)
|
||||
{
|
||||
DataTypeName = columnInfo.DataTypeName.ToLowerInvariant();
|
||||
DetermineSqlDbType();
|
||||
DataType = TypeConvertor.ToNetType(this.SqlDbType);
|
||||
if (DataType == typeof(String))
|
||||
{
|
||||
this.ColumnSize = int.MaxValue;
|
||||
}
|
||||
AddNameAndDataFields(columnInfo.Name);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Default constructor, used for deserializing JSON RPC only
|
||||
/// </summary>
|
||||
public DbColumnWrapper()
|
||||
{
|
||||
}
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not the column is bytes
|
||||
/// </summary>
|
||||
public bool IsBytes { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not the column is a character type
|
||||
/// </summary>
|
||||
public bool IsChars { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not the column is a SqlVariant type
|
||||
/// </summary>
|
||||
public bool IsSqlVariant { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not the column is a user-defined type
|
||||
/// </summary>
|
||||
public bool IsUdt { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not the column is XML
|
||||
/// </summary>
|
||||
public bool IsXml { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not the column is JSON
|
||||
/// </summary>
|
||||
public bool IsJson { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The SqlDbType of the column, for use in a SqlParameter
|
||||
/// </summary>
|
||||
public SqlDbType SqlDbType { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whther this is a HierarchyId column
|
||||
/// </summary>
|
||||
public bool IsHierarchyId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not the column is an unknown type
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Logic taken from SSDT determination of unknown columns. It may not even be possible to
|
||||
/// have "unknown" column types with the .NET Core SqlClient.
|
||||
/// </remarks>
|
||||
public bool IsUnknownType => DataType == typeof(object) &&
|
||||
DataTypeName.Equals(UnknownTypeName, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
private void DetermineSqlDbType()
|
||||
{
|
||||
if(string.IsNullOrEmpty(DataTypeName))
|
||||
{
|
||||
SqlDbType = SqlDbType.Udt;
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine the SqlDbType
|
||||
SqlDbType type;
|
||||
if (Enum.TryParse(DataTypeName, true, out type))
|
||||
{
|
||||
SqlDbType = type;
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (DataTypeName)
|
||||
{
|
||||
case "numeric":
|
||||
SqlDbType = SqlDbType.Decimal;
|
||||
break;
|
||||
case "sql_variant":
|
||||
SqlDbType = SqlDbType.Variant;
|
||||
break;
|
||||
case "timestamp":
|
||||
SqlDbType = SqlDbType.VarBinary;
|
||||
break;
|
||||
case "sysname":
|
||||
SqlDbType = SqlDbType.NVarChar;
|
||||
break;
|
||||
default:
|
||||
SqlDbType = DataTypeName.EndsWith(".sys.hierarchyid") ? SqlDbType.NVarChar : SqlDbType.Udt;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AddNameAndDataFields(string columnName)
|
||||
{
|
||||
// We want the display name for the column to always exist
|
||||
ColumnName = string.IsNullOrEmpty(columnName)
|
||||
? SR.QueryServiceColumnNull
|
||||
: columnName;
|
||||
|
||||
switch (DataTypeName)
|
||||
{
|
||||
case "varchar":
|
||||
case "nvarchar":
|
||||
IsChars = true;
|
||||
|
||||
Debug.Assert(ColumnSize.HasValue);
|
||||
if (ColumnSize.Value == int.MaxValue)
|
||||
{
|
||||
IsLong = true;
|
||||
}
|
||||
break;
|
||||
case "text":
|
||||
case "ntext":
|
||||
IsChars = true;
|
||||
IsLong = true;
|
||||
break;
|
||||
case "xml":
|
||||
IsXml = true;
|
||||
IsLong = true;
|
||||
break;
|
||||
case "binary":
|
||||
case "image":
|
||||
IsBytes = true;
|
||||
IsLong = true;
|
||||
break;
|
||||
case "varbinary":
|
||||
case "rowversion":
|
||||
IsBytes = true;
|
||||
|
||||
Debug.Assert(ColumnSize.HasValue);
|
||||
if (ColumnSize.Value == int.MaxValue)
|
||||
{
|
||||
IsLong = true;
|
||||
}
|
||||
break;
|
||||
case "sql_variant":
|
||||
IsSqlVariant = true;
|
||||
break;
|
||||
default:
|
||||
if (!AllServerDataTypes.Contains(DataTypeName))
|
||||
{
|
||||
// treat all UDT's as long/bytes data types to prevent the CLR from attempting
|
||||
// to load the UDT assembly into our process to call ToString() on the object.
|
||||
|
||||
IsUdt = true;
|
||||
IsBytes = true;
|
||||
IsLong = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Convert a base data type to another base data type
|
||||
/// </summary>
|
||||
public sealed class TypeConvertor
|
||||
{
|
||||
private static Dictionary<SqlDbType,Type> _typeMap = new Dictionary<SqlDbType,Type>();
|
||||
|
||||
static TypeConvertor()
|
||||
{
|
||||
_typeMap[SqlDbType.BigInt] = typeof(Int64);
|
||||
_typeMap[SqlDbType.Binary] = typeof(Byte);
|
||||
_typeMap[SqlDbType.Bit] = typeof(Boolean);
|
||||
_typeMap[SqlDbType.Char] = typeof(String);
|
||||
_typeMap[SqlDbType.DateTime] = typeof(DateTime);
|
||||
_typeMap[SqlDbType.Decimal] = typeof(Decimal);
|
||||
_typeMap[SqlDbType.Float] = typeof(Double);
|
||||
_typeMap[SqlDbType.Image] = typeof(Byte[]);
|
||||
_typeMap[SqlDbType.Int] = typeof(Int32);
|
||||
_typeMap[SqlDbType.Money] = typeof(Decimal);
|
||||
_typeMap[SqlDbType.NChar] = typeof(String);
|
||||
_typeMap[SqlDbType.NChar] = typeof(String);
|
||||
_typeMap[SqlDbType.NChar] = typeof(String);
|
||||
_typeMap[SqlDbType.NText] = typeof(String);
|
||||
_typeMap[SqlDbType.NVarChar] = typeof(String);
|
||||
_typeMap[SqlDbType.Real] = typeof(Single);
|
||||
_typeMap[SqlDbType.UniqueIdentifier] = typeof(Guid);
|
||||
_typeMap[SqlDbType.SmallDateTime] = typeof(DateTime);
|
||||
_typeMap[SqlDbType.SmallInt] = typeof(Int16);
|
||||
_typeMap[SqlDbType.SmallMoney] = typeof(Decimal);
|
||||
_typeMap[SqlDbType.Text] = typeof(String);
|
||||
_typeMap[SqlDbType.Timestamp] = typeof(Byte[]);
|
||||
_typeMap[SqlDbType.TinyInt] = typeof(Byte);
|
||||
_typeMap[SqlDbType.VarBinary] = typeof(Byte[]);
|
||||
_typeMap[SqlDbType.VarChar] = typeof(String);
|
||||
_typeMap[SqlDbType.Variant] = typeof(Object);
|
||||
// Note: treating as string
|
||||
_typeMap[SqlDbType.Xml] = typeof(String);
|
||||
_typeMap[SqlDbType.TinyInt] = typeof(Byte);
|
||||
_typeMap[SqlDbType.TinyInt] = typeof(Byte);
|
||||
_typeMap[SqlDbType.TinyInt] = typeof(Byte);
|
||||
_typeMap[SqlDbType.TinyInt] = typeof(Byte);
|
||||
}
|
||||
|
||||
private TypeConvertor()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Convert TSQL type to .Net data type
|
||||
/// </summary>
|
||||
/// <param name="sqlDbType"></param>
|
||||
/// <returns></returns>
|
||||
public static Type ToNetType(SqlDbType sqlDbType)
|
||||
{
|
||||
Type netType;
|
||||
if (!_typeMap.TryGetValue(sqlDbType, out netType))
|
||||
{
|
||||
netType = typeof(String);
|
||||
}
|
||||
return netType;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
using Microsoft.SqlTools.Hosting.Protocol.Contracts;
|
||||
|
||||
namespace Microsoft.Kusto.ServiceLayer.QueryExecution.Contracts.ExecuteRequests
|
||||
{
|
||||
/// <summary>
|
||||
/// Parameters to be sent back as part of a batch start or complete event to indicate that a
|
||||
/// batch of a query started or completed.
|
||||
/// </summary>
|
||||
public class BatchEventParams
|
||||
{
|
||||
/// <summary>
|
||||
/// Summary of the batch that just completed
|
||||
/// </summary>
|
||||
public BatchSummary BatchSummary { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// URI for the editor that owns the query
|
||||
/// </summary>
|
||||
public string OwnerUri { get; set; }
|
||||
}
|
||||
|
||||
public class BatchCompleteEvent
|
||||
{
|
||||
public static readonly
|
||||
EventType<BatchEventParams> Type =
|
||||
EventType<BatchEventParams>.Create("query/batchComplete");
|
||||
}
|
||||
|
||||
public class BatchStartEvent
|
||||
{
|
||||
public static readonly
|
||||
EventType<BatchEventParams> Type =
|
||||
EventType<BatchEventParams>.Create("query/batchStart");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using Microsoft.SqlTools.Hosting.Protocol.Contracts;
|
||||
|
||||
namespace Microsoft.Kusto.ServiceLayer.QueryExecution.Contracts.ExecuteRequests
|
||||
{
|
||||
/// <summary>
|
||||
/// Parameters for executing a query from a document open in the workspace
|
||||
/// </summary>
|
||||
public class ExecuteDocumentSelectionParams : ExecuteRequestParamsBase
|
||||
{
|
||||
/// <summary>
|
||||
/// The selection from the document
|
||||
/// </summary>
|
||||
public SelectionData QuerySelection { get; set; }
|
||||
}
|
||||
|
||||
public class ExecuteDocumentSelectionRequest
|
||||
{
|
||||
public static readonly
|
||||
RequestType<ExecuteDocumentSelectionParams, ExecuteRequestResult> Type =
|
||||
RequestType<ExecuteDocumentSelectionParams, ExecuteRequestResult>.Create("query/executeDocumentSelection");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using Microsoft.SqlTools.Hosting.Protocol.Contracts;
|
||||
|
||||
namespace Microsoft.Kusto.ServiceLayer.QueryExecution.Contracts.ExecuteRequests
|
||||
{
|
||||
/// <summary>
|
||||
/// Parameters for executing a query from a document open in the workspace
|
||||
/// </summary>
|
||||
public class ExecuteDocumentStatementParams : ExecuteRequestParamsBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Line in the document for the location of the SQL statement
|
||||
/// </summary>
|
||||
public int Line { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Column in the document for the location of the SQL statement
|
||||
/// </summary>
|
||||
public int Column { get; set; }
|
||||
}
|
||||
|
||||
public class ExecuteDocumentStatementRequest
|
||||
{
|
||||
public static readonly
|
||||
RequestType<ExecuteDocumentStatementParams, ExecuteRequestResult> Type =
|
||||
RequestType<ExecuteDocumentStatementParams, ExecuteRequestResult>.Create("query/executedocumentstatement");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
namespace Microsoft.Kusto.ServiceLayer.QueryExecution.Contracts.ExecuteRequests
|
||||
{
|
||||
/// <summary>
|
||||
/// Basic parameters that are required for executing a query
|
||||
/// </summary>
|
||||
public abstract class ExecuteRequestParamsBase
|
||||
{
|
||||
/// <summary>
|
||||
/// URI for the editor that is asking for the query execute
|
||||
/// </summary>
|
||||
public string OwnerUri { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Execution plan options
|
||||
/// </summary>
|
||||
public ExecutionPlanOptions ExecutionPlanOptions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Flag to get full column schema via additional queries.
|
||||
/// </summary>
|
||||
public bool GetFullColumnSchema { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
namespace Microsoft.Kusto.ServiceLayer.QueryExecution.Contracts.ExecuteRequests
|
||||
{
|
||||
/// <summary>
|
||||
/// Parameters for the query execute result
|
||||
/// </summary>
|
||||
public class ExecuteRequestResult
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using Microsoft.SqlTools.Hosting.Protocol.Contracts;
|
||||
|
||||
namespace Microsoft.Kusto.ServiceLayer.QueryExecution.Contracts.ExecuteRequests
|
||||
{
|
||||
/// <summary>
|
||||
/// Parameters for executing a query directly
|
||||
/// </summary>
|
||||
public class ExecuteStringParams : ExecuteRequestParamsBase
|
||||
{
|
||||
/// <summary>
|
||||
/// The query to execute
|
||||
/// </summary>
|
||||
public string Query { get; set; }
|
||||
}
|
||||
|
||||
public class ExecuteStringRequest
|
||||
{
|
||||
public static readonly
|
||||
RequestType<ExecuteStringParams, ExecuteRequestResult> Type =
|
||||
RequestType<ExecuteStringParams, ExecuteRequestResult>.Create("query/executeString");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using Microsoft.SqlTools.Hosting.Protocol.Contracts;
|
||||
|
||||
namespace Microsoft.Kusto.ServiceLayer.QueryExecution.Contracts.ExecuteRequests
|
||||
{
|
||||
/// <summary>
|
||||
/// Parameters to be sent back with a message notification
|
||||
/// </summary>
|
||||
public class MessageParams
|
||||
{
|
||||
/// <summary>
|
||||
/// URI for the editor that owns the query
|
||||
/// </summary>
|
||||
public string OwnerUri { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The message that is being returned
|
||||
/// </summary>
|
||||
public ResultMessage Message { get; set; }
|
||||
}
|
||||
|
||||
public class MessageEvent
|
||||
{
|
||||
public static readonly
|
||||
EventType<MessageParams> Type =
|
||||
EventType<MessageParams>.Create("query/message");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using Microsoft.SqlTools.Hosting.Protocol.Contracts;
|
||||
|
||||
namespace Microsoft.Kusto.ServiceLayer.QueryExecution.Contracts.ExecuteRequests
|
||||
{
|
||||
/// <summary>
|
||||
/// Parameters to be sent back with a query execution complete event
|
||||
/// </summary>
|
||||
public class QueryCompleteParams
|
||||
{
|
||||
/// <summary>
|
||||
/// URI for the editor that owns the query
|
||||
/// </summary>
|
||||
public string OwnerUri { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Summaries of the result sets that were returned with the query
|
||||
/// </summary>
|
||||
public BatchSummary[] BatchSummaries { get; set; }
|
||||
}
|
||||
|
||||
public class QueryCompleteEvent
|
||||
{
|
||||
public static readonly
|
||||
EventType<QueryCompleteParams> Type =
|
||||
EventType<QueryCompleteParams>.Create("query/complete");
|
||||
}
|
||||
}
|
||||
@@ -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 Microsoft.SqlTools.Hosting.Protocol.Contracts;
|
||||
|
||||
namespace Microsoft.Kusto.ServiceLayer.QueryExecution.Contracts.ExecuteRequests
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class of parameters to return when a result set is available, updated or completed
|
||||
/// </summary>
|
||||
public abstract class ResultSetEventParams
|
||||
{
|
||||
public ResultSetSummary ResultSetSummary { get; set; }
|
||||
|
||||
public string OwnerUri { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parameters to return when a result set is completed.
|
||||
/// </summary>
|
||||
public class ResultSetCompleteEventParams : ResultSetEventParams
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parameters to return when a result set is available.
|
||||
/// </summary>
|
||||
public class ResultSetAvailableEventParams : ResultSetEventParams
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parameters to return when a result set is updated
|
||||
/// </summary>
|
||||
public class ResultSetUpdatedEventParams : ResultSetEventParams
|
||||
{
|
||||
}
|
||||
|
||||
public class ResultSetCompleteEvent
|
||||
{
|
||||
public static string MethodName { get; } = "query/resultSetComplete";
|
||||
|
||||
public static readonly
|
||||
EventType<ResultSetCompleteEventParams> Type =
|
||||
EventType<ResultSetCompleteEventParams>.Create(MethodName);
|
||||
}
|
||||
|
||||
public class ResultSetAvailableEvent
|
||||
{
|
||||
public static string MethodName { get; } = "query/resultSetAvailable";
|
||||
|
||||
public static readonly
|
||||
EventType<ResultSetAvailableEventParams> Type =
|
||||
EventType<ResultSetAvailableEventParams>.Create(MethodName);
|
||||
}
|
||||
|
||||
public class ResultSetUpdatedEvent
|
||||
{
|
||||
public static string MethodName { get; } = "query/resultSetUpdated";
|
||||
|
||||
public static readonly
|
||||
EventType<ResultSetUpdatedEventParams> Type =
|
||||
EventType<ResultSetUpdatedEventParams>.Create(MethodName);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using Microsoft.SqlTools.Hosting.Protocol.Contracts;
|
||||
|
||||
namespace Microsoft.Kusto.ServiceLayer.QueryExecution.Contracts.ExecuteRequests
|
||||
{
|
||||
/// <summary>
|
||||
/// Parameters for executing a query from a provided string
|
||||
/// </summary>
|
||||
public class SimpleExecuteParams
|
||||
{
|
||||
/// <summary>
|
||||
/// The string to execute
|
||||
/// </summary>
|
||||
public string QueryString { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The owneruri to get connection from
|
||||
/// </summary>
|
||||
public string OwnerUri { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result
|
||||
/// </summary>
|
||||
public class SimpleExecuteResult
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// The number of rows that was returned with the resultset
|
||||
/// </summary>
|
||||
public long RowCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Details about the columns that are provided as solutions
|
||||
/// </summary>
|
||||
public DbColumnWrapper[] ColumnInfo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 2D array of the cell values requested from result set
|
||||
/// </summary>
|
||||
public DbCellValue[][] Rows { get; set; }
|
||||
}
|
||||
|
||||
public class SimpleExecuteRequest
|
||||
{
|
||||
public static readonly
|
||||
RequestType<SimpleExecuteParams, SimpleExecuteResult> Type =
|
||||
RequestType<SimpleExecuteParams, SimpleExecuteResult>.Create("query/simpleexecute");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
namespace Microsoft.Kusto.ServiceLayer.QueryExecution.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// Class used to represent an execution plan from a query for transmission across JSON RPC
|
||||
/// </summary>
|
||||
public class ExecutionPlan
|
||||
{
|
||||
/// <summary>
|
||||
/// The format of the execution plan
|
||||
/// </summary>
|
||||
public string Format { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The execution plan content
|
||||
/// </summary>
|
||||
public string Content { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
namespace Microsoft.Kusto.ServiceLayer.QueryExecution.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// Incoming execution plan options from the extension
|
||||
/// </summary>
|
||||
public struct ExecutionPlanOptions
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Setting to return the actual execution plan as XML
|
||||
/// </summary>
|
||||
public bool IncludeActualExecutionPlanXml { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Setting to return the estimated execution plan as XML
|
||||
/// </summary>
|
||||
public bool IncludeEstimatedExecutionPlanXml { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using Microsoft.SqlTools.Hosting.Protocol.Contracts;
|
||||
|
||||
namespace Microsoft.Kusto.ServiceLayer.QueryExecution.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// Parameters for the query cancellation request
|
||||
/// </summary>
|
||||
public class QueryCancelParams
|
||||
{
|
||||
public string OwnerUri { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parameters to return as the result of a query dispose request
|
||||
/// </summary>
|
||||
public class QueryCancelResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Any error messages that occurred during disposing the result set. Optional, can be set
|
||||
/// to null if there were no errors.
|
||||
/// </summary>
|
||||
public string Messages { get; set; }
|
||||
}
|
||||
|
||||
public class QueryCancelRequest
|
||||
{
|
||||
public static readonly
|
||||
RequestType<QueryCancelParams, QueryCancelResult> Type =
|
||||
RequestType<QueryCancelParams, QueryCancelResult>.Create("query/cancel");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using Microsoft.SqlTools.Hosting.Protocol.Contracts;
|
||||
|
||||
namespace Microsoft.Kusto.ServiceLayer.QueryExecution.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// Parameters for the query dispose request
|
||||
/// </summary>
|
||||
public class QueryDisposeParams
|
||||
{
|
||||
public string OwnerUri { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parameters to return as the result of a query dispose request
|
||||
/// </summary>
|
||||
public class QueryDisposeResult
|
||||
{
|
||||
}
|
||||
|
||||
public class QueryDisposeRequest
|
||||
{
|
||||
public static readonly
|
||||
RequestType<QueryDisposeParams, QueryDisposeResult> Type =
|
||||
RequestType<QueryDisposeParams, QueryDisposeResult>.Create("query/dispose");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using Microsoft.SqlTools.Hosting.Protocol.Contracts;
|
||||
|
||||
namespace Microsoft.Kusto.ServiceLayer.QueryExecution.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// Parameters for query execution plan request
|
||||
/// </summary>
|
||||
public class QueryExecutionPlanParams
|
||||
{
|
||||
/// <summary>
|
||||
/// URI for the file that owns the query to look up the results for
|
||||
/// </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>
|
||||
public int ResultSetIndex { get; set; }
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parameters for the query execution plan request
|
||||
/// </summary>
|
||||
public class QueryExecutionPlanResult
|
||||
{
|
||||
/// <summary>
|
||||
/// The requested execution plan. Optional, can be set to null to indicate an error
|
||||
/// </summary>
|
||||
public ExecutionPlan ExecutionPlan { get; set; }
|
||||
}
|
||||
|
||||
public class QueryExecutionPlanRequest
|
||||
{
|
||||
public static readonly
|
||||
RequestType<QueryExecutionPlanParams, QueryExecutionPlanResult> Type =
|
||||
RequestType<QueryExecutionPlanParams, QueryExecutionPlanResult>.Create("query/executionPlan");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.Kusto.ServiceLayer.QueryExecution.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// Result message object with timestamp and actual message
|
||||
/// </summary>
|
||||
public class ResultMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// ID of the batch that generated this message. If null, this message
|
||||
/// was not generated as part of a batch
|
||||
/// </summary>
|
||||
public int? BatchId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not this message is an error
|
||||
/// </summary>
|
||||
public bool IsError { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp of the message
|
||||
/// Stored in UTC ISO 8601 format; should be localized before displaying to any user
|
||||
/// </summary>
|
||||
public string Time { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Message contents
|
||||
/// </summary>
|
||||
public string Message { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor with default "Now" time
|
||||
/// </summary>
|
||||
public ResultMessage(string message, bool isError, int? batchId)
|
||||
{
|
||||
BatchId = batchId;
|
||||
IsError = isError;
|
||||
Time = DateTime.Now.ToString("o");
|
||||
Message = message;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Default constructor, used for deserializing JSON RPC only
|
||||
/// </summary>
|
||||
public ResultMessage()
|
||||
{
|
||||
}
|
||||
public override string ToString() => $"Message on Batch Id:'{BatchId}', IsError:'{IsError}', Message:'{Message}'";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
namespace Microsoft.Kusto.ServiceLayer.QueryExecution.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// Class used to represent a subset of results from a query for transmission across JSON RPC
|
||||
/// </summary>
|
||||
public class ResultSetSubset
|
||||
{
|
||||
/// <summary>
|
||||
/// The number of rows returned from result set, useful for determining if less rows were
|
||||
/// returned than requested.
|
||||
/// </summary>
|
||||
public int RowCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 2D array of the cell values requested from result set
|
||||
/// </summary>
|
||||
public DbCellValue[][] Rows { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
namespace Microsoft.Kusto.ServiceLayer.QueryExecution.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a summary of information about a result without returning any cells of the results
|
||||
/// </summary>
|
||||
public class ResultSetSummary
|
||||
{
|
||||
/// <summary>
|
||||
/// The ID of the result set within the batch results
|
||||
/// </summary>
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the batch set within the query
|
||||
/// </summary>
|
||||
public int BatchId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of rows that are available for the resultset thus far
|
||||
/// </summary>
|
||||
public long RowCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If true it indicates that all rows have been fetched and the RowCount being sent across is final for this ResultSet
|
||||
/// </summary>
|
||||
public bool Complete { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Details about the columns that are provided as solutions
|
||||
/// </summary>
|
||||
public DbColumnWrapper[] ColumnInfo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The special action definition of the result set
|
||||
/// </summary>
|
||||
public SpecialAction SpecialAction { get; set; }
|
||||
|
||||
public override string ToString() => $"Result Summary Id:{Id}, Batch Id:'{BatchId}', RowCount:'{RowCount}', Complete:'{Complete}', SpecialAction:'{SpecialAction}'";
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using Microsoft.SqlTools.Hosting.Protocol.Contracts;
|
||||
|
||||
namespace Microsoft.Kusto.ServiceLayer.QueryExecution.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// Parameters for the save results request
|
||||
/// </summary>
|
||||
public class SaveResultsRequestParams
|
||||
{
|
||||
/// <summary>
|
||||
/// The path of the file to save results in
|
||||
/// </summary>
|
||||
public string FilePath { 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>
|
||||
public int ResultSetIndex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// URI for the editor that called save results
|
||||
/// </summary>
|
||||
public string OwnerUri { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Start index of the selected rows (inclusive)
|
||||
/// </summary>
|
||||
public int? RowStartIndex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// End index of the selected rows (inclusive)
|
||||
/// </summary>
|
||||
public int? RowEndIndex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Start index of the selected columns (inclusive)
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public int? ColumnStartIndex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// End index of the selected columns (inclusive)
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public int? ColumnEndIndex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Check if request is a subset of result set or whole result set
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
internal bool IsSaveSelection
|
||||
{
|
||||
get
|
||||
{
|
||||
return ColumnStartIndex.HasValue && ColumnEndIndex.HasValue
|
||||
&& RowStartIndex.HasValue && RowEndIndex.HasValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parameters to save results as CSV
|
||||
/// </summary>
|
||||
public class SaveResultsAsCsvRequestParams: SaveResultsRequestParams
|
||||
{
|
||||
/// <summary>
|
||||
/// Include headers of columns in CSV
|
||||
/// </summary>
|
||||
public bool IncludeHeaders { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Delimiter for separating data items in CSV
|
||||
/// </summary>
|
||||
public string Delimiter { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// either CR, CRLF or LF to seperate rows in CSV
|
||||
/// </summary>
|
||||
public string LineSeperator { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Text identifier for alphanumeric columns in CSV
|
||||
/// </summary>
|
||||
public string TextIdentifier { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Encoding of the CSV file
|
||||
/// </summary>
|
||||
public string Encoding { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parameters to save results as Excel
|
||||
/// </summary>
|
||||
public class SaveResultsAsExcelRequestParams : SaveResultsRequestParams
|
||||
{
|
||||
/// <summary>
|
||||
/// Include headers of columns in Excel
|
||||
/// </summary>
|
||||
public bool IncludeHeaders { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parameters to save results as JSON
|
||||
/// </summary>
|
||||
public class SaveResultsAsJsonRequestParams: SaveResultsRequestParams
|
||||
{
|
||||
//TODO: define config for save as JSON
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parameters to save results as XML
|
||||
/// </summary>
|
||||
public class SaveResultsAsXmlRequestParams: SaveResultsRequestParams
|
||||
{
|
||||
/// <summary>
|
||||
/// Formatting of the XML file
|
||||
/// </summary>
|
||||
public bool Formatted { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Encoding of the XML file
|
||||
/// </summary>
|
||||
public string Encoding { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parameters for the save results result
|
||||
/// </summary>
|
||||
public class SaveResultRequestResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Error messages for saving to file.
|
||||
/// </summary>
|
||||
public string Messages { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request type to save results as CSV
|
||||
/// </summary>
|
||||
public class SaveResultsAsCsvRequest
|
||||
{
|
||||
public static readonly
|
||||
RequestType<SaveResultsAsCsvRequestParams, SaveResultRequestResult> Type =
|
||||
RequestType<SaveResultsAsCsvRequestParams, SaveResultRequestResult>.Create("query/saveCsv");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request type to save results as Excel
|
||||
/// </summary>
|
||||
public class SaveResultsAsExcelRequest
|
||||
{
|
||||
public static readonly
|
||||
RequestType<SaveResultsAsExcelRequestParams, SaveResultRequestResult> Type =
|
||||
RequestType<SaveResultsAsExcelRequestParams, SaveResultRequestResult>.Create("query/saveExcel");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request type to save results as JSON
|
||||
/// </summary>
|
||||
public class SaveResultsAsJsonRequest
|
||||
{
|
||||
public static readonly
|
||||
RequestType<SaveResultsAsJsonRequestParams, SaveResultRequestResult> Type =
|
||||
RequestType<SaveResultsAsJsonRequestParams, SaveResultRequestResult>.Create("query/saveJson");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request type to save results as XML
|
||||
/// </summary>
|
||||
public class SaveResultsAsXmlRequest
|
||||
{
|
||||
public static readonly
|
||||
RequestType<SaveResultsAsXmlRequestParams, SaveResultRequestResult> Type =
|
||||
RequestType<SaveResultsAsXmlRequestParams, SaveResultRequestResult>.Create("query/saveXml");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
using Microsoft.Kusto.ServiceLayer.Workspace.Contracts;
|
||||
|
||||
namespace Microsoft.Kusto.ServiceLayer.QueryExecution.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// Container class for a selection range from file
|
||||
/// </summary>
|
||||
/// TODO: Remove this in favor of buffer range end-to-end
|
||||
public class SelectionData
|
||||
{
|
||||
public SelectionData() { }
|
||||
|
||||
public SelectionData(int startLine, int startColumn, int endLine, int endColumn)
|
||||
{
|
||||
StartLine = startLine;
|
||||
StartColumn = startColumn;
|
||||
EndLine = endLine;
|
||||
EndColumn = endColumn;
|
||||
}
|
||||
|
||||
#region Properties
|
||||
|
||||
public int EndColumn { get; set; }
|
||||
|
||||
public int EndLine { get; set; }
|
||||
|
||||
public int StartColumn { get; set; }
|
||||
public int StartLine { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
public BufferRange ToBufferRange()
|
||||
{
|
||||
return new BufferRange(StartLine, StartColumn, EndLine, EndColumn);
|
||||
}
|
||||
|
||||
public static SelectionData FromBufferRange(BufferRange range)
|
||||
{
|
||||
return new SelectionData
|
||||
{
|
||||
StartLine = range.Start.Line,
|
||||
StartColumn = range.Start.Column,
|
||||
EndLine = range.End.Line,
|
||||
EndColumn = range.End.Column
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using System.Data;
|
||||
using Microsoft.SqlTools.Hosting.Protocol.Contracts;
|
||||
using Microsoft.SqlTools.Utility;
|
||||
|
||||
namespace Microsoft.Kusto.ServiceLayer.QueryExecution.Contracts
|
||||
{
|
||||
|
||||
|
||||
public class ColumnInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Name of this column
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
public string DataTypeName { get; set; }
|
||||
|
||||
public ColumnInfo()
|
||||
{
|
||||
}
|
||||
|
||||
public ColumnInfo(string name, string dataTypeName)
|
||||
{
|
||||
this.Name = name;
|
||||
this.DataTypeName = dataTypeName;
|
||||
}
|
||||
}
|
||||
|
||||
public interface ISerializationParams
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Path to file that the serialized results will be stored in
|
||||
/// </summary>
|
||||
string FilePath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Results that are to be serialized into 'SaveFormat' format
|
||||
/// </summary>
|
||||
DbCellValue[][] Rows { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the current set of Rows passed in is the last for this file
|
||||
// </summary>
|
||||
bool IsLastBatch { get; set; }
|
||||
}
|
||||
/// <summary>
|
||||
/// Class used for storing results and how the results are to be serialized
|
||||
/// </summary>
|
||||
public class SerializeDataContinueRequestParams : ISerializationParams
|
||||
{
|
||||
/// <summary>
|
||||
/// Path to file that the serialized results will be stored in
|
||||
/// </summary>
|
||||
public string FilePath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Results that are to be serialized into 'SaveFormat' format
|
||||
/// </summary>
|
||||
public DbCellValue[][] Rows { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the current set of Rows passed in is the last for this file
|
||||
// </summary>
|
||||
public bool IsLastBatch { get; set; }
|
||||
|
||||
}
|
||||
/// <summary>
|
||||
/// Class used for storing results and how the results are to be serialized
|
||||
/// </summary>
|
||||
public class SerializeDataStartRequestParams : GeneralRequestDetails, ISerializationParams
|
||||
{
|
||||
/// <summary>
|
||||
/// String representation of the type that service is supposed to serialize to
|
||||
/// E.g. "json" or "csv"
|
||||
/// </summary>
|
||||
public string SaveFormat { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Path to file that the serialized results will be stored in
|
||||
/// </summary>
|
||||
public string FilePath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Results that are to be serialized into 'SaveFormat' format
|
||||
/// </summary>
|
||||
public DbCellValue[][] Rows { get; set; }
|
||||
|
||||
public ColumnInfo[] Columns { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this is the only request expected for this file.
|
||||
// </summary>
|
||||
public bool IsLastBatch { get; set; }
|
||||
|
||||
public SerializeDataStartRequestParams()
|
||||
{
|
||||
}
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
public SerializeDataStartRequestParams(string saveFormat,
|
||||
string savePath,
|
||||
DbCellValue[][] rows,
|
||||
bool isLast)
|
||||
{
|
||||
this.SaveFormat = saveFormat;
|
||||
this.FilePath = savePath;
|
||||
this.Rows = Rows;
|
||||
this.IsLastBatch = isLast;
|
||||
}
|
||||
|
||||
internal bool IncludeHeaders
|
||||
{
|
||||
get { return this.GetOptionValue<bool>(SerializationOptionsHelper.IncludeHeaders); }
|
||||
set { this.SetOptionValue<bool>(SerializationOptionsHelper.IncludeHeaders, value); }
|
||||
}
|
||||
|
||||
internal string Delimiter
|
||||
{
|
||||
get { return this.GetOptionValue<string>(SerializationOptionsHelper.Delimiter); }
|
||||
set { this.SetOptionValue<string>(SerializationOptionsHelper.Delimiter, value); }
|
||||
}
|
||||
|
||||
internal string LineSeparator
|
||||
{
|
||||
get { return this.GetOptionValue<string>(SerializationOptionsHelper.LineSeparator); }
|
||||
set { this.SetOptionValue<string>(SerializationOptionsHelper.LineSeparator, value); }
|
||||
}
|
||||
|
||||
internal string TextIdentifier
|
||||
{
|
||||
get { return this.GetOptionValue<string>(SerializationOptionsHelper.TextIdentifier); }
|
||||
set { this.SetOptionValue<string>(SerializationOptionsHelper.TextIdentifier, value); }
|
||||
}
|
||||
|
||||
internal string Encoding
|
||||
{
|
||||
get { return this.GetOptionValue<string>(SerializationOptionsHelper.Encoding); }
|
||||
set { this.SetOptionValue<string>(SerializationOptionsHelper.Encoding, value); }
|
||||
}
|
||||
|
||||
internal bool Formatted
|
||||
{
|
||||
get { return this.GetOptionValue<bool>(SerializationOptionsHelper.Formatted); }
|
||||
set { this.SetOptionValue<bool>(SerializationOptionsHelper.Formatted, value); }
|
||||
}
|
||||
}
|
||||
|
||||
public class SerializeDataResult
|
||||
{
|
||||
public string Messages { get; set; }
|
||||
|
||||
public bool Succeeded { get; set; }
|
||||
}
|
||||
|
||||
public class SerializeStartRequest
|
||||
{
|
||||
public static readonly RequestType<SerializeDataStartRequestParams, SerializeDataResult> Type = RequestType<SerializeDataStartRequestParams, SerializeDataResult>.Create("serialize/start");
|
||||
}
|
||||
public class SerializeContinueRequest
|
||||
{
|
||||
public static readonly RequestType<SerializeDataContinueRequestParams, SerializeDataResult> Type = RequestType<SerializeDataContinueRequestParams, SerializeDataResult>.Create("serialize/continue");
|
||||
}
|
||||
|
||||
class SerializationOptionsHelper
|
||||
{
|
||||
internal const string IncludeHeaders = "includeHeaders";
|
||||
internal const string Delimiter = "delimiter";
|
||||
internal const string LineSeparator = "lineSeparator";
|
||||
internal const string TextIdentifier = "textIdentifier";
|
||||
internal const string Encoding = "encoding";
|
||||
internal const string Formatted = "formatted";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using Microsoft.SqlTools.Hosting.Protocol.Contracts;
|
||||
|
||||
namespace Microsoft.Kusto.ServiceLayer.QueryExecution.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// Parameters for a query result subset retrieval request
|
||||
/// </summary>
|
||||
public class SubsetParams
|
||||
{
|
||||
/// <summary>
|
||||
/// URI for the file that owns the query to look up the results for
|
||||
/// </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>
|
||||
public int ResultSetIndex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Beginning index of the rows to return from the selected resultset. This index will be
|
||||
/// included in the results.
|
||||
/// </summary>
|
||||
public long RowsStartIndex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of rows to include in the result of this request. If the number of the rows
|
||||
/// exceeds the number of rows available after the start index, all available rows after
|
||||
/// the start index will be returned.
|
||||
/// </summary>
|
||||
public int RowsCount { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parameters for the result of a subset retrieval request
|
||||
/// </summary>
|
||||
public class SubsetResult
|
||||
{
|
||||
/// <summary>
|
||||
/// The requested subset of results. Optional, can be set to null to indicate an error
|
||||
/// </summary>
|
||||
public ResultSetSubset ResultSubset { get; set; }
|
||||
}
|
||||
|
||||
public class SubsetRequest
|
||||
{
|
||||
public static readonly
|
||||
RequestType<SubsetParams, SubsetResult> Type =
|
||||
RequestType<SubsetParams, SubsetResult>.Create("query/subset");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using Microsoft.Kusto.ServiceLayer.QueryExecution.Contracts;
|
||||
|
||||
namespace Microsoft.Kusto.ServiceLayer.QueryExecution.DataStorage
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a value returned from a read from a file stream. This is used to eliminate ref
|
||||
/// parameters used in the read methods.
|
||||
/// </summary>
|
||||
public struct FileStreamReadResult
|
||||
{
|
||||
/// <summary>
|
||||
/// The total length in bytes of the value, (including the bytes used to store the length
|
||||
/// of the value)
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Cell values are stored such that the length of the value is stored first, then the
|
||||
/// value itself is stored. Eg, a string may be stored as 0x03 0x6C 0x6F 0x6C. Under this
|
||||
/// system, the value would be "lol", the length would be 3, and the total length would be
|
||||
/// 4 bytes.
|
||||
/// </remarks>
|
||||
public int TotalLength { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Value of the cell
|
||||
/// </summary>
|
||||
public DbCellValue Value { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new FileStreamReadResult
|
||||
/// </summary>
|
||||
/// <param name="value">The value of the result, ready for consumption by a client</param>
|
||||
/// <param name="totalLength">The number of bytes for the used to store the value's length and value</param>s
|
||||
public FileStreamReadResult(DbCellValue value, int totalLength)
|
||||
{
|
||||
Value = value;
|
||||
TotalLength = totalLength;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
namespace Microsoft.Kusto.ServiceLayer.QueryExecution.DataStorage
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for a factory that creates filesystem readers/writers
|
||||
/// </summary>
|
||||
public interface IFileStreamFactory
|
||||
{
|
||||
string CreateFile();
|
||||
|
||||
IFileStreamReader GetReader(string fileName);
|
||||
|
||||
IFileStreamWriter GetWriter(string fileName);
|
||||
|
||||
void DisposeFile(string fileName);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// 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 Microsoft.Kusto.ServiceLayer.QueryExecution.Contracts;
|
||||
|
||||
namespace Microsoft.Kusto.ServiceLayer.QueryExecution.DataStorage
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for a object that reads from the filesystem
|
||||
/// </summary>
|
||||
public interface IFileStreamReader : IDisposable
|
||||
{
|
||||
IList<DbCellValue> ReadRow(long offset, long rowId, IEnumerable<DbColumnWrapper> columns);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// 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 Microsoft.Kusto.ServiceLayer.QueryExecution.Contracts;
|
||||
|
||||
|
||||
namespace Microsoft.Kusto.ServiceLayer.QueryExecution.DataStorage
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for a object that writes to a filesystem wrapper
|
||||
/// </summary>
|
||||
public interface IFileStreamWriter : IDisposable
|
||||
{
|
||||
int WriteRow(StorageDataReader dataReader);
|
||||
void WriteRow(IList<DbCellValue> row, IList<DbColumnWrapper> columns);
|
||||
void Seek(long offset);
|
||||
void FlushBuffer();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
//
|
||||
// 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.IO;
|
||||
using Microsoft.Kusto.ServiceLayer.QueryExecution.Contracts;
|
||||
using Microsoft.Kusto.ServiceLayer.SqlContext;
|
||||
using Microsoft.Kusto.ServiceLayer.Utility;
|
||||
|
||||
namespace Microsoft.Kusto.ServiceLayer.QueryExecution.DataStorage
|
||||
{
|
||||
/// <summary>
|
||||
/// Factory for creating a reader/writer pair that will read from the temporary buffer file
|
||||
/// and output to a CSV file.
|
||||
/// </summary>
|
||||
public class SaveAsCsvFileStreamFactory : IFileStreamFactory
|
||||
{
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// Settings for query execution
|
||||
/// </summary>
|
||||
public QueryExecutionSettings QueryExecutionSettings { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Parameters for the save as CSV request
|
||||
/// </summary>
|
||||
public SaveResultsAsCsvRequestParams SaveRequestParams { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// File names are not meant to be created with this factory.
|
||||
/// </summary>
|
||||
/// <exception cref="NotImplementedException">Thrown all times</exception>
|
||||
[Obsolete]
|
||||
public string CreateFile()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a new service buffer reader for reading results back in from the temporary buffer files, file share is ReadWrite to allow concurrent reads/writes to the file.
|
||||
/// </summary>
|
||||
/// <param name="fileName">Path to the temp buffer file</param>
|
||||
/// <returns>Stream reader</returns>
|
||||
public IFileStreamReader GetReader(string fileName)
|
||||
{
|
||||
return new ServiceBufferFileStreamReader(new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite), QueryExecutionSettings);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a new CSV writer for writing results to a CSV file, file share is ReadWrite to allow concurrent reads/writes to the file.
|
||||
/// </summary>
|
||||
/// <param name="fileName">Path to the CSV output file</param>
|
||||
/// <returns>Stream writer</returns>
|
||||
public IFileStreamWriter GetWriter(string fileName)
|
||||
{
|
||||
return new SaveAsCsvFileStreamWriter(new FileStream(fileName, FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite), SaveRequestParams);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Safely deletes the file
|
||||
/// </summary>
|
||||
/// <param name="fileName">Path to the file to delete</param>
|
||||
public void DisposeFile(string fileName)
|
||||
{
|
||||
FileUtilities.SafeFileDelete(fileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
//
|
||||
// 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.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.Kusto.ServiceLayer.QueryExecution.Contracts;
|
||||
|
||||
namespace Microsoft.Kusto.ServiceLayer.QueryExecution.DataStorage
|
||||
{
|
||||
/// <summary>
|
||||
/// Writer for writing rows of results to a CSV file
|
||||
/// </summary>
|
||||
public class SaveAsCsvFileStreamWriter : SaveAsStreamWriter
|
||||
{
|
||||
|
||||
#region Member Variables
|
||||
|
||||
private readonly SaveResultsAsCsvRequestParams saveParams;
|
||||
private bool headerWritten;
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Constructor, stores the CSV specific request params locally, chains into the base
|
||||
/// constructor
|
||||
/// </summary>
|
||||
/// <param name="stream">FileStream to access the CSV file output</param>
|
||||
/// <param name="requestParams">CSV save as request parameters</param>
|
||||
public SaveAsCsvFileStreamWriter(Stream stream, SaveResultsAsCsvRequestParams requestParams)
|
||||
: base(stream, requestParams)
|
||||
{
|
||||
saveParams = requestParams;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a row of data as a CSV row. If this is the first row and the user has requested
|
||||
/// it, the headers for the column will be emitted as well.
|
||||
/// </summary>
|
||||
/// <param name="row">The data of the row to output to the file</param>
|
||||
/// <param name="columns">
|
||||
/// The entire list of columns for the result set. They will be filtered down as per the
|
||||
/// request params.
|
||||
/// </param>
|
||||
public override void WriteRow(IList<DbCellValue> row, IList<DbColumnWrapper> columns)
|
||||
{
|
||||
char delimiter = ',';
|
||||
if(!string.IsNullOrEmpty(saveParams.Delimiter))
|
||||
{
|
||||
// first char in string
|
||||
delimiter = saveParams.Delimiter[0];
|
||||
}
|
||||
|
||||
string lineSeperator = Environment.NewLine;
|
||||
if(!string.IsNullOrEmpty(saveParams.LineSeperator))
|
||||
{
|
||||
lineSeperator = saveParams.LineSeperator;
|
||||
}
|
||||
|
||||
char textIdentifier = '"';
|
||||
if(!string.IsNullOrEmpty(saveParams.TextIdentifier))
|
||||
{
|
||||
// first char in string
|
||||
textIdentifier = saveParams.TextIdentifier[0];
|
||||
}
|
||||
|
||||
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||
int codepage;
|
||||
Encoding encoding;
|
||||
try
|
||||
{
|
||||
if(int.TryParse(saveParams.Encoding, out codepage))
|
||||
{
|
||||
encoding = Encoding.GetEncoding(codepage);
|
||||
}
|
||||
else
|
||||
{
|
||||
encoding = Encoding.GetEncoding(saveParams.Encoding);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Fallback encoding when specified codepage is invalid
|
||||
encoding = Encoding.GetEncoding("utf-8");
|
||||
}
|
||||
|
||||
// Write out the header if we haven't already and the user chose to have it
|
||||
if (saveParams.IncludeHeaders && !headerWritten)
|
||||
{
|
||||
// Build the string
|
||||
var selectedColumns = columns.Skip(ColumnStartIndex ?? 0).Take(ColumnCount ?? columns.Count)
|
||||
.Select(c => EncodeCsvField(c.ColumnName, delimiter, textIdentifier) ?? string.Empty);
|
||||
|
||||
string headerLine = string.Join(delimiter, selectedColumns);
|
||||
|
||||
// Encode it and write it out
|
||||
byte[] headerBytes = encoding.GetBytes(headerLine + lineSeperator);
|
||||
FileStream.Write(headerBytes, 0, headerBytes.Length);
|
||||
|
||||
headerWritten = true;
|
||||
}
|
||||
|
||||
// Build the string for the row
|
||||
var selectedCells = row.Skip(ColumnStartIndex ?? 0)
|
||||
.Take(ColumnCount ?? columns.Count)
|
||||
.Select(c => EncodeCsvField(c.DisplayValue, delimiter, textIdentifier));
|
||||
string rowLine = string.Join(delimiter, selectedCells);
|
||||
|
||||
// Encode it and write it out
|
||||
byte[] rowBytes = encoding.GetBytes(rowLine + lineSeperator);
|
||||
FileStream.Write(rowBytes, 0, rowBytes.Length);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encodes a single field for inserting into a CSV record. The following rules are applied:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>All double quotes (") are replaced with a pair of consecutive double quotes</description></item>
|
||||
/// </list>
|
||||
/// The entire field is also surrounded by a pair of double quotes if any of the following conditions are met:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>The field begins or ends with a space</description></item>
|
||||
/// <item><description>The field begins or ends with a tab</description></item>
|
||||
/// <item><description>The field contains the ListSeparator string</description></item>
|
||||
/// <item><description>The field contains the '\n' character</description></item>
|
||||
/// <item><description>The field contains the '\r' character</description></item>
|
||||
/// <item><description>The field contains the '"' character</description></item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
/// <param name="field">The field to encode</param>
|
||||
/// <returns>The CSV encoded version of the original field</returns>
|
||||
internal static string EncodeCsvField(string field, char delimiter, char textIdentifier)
|
||||
{
|
||||
string strTextIdentifier = textIdentifier.ToString();
|
||||
|
||||
// Special case for nulls
|
||||
if (field == null)
|
||||
{
|
||||
return "NULL";
|
||||
}
|
||||
|
||||
// Whether this field has special characters which require it to be embedded in quotes
|
||||
bool embedInQuotes = field.IndexOfAny(new[] { delimiter, '\r', '\n', textIdentifier }) >= 0 // Contains special characters
|
||||
|| field.StartsWith(" ") || field.EndsWith(" ") // Start/Ends with space
|
||||
|| field.StartsWith("\t") || field.EndsWith("\t"); // Starts/Ends with tab
|
||||
|
||||
//Replace all quotes in the original field with double quotes
|
||||
string ret = field.Replace(strTextIdentifier, strTextIdentifier + strTextIdentifier);
|
||||
|
||||
if (embedInQuotes)
|
||||
{
|
||||
ret = strTextIdentifier + $"{ret}" + strTextIdentifier;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
//
|
||||
// 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.IO;
|
||||
using Microsoft.Kusto.ServiceLayer.QueryExecution.Contracts;
|
||||
using Microsoft.Kusto.ServiceLayer.SqlContext;
|
||||
using Microsoft.Kusto.ServiceLayer.Utility;
|
||||
|
||||
namespace Microsoft.Kusto.ServiceLayer.QueryExecution.DataStorage
|
||||
{
|
||||
/// <summary>
|
||||
/// Factory for creating a reader/writer pair that will read from the temporary buffer file
|
||||
/// and output to a Excel file.
|
||||
/// </summary>
|
||||
public class SaveAsExcelFileStreamFactory : IFileStreamFactory
|
||||
{
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// Settings for query execution
|
||||
/// </summary>
|
||||
public QueryExecutionSettings QueryExecutionSettings { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Parameters for the save as Excel request
|
||||
/// </summary>
|
||||
public SaveResultsAsExcelRequestParams SaveRequestParams { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// File names are not meant to be created with this factory.
|
||||
/// </summary>
|
||||
/// <exception cref="NotImplementedException">Thrown all times</exception>
|
||||
[Obsolete]
|
||||
public string CreateFile()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a new service buffer reader for reading results back in from the temporary buffer files, file share is ReadWrite to allow concurrent reads/writes to the file.
|
||||
/// </summary>
|
||||
/// <param name="fileName">Path to the temp buffer file</param>
|
||||
/// <returns>Stream reader</returns>
|
||||
public IFileStreamReader GetReader(string fileName)
|
||||
{
|
||||
return new ServiceBufferFileStreamReader(new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite), QueryExecutionSettings);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a new Excel writer for writing results to a Excel file, file share is ReadWrite to allow concurrent reads/writes to the file.
|
||||
/// </summary>
|
||||
/// <param name="fileName">Path to the Excel output file</param>
|
||||
/// <returns>Stream writer</returns>
|
||||
public IFileStreamWriter GetWriter(string fileName)
|
||||
{
|
||||
return new SaveAsExcelFileStreamWriter(new FileStream(fileName, FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite), SaveRequestParams);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Safely deletes the file
|
||||
/// </summary>
|
||||
/// <param name="fileName">Path to the file to delete</param>
|
||||
public void DisposeFile(string fileName)
|
||||
{
|
||||
FileUtilities.SafeFileDelete(fileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
//
|
||||
// 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.IO;
|
||||
using Microsoft.Kusto.ServiceLayer.QueryExecution.Contracts;
|
||||
|
||||
namespace Microsoft.Kusto.ServiceLayer.QueryExecution.DataStorage
|
||||
{
|
||||
/// <summary>
|
||||
/// Writer for writing rows of results to a Excel file
|
||||
/// </summary>
|
||||
public class SaveAsExcelFileStreamWriter : SaveAsStreamWriter
|
||||
{
|
||||
|
||||
#region Member Variables
|
||||
|
||||
private readonly SaveResultsAsExcelRequestParams saveParams;
|
||||
private bool headerWritten;
|
||||
private SaveAsExcelFileStreamWriterHelper helper;
|
||||
private SaveAsExcelFileStreamWriterHelper.ExcelSheet sheet;
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Constructor, stores the Excel specific request params locally, chains into the base
|
||||
/// constructor
|
||||
/// </summary>
|
||||
/// <param name="stream">FileStream to access the Excel file output</param>
|
||||
/// <param name="requestParams">Excel save as request parameters</param>
|
||||
public SaveAsExcelFileStreamWriter(Stream stream, SaveResultsAsExcelRequestParams requestParams)
|
||||
: base(stream, requestParams)
|
||||
{
|
||||
saveParams = requestParams;
|
||||
helper = new SaveAsExcelFileStreamWriterHelper(stream);
|
||||
sheet = helper.AddSheet();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a row of data as a Excel row. If this is the first row and the user has requested
|
||||
/// it, the headers for the column will be emitted as well.
|
||||
/// </summary>
|
||||
/// <param name="row">The data of the row to output to the file</param>
|
||||
/// <param name="columns">
|
||||
/// The entire list of columns for the result set. They will be filtered down as per the
|
||||
/// request params.
|
||||
/// </param>
|
||||
public override void WriteRow(IList<DbCellValue> row, IList<DbColumnWrapper> columns)
|
||||
{
|
||||
int columnStart = ColumnStartIndex ?? 0;
|
||||
int columnEnd = (ColumnEndIndex != null) ? ColumnEndIndex.Value + 1 : columns.Count;
|
||||
|
||||
// Write out the header if we haven't already and the user chose to have it
|
||||
if (saveParams.IncludeHeaders && !headerWritten)
|
||||
{
|
||||
sheet.AddRow();
|
||||
for (int i = columnStart; i < columnEnd; i++)
|
||||
{
|
||||
sheet.AddCell(columns[i].ColumnName);
|
||||
}
|
||||
headerWritten = true;
|
||||
}
|
||||
|
||||
sheet.AddRow();
|
||||
for (int i = columnStart; i < columnEnd; i++)
|
||||
{
|
||||
sheet.AddCell(row[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private bool disposed;
|
||||
override protected void Dispose(bool disposing)
|
||||
{
|
||||
if (disposed)
|
||||
return;
|
||||
|
||||
sheet.Dispose();
|
||||
helper.Dispose();
|
||||
|
||||
disposed = true;
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,805 @@
|
||||
//
|
||||
// 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.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Xml;
|
||||
using Microsoft.Kusto.ServiceLayer.QueryExecution.Contracts;
|
||||
|
||||
namespace Microsoft.Kusto.ServiceLayer.QueryExecution.DataStorage
|
||||
{
|
||||
// A xlsx file is a zip with specific folder structure.
|
||||
// http://www.ecma-international.org/publications/standards/Ecma-376.htm
|
||||
|
||||
// The page number in the comments are based on
|
||||
// ECMA-376, Fifth Edition, Part 1 - Fundamentals And Markup Language Reference
|
||||
|
||||
// Page 75, SpreadsheetML package structure
|
||||
// |- [Content_Types].xml
|
||||
// |- _rels
|
||||
// |- .rels
|
||||
// |- xl
|
||||
// |- workbook.xml
|
||||
// |- styles.xml
|
||||
// |- _rels
|
||||
// |- workbook.xml.rels
|
||||
// |- worksheets
|
||||
// |- sheet1.xml
|
||||
|
||||
/// <summary>
|
||||
/// A helper class for write xlsx file base on ECMA-376. It tries to be minimal,
|
||||
/// both in implementation and runtime allocation.
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// This sample shows how to use the class
|
||||
/// <code>
|
||||
/// public class TestClass
|
||||
/// {
|
||||
/// public static int Main()
|
||||
/// {
|
||||
/// using (Stream stream = File.Create("test.xlsx"))
|
||||
/// using (var helper = new SaveAsExcelFileStreamWriterHelper(stream, false))
|
||||
/// using (var sheet = helper.AddSheet())
|
||||
/// {
|
||||
/// sheet.AddRow();
|
||||
/// sheet.AddCell("string");
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
|
||||
internal sealed class SaveAsExcelFileStreamWriterHelper : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Present a Excel sheet
|
||||
/// </summary>
|
||||
public sealed class ExcelSheet : IDisposable
|
||||
{
|
||||
// The excel epoch is 1/1/1900, but it has 1/0/1900 and 2/29/1900
|
||||
// which is equal to set the epoch back two days to 12/30/1899
|
||||
// new DateTime(1899,12,30).Ticks
|
||||
private const long ExcelEpochTick = 599264352000000000L;
|
||||
|
||||
// Excel can not use date before 1/0/1900 and
|
||||
// date before 3/1/1900 is wrong, off by 1 because of 2/29/1900
|
||||
// thus, for any date before 3/1/1900, use string for date
|
||||
// new DateTime(1900,3,1).Ticks
|
||||
private const long ExcelDateCutoffTick = 599317056000000000L;
|
||||
|
||||
// new TimeSpan(24,0,0).Ticks
|
||||
private const long TicksPerDay = 864000000000L;
|
||||
|
||||
private XmlWriter writer;
|
||||
private ReferenceManager referenceManager;
|
||||
private bool hasOpenRowTag;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the ExcelSheet class.
|
||||
/// </summary>
|
||||
/// <param name="writer">XmlWriter to write the sheet data</param>
|
||||
internal ExcelSheet(XmlWriter writer)
|
||||
{
|
||||
this.writer = writer;
|
||||
writer.WriteStartDocument();
|
||||
writer.WriteStartElement("worksheet", "http://schemas.openxmlformats.org/spreadsheetml/2006/main");
|
||||
writer.WriteAttributeString("xmlns", "r", null, "http://schemas.openxmlformats.org/officeDocument/2006/relationships");
|
||||
writer.WriteStartElement("sheetData");
|
||||
referenceManager = new ReferenceManager(writer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start a new row
|
||||
/// </summary>
|
||||
public void AddRow()
|
||||
{
|
||||
EndRowIfNeeded();
|
||||
hasOpenRowTag = true;
|
||||
|
||||
referenceManager.AssureRowReference();
|
||||
|
||||
writer.WriteStartElement("row");
|
||||
referenceManager.WriteAndIncreaseRowReference();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write a string cell
|
||||
/// </summary>
|
||||
/// <param name="value">string value to write</param>
|
||||
public void AddCell(string value)
|
||||
{
|
||||
// string needs <c t="inlineStr"><is><t>string</t></is></c>
|
||||
// This class uses inlineStr instead of more common shared string table
|
||||
// to improve write performance and reduce implementation complexity
|
||||
referenceManager.AssureColumnReference();
|
||||
if (value == null)
|
||||
{
|
||||
AddCellEmpty();
|
||||
return;
|
||||
}
|
||||
|
||||
writer.WriteStartElement("c");
|
||||
|
||||
referenceManager.WriteAndIncreaseColumnReference();
|
||||
|
||||
writer.WriteAttributeString("t", "inlineStr");
|
||||
|
||||
writer.WriteStartElement("is");
|
||||
writer.WriteStartElement("t");
|
||||
writer.WriteValue(value);
|
||||
writer.WriteEndElement();
|
||||
writer.WriteEndElement();
|
||||
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write a object cell
|
||||
/// </summary>
|
||||
/// The program will try to output number/datetime, otherwise, call the ToString
|
||||
/// <param name="o"></param>
|
||||
public void AddCell(DbCellValue dbCellValue)
|
||||
{
|
||||
object o = dbCellValue.RawObject;
|
||||
if (dbCellValue.IsNull || o == null)
|
||||
{
|
||||
AddCellEmpty();
|
||||
return;
|
||||
}
|
||||
switch (Type.GetTypeCode(o.GetType()))
|
||||
{
|
||||
case TypeCode.Boolean:
|
||||
AddCell((bool)o);
|
||||
break;
|
||||
case TypeCode.Byte:
|
||||
case TypeCode.Int16:
|
||||
case TypeCode.Int32:
|
||||
case TypeCode.Int64:
|
||||
case TypeCode.Single:
|
||||
case TypeCode.Double:
|
||||
case TypeCode.Decimal:
|
||||
AddCellBoxedNumber(o);
|
||||
break;
|
||||
case TypeCode.DateTime:
|
||||
AddCell((DateTime)o);
|
||||
break;
|
||||
case TypeCode.String:
|
||||
AddCell((string)o);
|
||||
break;
|
||||
default:
|
||||
if (o is TimeSpan) //TimeSpan doesn't have TypeCode
|
||||
{
|
||||
AddCell((TimeSpan)o);
|
||||
break;
|
||||
}
|
||||
AddCell(dbCellValue.DisplayValue);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Close the <row><sheetData><worksheet> tags and close the stream
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
EndRowIfNeeded();
|
||||
writer.WriteEndElement(); // <sheetData>
|
||||
writer.WriteEndElement(); // <worksheet>
|
||||
writer.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write a empty cell
|
||||
/// </summary>
|
||||
/// This only increases the internal bookmark and doesn't arcturally write out anything.
|
||||
private void AddCellEmpty()
|
||||
{
|
||||
referenceManager.IncreaseColumnReference();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write a bool cell.
|
||||
/// </summary>
|
||||
/// <param name="time"></param>
|
||||
private void AddCell(bool value)
|
||||
{
|
||||
// Excel FALSE: <c r="A1" t="b"><v>0</v></c>
|
||||
// Excel TRUE: <c r="A1" t="b"><v>1</v></c>
|
||||
referenceManager.AssureColumnReference();
|
||||
|
||||
writer.WriteStartElement("c");
|
||||
|
||||
referenceManager.WriteAndIncreaseColumnReference();
|
||||
|
||||
writer.WriteAttributeString("t", "b");
|
||||
|
||||
writer.WriteStartElement("v");
|
||||
if (value)
|
||||
{
|
||||
writer.WriteValue("1"); //use string to avoid convert
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.WriteValue("0");
|
||||
}
|
||||
writer.WriteEndElement();
|
||||
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write a TimeSpan cell.
|
||||
/// </summary>
|
||||
/// <param name="time"></param>
|
||||
private void AddCell(TimeSpan time)
|
||||
{
|
||||
referenceManager.AssureColumnReference();
|
||||
double excelDate = (double)time.Ticks / (double)TicksPerDay;
|
||||
// The default hh:mm:ss format do not support more than 24 hours
|
||||
// For that case, use the format string [h]:mm:ss
|
||||
if (time.Ticks >= TicksPerDay)
|
||||
{
|
||||
AddCellDateTimeInternal(excelDate, Style.TimeMoreThan24Hours);
|
||||
}
|
||||
else
|
||||
{
|
||||
AddCellDateTimeInternal(excelDate, Style.Time);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write a DateTime cell.
|
||||
/// </summary>
|
||||
/// <param name="dateTime">Datetime</param>
|
||||
/// <remark>
|
||||
/// If the DateTime does not have date part, it will be written as datetime and show as time only
|
||||
/// If the DateTime is before 1900-03-01, save as string because excel doesn't support them.
|
||||
/// Otherwise, save as datetime, and if the time is 00:00:00, show as yyyy-MM-dd.
|
||||
/// Show the datetime as yyyy-MM-dd HH:mm:ss if none of the previous situations
|
||||
/// </remark>
|
||||
private void AddCell(DateTime dateTime)
|
||||
{
|
||||
referenceManager.AssureColumnReference();
|
||||
long ticks = dateTime.Ticks;
|
||||
Style style = Style.DateTime;
|
||||
double excelDate;
|
||||
if (ticks < TicksPerDay) //date empty, time only
|
||||
{
|
||||
style = Style.Time;
|
||||
excelDate = ((double)ticks) / (double)TicksPerDay;
|
||||
}
|
||||
else if (ticks < ExcelDateCutoffTick) //before excel cut-off, use string
|
||||
{
|
||||
AddCell(dateTime.ToString("yyyy-MM-dd", System.Globalization.CultureInfo.InvariantCulture));
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ticks % TicksPerDay == 0) //time empty, date only
|
||||
{
|
||||
style = Style.Date;
|
||||
}
|
||||
excelDate = ((double)(ticks - ExcelEpochTick)) / (double)TicksPerDay;
|
||||
}
|
||||
AddCellDateTimeInternal(excelDate, style);
|
||||
}
|
||||
|
||||
// number needs <c r="A1"><v>12.5</v></c>
|
||||
private void AddCellBoxedNumber(object number)
|
||||
{
|
||||
referenceManager.AssureColumnReference();
|
||||
|
||||
writer.WriteStartElement("c");
|
||||
|
||||
referenceManager.WriteAndIncreaseColumnReference();
|
||||
|
||||
writer.WriteStartElement("v");
|
||||
writer.WriteValue(number);
|
||||
writer.WriteEndElement();
|
||||
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
|
||||
|
||||
// datetime needs <c r="A1" s="2"><v>26012.451</v></c>
|
||||
private void AddCellDateTimeInternal(double excelDate, Style style)
|
||||
{
|
||||
writer.WriteStartElement("c");
|
||||
|
||||
referenceManager.WriteAndIncreaseColumnReference();
|
||||
|
||||
writer.WriteStartAttribute("s");
|
||||
writer.WriteValue((int)style);
|
||||
writer.WriteEndAttribute();
|
||||
|
||||
writer.WriteStartElement("v");
|
||||
writer.WriteValue(excelDate);
|
||||
writer.WriteEndElement();
|
||||
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
|
||||
private void EndRowIfNeeded()
|
||||
{
|
||||
if (hasOpenRowTag)
|
||||
{
|
||||
writer.WriteEndElement(); // <row>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper class to track the current cell reference.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// SpreadsheetML cell needs a reference attribute. (e.g. r="A1"). This class is used
|
||||
/// to track the current cell reference.
|
||||
/// </remarks>
|
||||
internal class ReferenceManager
|
||||
{
|
||||
private int currColumn; // 0 is invalid, the first AddRow will set to 1
|
||||
private int currRow = 1;
|
||||
|
||||
// In order to reduce allocation, current reference is saved in this array,
|
||||
// and write to the XmlWriter through WriteChars.
|
||||
// For example, when the reference has value AA15,
|
||||
// The content of this array will be @AA15xxxxx, with currReferenceRowLength=2
|
||||
// and currReferenceColumnLength=2
|
||||
private char[] currReference = new char[3 + 7]; //maximal XFD1048576
|
||||
private int currReferenceRowLength;
|
||||
private int currReferenceColumnLength;
|
||||
|
||||
private XmlWriter writer;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the ReferenceManager class.
|
||||
/// </summary>
|
||||
/// <param name="writer">XmlWriter to write the reference attribute to.</param>
|
||||
public ReferenceManager(XmlWriter writer)
|
||||
{
|
||||
this.writer = writer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check that we have not write too many columns. (xlsx has a limit of 16384 columns)
|
||||
/// </summary>
|
||||
public void AssureColumnReference()
|
||||
{
|
||||
if (currColumn == 0)
|
||||
{
|
||||
throw new InvalidOperationException("AddRow must be called before AddCell");
|
||||
|
||||
}
|
||||
if (currColumn > 16384)
|
||||
{
|
||||
throw new InvalidOperationException("max column number is 16384, see https://support.office.com/en-us/article/Excel-specifications-and-limits-1672b34d-7043-467e-8e27-269d656771c3");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out the r="A1" attribute and increase the column number of internal bookmark
|
||||
/// </summary>
|
||||
public void WriteAndIncreaseColumnReference()
|
||||
{
|
||||
writer.WriteStartAttribute("r");
|
||||
writer.WriteChars(currReference, 3 - currReferenceColumnLength, currReferenceRowLength + currReferenceColumnLength);
|
||||
writer.WriteEndAttribute();
|
||||
IncreaseColumnReference();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Increase the column of internal bookmark.
|
||||
/// </summary>
|
||||
public void IncreaseColumnReference()
|
||||
{
|
||||
// This function change the first three chars of currReference array
|
||||
// The logic is simple, when a start a new row, the array is reset to @@A
|
||||
// where @='A'-1. At each increase, check if the current reference is Z
|
||||
// and move to AA if needed, since the maximal is 16384, or XFD, the code
|
||||
// manipulates the array element directly instead of loop
|
||||
char[] reference = currReference;
|
||||
currColumn++;
|
||||
if ('Z' == reference[2]++)
|
||||
{
|
||||
reference[2] = 'A';
|
||||
if (currReferenceColumnLength < 2)
|
||||
{
|
||||
currReferenceColumnLength = 2;
|
||||
}
|
||||
if ('Z' == reference[1]++)
|
||||
{
|
||||
reference[0]++;
|
||||
reference[1] = 'A';
|
||||
currReferenceColumnLength = 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check that we have not write too many rows. (xlsx has a limit of 1048576 rows)
|
||||
/// </summary>
|
||||
public void AssureRowReference()
|
||||
{
|
||||
if (currRow > 1048576)
|
||||
{
|
||||
throw new InvalidOperationException("max row number is 1048576, see https://support.office.com/en-us/article/Excel-specifications-and-limits-1672b34d-7043-467e-8e27-269d656771c3");
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Write out the r="1" attribute and increase the row number of internal bookmark
|
||||
/// </summary>
|
||||
public void WriteAndIncreaseRowReference()
|
||||
{
|
||||
writer.WriteStartAttribute("r");
|
||||
writer.WriteValue(currRow);
|
||||
writer.WriteEndAttribute();
|
||||
|
||||
ResetColumnReference(); //This need to be called before the increase
|
||||
|
||||
currRow++;
|
||||
}
|
||||
|
||||
// Reset the Column Reference
|
||||
// This will reset the first three chars of currReference array to '@@A'
|
||||
// and the rest to the array to the string presentation of the current row.
|
||||
private void ResetColumnReference()
|
||||
{
|
||||
currColumn = 1;
|
||||
currReference[0] = currReference[1] = (char)('A' - 1);
|
||||
currReference[2] = 'A';
|
||||
currReferenceColumnLength = 1;
|
||||
|
||||
string rowReference = XmlConvert.ToString(currRow);
|
||||
currReferenceRowLength = rowReference.Length;
|
||||
rowReference.CopyTo(0, currReference, 3, rowReference.Length);
|
||||
}
|
||||
}
|
||||
|
||||
private enum Style
|
||||
{
|
||||
Normal = 0,
|
||||
Date = 1,
|
||||
Time = 2,
|
||||
DateTime = 3,
|
||||
TimeMoreThan24Hours = 4,
|
||||
}
|
||||
|
||||
private ZipArchive zipArchive;
|
||||
private List<string> sheetNames = new List<string>();
|
||||
private XmlWriterSettings writerSetting = new XmlWriterSettings()
|
||||
{
|
||||
CloseOutput = true,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the SaveAsExcelFileStreamWriterHelper class.
|
||||
/// </summary>
|
||||
/// <param name="stream">The input or output stream.</param>
|
||||
public SaveAsExcelFileStreamWriterHelper(Stream stream)
|
||||
{
|
||||
zipArchive = new ZipArchive(stream, ZipArchiveMode.Create, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the SaveAsExcelFileStreamWriterHelper class.
|
||||
/// </summary>
|
||||
/// <param name="stream">The input or output stream.</param>
|
||||
/// <param name="leaveOpen">true to leave the stream open after the
|
||||
/// SaveAsExcelFileStreamWriterHelper object is disposed; otherwise, false.</param>
|
||||
public SaveAsExcelFileStreamWriterHelper(Stream stream, bool leaveOpen)
|
||||
{
|
||||
zipArchive = new ZipArchive(stream, ZipArchiveMode.Create, leaveOpen);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add sheet inside the Xlsx file.
|
||||
/// </summary>
|
||||
/// <param name="sheetName">Sheet name</param>
|
||||
/// <returns>ExcelSheet for writing the sheet content</returns>
|
||||
/// <remarks>
|
||||
/// When the sheetName is null, sheet1,shhet2,..., will be used.
|
||||
/// The following charactors are not allowed in the sheetName
|
||||
/// '\', '/','*','[',']',':','?'
|
||||
/// </remarks>
|
||||
public ExcelSheet AddSheet(string sheetName = null)
|
||||
{
|
||||
string sheetFileName = "sheet" + (sheetNames.Count + 1);
|
||||
if (sheetName == null)
|
||||
{
|
||||
sheetName = sheetFileName;
|
||||
}
|
||||
EnsureValidSheetName(sheetName);
|
||||
|
||||
sheetNames.Add(sheetName);
|
||||
XmlWriter sheetWriter = AddEntry($"xl/worksheets/{sheetFileName}.xml");
|
||||
return new ExcelSheet(sheetWriter);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write out the rest of the xlsx files and release the resources used by the current instance
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
WriteMinimalTemplate();
|
||||
zipArchive.Dispose();
|
||||
}
|
||||
|
||||
|
||||
private XmlWriter AddEntry(string entryName)
|
||||
{
|
||||
ZipArchiveEntry entry = zipArchive.CreateEntry(entryName, CompressionLevel.Fastest);
|
||||
return XmlWriter.Create(entry.Open(), writerSetting);
|
||||
}
|
||||
|
||||
//ECMA-376 page 75
|
||||
private void WriteMinimalTemplate()
|
||||
{
|
||||
WriteTopRel();
|
||||
WriteWorkbook();
|
||||
WriteStyle();
|
||||
WriteContentType();
|
||||
WriteWorkbookRel();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// write [Content_Types].xml
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This file need to describe all the files in the zip.
|
||||
/// </remarks>
|
||||
private void WriteContentType()
|
||||
{
|
||||
using (XmlWriter xw = AddEntry("[Content_Types].xml"))
|
||||
{
|
||||
xw.WriteStartDocument();
|
||||
xw.WriteStartElement("Types", "http://schemas.openxmlformats.org/package/2006/content-types");
|
||||
|
||||
xw.WriteStartElement("Default");
|
||||
xw.WriteAttributeString("Extension", "rels");
|
||||
xw.WriteAttributeString("ContentType", "application/vnd.openxmlformats-package.relationships+xml");
|
||||
xw.WriteEndElement();
|
||||
|
||||
xw.WriteStartElement("Override");
|
||||
xw.WriteAttributeString("PartName", "/xl/workbook.xml");
|
||||
xw.WriteAttributeString("ContentType", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml");
|
||||
xw.WriteEndElement();
|
||||
|
||||
xw.WriteStartElement("Override");
|
||||
xw.WriteAttributeString("PartName", "/xl/styles.xml");
|
||||
xw.WriteAttributeString("ContentType", "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml");
|
||||
xw.WriteEndElement();
|
||||
|
||||
for (int i = 1; i <= sheetNames.Count; ++i)
|
||||
{
|
||||
xw.WriteStartElement("Override");
|
||||
xw.WriteAttributeString("PartName", "/xl/worksheets/sheet" + i + ".xml");
|
||||
xw.WriteAttributeString("ContentType", "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml");
|
||||
xw.WriteEndElement();
|
||||
}
|
||||
xw.WriteEndElement();
|
||||
xw.WriteEndDocument();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write _rels/.rels. This file only need to reference main workbook
|
||||
/// </summary>
|
||||
private void WriteTopRel()
|
||||
{
|
||||
using (XmlWriter xw = AddEntry("_rels/.rels"))
|
||||
{
|
||||
xw.WriteStartDocument();
|
||||
|
||||
xw.WriteStartElement("Relationships", "http://schemas.openxmlformats.org/package/2006/relationships");
|
||||
|
||||
xw.WriteStartElement("Relationship");
|
||||
xw.WriteAttributeString("Id", "rId1");
|
||||
xw.WriteAttributeString("Type", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument");
|
||||
xw.WriteAttributeString("Target", "xl/workbook.xml");
|
||||
xw.WriteEndElement();
|
||||
|
||||
xw.WriteEndElement();
|
||||
|
||||
xw.WriteEndDocument();
|
||||
}
|
||||
}
|
||||
|
||||
private static char[] invalidSheetNameCharacters = new char[]
|
||||
{
|
||||
'\\', '/','*','[',']',':','?'
|
||||
};
|
||||
private void EnsureValidSheetName(string sheetName)
|
||||
{
|
||||
if (sheetName.IndexOfAny(invalidSheetNameCharacters) != -1)
|
||||
{
|
||||
throw new ArgumentException($"Invalid sheetname: sheetName");
|
||||
}
|
||||
if (sheetNames.IndexOf(sheetName) != -1)
|
||||
{
|
||||
throw new ArgumentException($"Duplicate sheetName: {sheetName}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write xl/workbook.xml. This file will references the sheets through ids in xl/_rels/workbook.xml.rels
|
||||
/// </summary>
|
||||
private void WriteWorkbook()
|
||||
{
|
||||
using (XmlWriter xw = AddEntry("xl/workbook.xml"))
|
||||
{
|
||||
xw.WriteStartDocument();
|
||||
xw.WriteStartElement("workbook", "http://schemas.openxmlformats.org/spreadsheetml/2006/main");
|
||||
xw.WriteAttributeString("xmlns", "r", null, "http://schemas.openxmlformats.org/officeDocument/2006/relationships");
|
||||
xw.WriteStartElement("sheets");
|
||||
for (int i = 1; i <= sheetNames.Count; i++)
|
||||
{
|
||||
xw.WriteStartElement("sheet");
|
||||
xw.WriteAttributeString("name", sheetNames[i - 1]);
|
||||
xw.WriteAttributeString("sheetId", i.ToString());
|
||||
xw.WriteAttributeString("r", "id", null, "rId" + i);
|
||||
xw.WriteEndElement();
|
||||
}
|
||||
xw.WriteEndDocument();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write xl/_rels/workbook.xml.rels. This file will have the paths of the style and sheets.
|
||||
/// </summary>
|
||||
private void WriteWorkbookRel()
|
||||
{
|
||||
using (XmlWriter xw = AddEntry("xl/_rels/workbook.xml.rels"))
|
||||
{
|
||||
xw.WriteStartDocument();
|
||||
xw.WriteStartElement("Relationships", "http://schemas.openxmlformats.org/package/2006/relationships");
|
||||
|
||||
xw.WriteStartElement("Relationship");
|
||||
xw.WriteAttributeString("Id", "rId0");
|
||||
xw.WriteAttributeString("Type", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles");
|
||||
xw.WriteAttributeString("Target", "styles.xml");
|
||||
xw.WriteEndElement();
|
||||
|
||||
for (int i = 1; i <= sheetNames.Count; i++)
|
||||
{
|
||||
xw.WriteStartElement("Relationship");
|
||||
xw.WriteAttributeString("Id", "rId" + i);
|
||||
xw.WriteAttributeString("Type", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet");
|
||||
xw.WriteAttributeString("Target", "worksheets/sheet" + i + ".xml");
|
||||
xw.WriteEndElement();
|
||||
}
|
||||
xw.WriteEndElement();
|
||||
xw.WriteEndDocument();
|
||||
}
|
||||
}
|
||||
|
||||
// Write the xl/styles.xml
|
||||
private void WriteStyle()
|
||||
{
|
||||
// the style 0 is used for general case, style 1 for date, style 2 for time and style 3 for datetime see Enum Style
|
||||
// reference chain: (index start with 0)
|
||||
// <c>(in sheet1.xml) --> (by s) <cellXfs> --> (by xfId) <cellStyleXfs>
|
||||
// --> (by numFmtId) <numFmts>
|
||||
// that is <c s="1"></c> will reference the second element of <cellXfs> <xf numFmtId=""162"" xfId=""0"" applyNumberFormat=""1""/>
|
||||
// then, this xf reference numFmt by name and get formatCode "hh:mm:ss"
|
||||
|
||||
using (XmlWriter xw = AddEntry("xl/styles.xml"))
|
||||
{
|
||||
xw.WriteStartElement("styleSheet", "http://schemas.openxmlformats.org/spreadsheetml/2006/main");
|
||||
|
||||
xw.WriteStartElement("numFmts");
|
||||
xw.WriteAttributeString("count", "4");
|
||||
xw.WriteStartElement("numFmt");
|
||||
xw.WriteAttributeString("numFmtId", "166");
|
||||
xw.WriteAttributeString("formatCode", "yyyy-mm-dd");
|
||||
xw.WriteEndElement();
|
||||
xw.WriteStartElement("numFmt");
|
||||
xw.WriteAttributeString("numFmtId", "167");
|
||||
xw.WriteAttributeString("formatCode", "hh:mm:ss");
|
||||
xw.WriteEndElement();
|
||||
xw.WriteStartElement("numFmt");
|
||||
xw.WriteAttributeString("numFmtId", "168");
|
||||
xw.WriteAttributeString("formatCode", "yyyy-mm-dd hh:mm:ss");
|
||||
xw.WriteEndElement();
|
||||
xw.WriteStartElement("numFmt");
|
||||
xw.WriteAttributeString("numFmtId", "169");
|
||||
xw.WriteAttributeString("formatCode", "[h]:mm:ss");
|
||||
xw.WriteEndElement();
|
||||
xw.WriteEndElement(); //mumFmts
|
||||
|
||||
|
||||
xw.WriteStartElement("fonts");
|
||||
xw.WriteAttributeString("count", "1");
|
||||
xw.WriteStartElement("font");
|
||||
xw.WriteStartElement("sz");
|
||||
xw.WriteAttributeString("val", "11");
|
||||
xw.WriteEndElement();
|
||||
xw.WriteStartElement("color");
|
||||
xw.WriteAttributeString("theme", "1");
|
||||
xw.WriteEndElement();
|
||||
xw.WriteStartElement("name");
|
||||
xw.WriteAttributeString("val", "Calibri");
|
||||
xw.WriteEndElement();
|
||||
xw.WriteStartElement("family");
|
||||
xw.WriteAttributeString("val", "2");
|
||||
xw.WriteEndElement();
|
||||
xw.WriteStartElement("scheme");
|
||||
xw.WriteAttributeString("val", "minor");
|
||||
xw.WriteEndElement();
|
||||
xw.WriteEndElement(); // font
|
||||
xw.WriteEndElement(); // fonts
|
||||
|
||||
xw.WriteStartElement("fills");
|
||||
xw.WriteAttributeString("count", "1");
|
||||
xw.WriteStartElement("fill");
|
||||
xw.WriteStartElement("patternFill");
|
||||
xw.WriteAttributeString("patternType", "none");
|
||||
xw.WriteEndElement(); // patternFill
|
||||
xw.WriteEndElement(); // fill
|
||||
xw.WriteEndElement(); // fills
|
||||
|
||||
xw.WriteStartElement("borders");
|
||||
xw.WriteAttributeString("count", "1");
|
||||
xw.WriteStartElement("border");
|
||||
xw.WriteElementString("left", null);
|
||||
xw.WriteElementString("right", null);
|
||||
xw.WriteElementString("top", null);
|
||||
xw.WriteElementString("bottom", null);
|
||||
xw.WriteElementString("diagonal", null);
|
||||
xw.WriteEndElement(); // board
|
||||
xw.WriteEndElement(); // borders
|
||||
|
||||
xw.WriteStartElement("cellStyleXfs");
|
||||
xw.WriteAttributeString("count", "1");
|
||||
xw.WriteStartElement("xf");
|
||||
xw.WriteAttributeString("numFmtId", "0");
|
||||
xw.WriteAttributeString("fontId", "0");
|
||||
xw.WriteAttributeString("fillId", "0");
|
||||
xw.WriteAttributeString("borderId", "0");
|
||||
xw.WriteEndElement(); // xf
|
||||
xw.WriteEndElement(); // cellStyleXfs
|
||||
|
||||
xw.WriteStartElement("cellXfs");
|
||||
xw.WriteAttributeString("count", "5");
|
||||
xw.WriteStartElement("xf");
|
||||
xw.WriteAttributeString("xfId", "0");
|
||||
xw.WriteEndElement();
|
||||
xw.WriteStartElement("xf");
|
||||
xw.WriteAttributeString("numFmtId", "166");
|
||||
xw.WriteAttributeString("xfId", "0");
|
||||
xw.WriteAttributeString("applyNumberFormat", "1");
|
||||
xw.WriteEndElement();
|
||||
xw.WriteStartElement("xf");
|
||||
xw.WriteAttributeString("numFmtId", "167");
|
||||
xw.WriteAttributeString("xfId", "0");
|
||||
xw.WriteAttributeString("applyNumberFormat", "1");
|
||||
xw.WriteEndElement();
|
||||
xw.WriteStartElement("xf");
|
||||
xw.WriteAttributeString("numFmtId", "168");
|
||||
xw.WriteAttributeString("xfId", "0");
|
||||
xw.WriteAttributeString("applyNumberFormat", "1");
|
||||
xw.WriteEndElement();
|
||||
xw.WriteStartElement("xf");
|
||||
xw.WriteAttributeString("numFmtId", "169");
|
||||
xw.WriteAttributeString("xfId", "0");
|
||||
xw.WriteAttributeString("applyNumberFormat", "1");
|
||||
xw.WriteEndElement();
|
||||
xw.WriteEndElement(); // cellXfs
|
||||
|
||||
xw.WriteStartElement("cellStyles");
|
||||
xw.WriteAttributeString("count", "1");
|
||||
xw.WriteStartElement("cellStyle");
|
||||
xw.WriteAttributeString("name", "Normal");
|
||||
xw.WriteAttributeString("builtinId", "0");
|
||||
xw.WriteAttributeString("xfId", "0");
|
||||
xw.WriteEndElement(); // cellStyle
|
||||
xw.WriteEndElement(); // cellStyles
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
//
|
||||
// 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.IO;
|
||||
using Microsoft.Kusto.ServiceLayer.QueryExecution.Contracts;
|
||||
using Microsoft.Kusto.ServiceLayer.SqlContext;
|
||||
using Microsoft.Kusto.ServiceLayer.Utility;
|
||||
|
||||
namespace Microsoft.Kusto.ServiceLayer.QueryExecution.DataStorage
|
||||
{
|
||||
public class SaveAsJsonFileStreamFactory : IFileStreamFactory
|
||||
{
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// Settings for query execution
|
||||
/// </summary>
|
||||
public QueryExecutionSettings QueryExecutionSettings { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Parameters for the save as JSON request
|
||||
/// </summary>
|
||||
public SaveResultsAsJsonRequestParams SaveRequestParams { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// File names are not meant to be created with this factory.
|
||||
/// </summary>
|
||||
/// <exception cref="NotImplementedException">Thrown all times</exception>
|
||||
[Obsolete]
|
||||
public string CreateFile()
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a new service buffer reader for reading results back in from the temporary buffer files, file share is ReadWrite to allow concurrent reads/writes to the file.
|
||||
/// </summary>
|
||||
/// <param name="fileName">Path to the temp buffer file</param>
|
||||
/// <returns>Stream reader</returns>
|
||||
public IFileStreamReader GetReader(string fileName)
|
||||
{
|
||||
return new ServiceBufferFileStreamReader(new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite), QueryExecutionSettings);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a new JSON writer for writing results to a JSON file, file share is ReadWrite to allow concurrent reads/writes to the file.
|
||||
/// </summary>
|
||||
/// <param name="fileName">Path to the JSON output file</param>
|
||||
/// <returns>Stream writer</returns>
|
||||
public IFileStreamWriter GetWriter(string fileName)
|
||||
{
|
||||
return new SaveAsJsonFileStreamWriter(new FileStream(fileName, FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite), SaveRequestParams);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Safely deletes the file
|
||||
/// </summary>
|
||||
/// <param name="fileName">Path to the file to delete</param>
|
||||
public void DisposeFile(string fileName)
|
||||
{
|
||||
FileUtilities.SafeFileDelete(fileName);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
//
|
||||
// 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.IO;
|
||||
using Microsoft.Kusto.ServiceLayer.QueryExecution.Contracts;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Microsoft.Kusto.ServiceLayer.QueryExecution.DataStorage
|
||||
{
|
||||
/// <summary>
|
||||
/// Writer for writing rows of results to a JSON file.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This implements its own IDisposable because the cleanup logic closes the array that was
|
||||
/// created when the writer was created. Since this behavior is different than the standard
|
||||
/// file stream cleanup, the extra Dispose method was added.
|
||||
/// </remarks>
|
||||
public class SaveAsJsonFileStreamWriter : SaveAsStreamWriter, IDisposable
|
||||
{
|
||||
#region Member Variables
|
||||
|
||||
private readonly StreamWriter streamWriter;
|
||||
private readonly JsonWriter jsonWriter;
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Constructor, writes the header to the file, chains into the base constructor
|
||||
/// </summary>
|
||||
/// <param name="stream">FileStream to access the JSON file output</param>
|
||||
/// <param name="requestParams">JSON save as request parameters</param>
|
||||
public SaveAsJsonFileStreamWriter(Stream stream, SaveResultsRequestParams requestParams)
|
||||
: base(stream, requestParams)
|
||||
{
|
||||
// Setup the internal state
|
||||
streamWriter = new StreamWriter(stream);
|
||||
jsonWriter = new JsonTextWriter(streamWriter);
|
||||
jsonWriter.Formatting = Formatting.Indented;
|
||||
|
||||
// Write the header of the file
|
||||
jsonWriter.WriteStartArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a row of data as a JSON object
|
||||
/// </summary>
|
||||
/// <param name="row">The data of the row to output to the file</param>
|
||||
/// <param name="columns">
|
||||
/// The entire list of columns for the result set. They will be filtered down as per the
|
||||
/// request params.
|
||||
/// </param>
|
||||
public override void WriteRow(IList<DbCellValue> row, IList<DbColumnWrapper> columns)
|
||||
{
|
||||
// Write the header for the object
|
||||
jsonWriter.WriteStartObject();
|
||||
|
||||
// Write the items out as properties
|
||||
int columnStart = ColumnStartIndex ?? 0;
|
||||
int columnEnd = (ColumnEndIndex != null) ? ColumnEndIndex.Value + 1 : columns.Count;
|
||||
for (int i = columnStart; i < columnEnd; i++)
|
||||
{
|
||||
jsonWriter.WritePropertyName(columns[i].ColumnName);
|
||||
if (row[i].RawObject == null)
|
||||
{
|
||||
jsonWriter.WriteNull();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Try converting to column type
|
||||
try
|
||||
{
|
||||
var value = Convert.ChangeType(row[i].DisplayValue, columns[i].DataType);
|
||||
jsonWriter.WriteValue(value);
|
||||
}
|
||||
// Default column type as string
|
||||
catch
|
||||
{
|
||||
jsonWriter.WriteValue(row[i].DisplayValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write the footer for the object
|
||||
jsonWriter.WriteEndObject();
|
||||
}
|
||||
|
||||
private bool disposed = false;
|
||||
/// <summary>
|
||||
/// Disposes the writer by closing up the array that contains the row objects
|
||||
/// </summary>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposed)
|
||||
return;
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
// Write the footer of the file
|
||||
jsonWriter.WriteEndArray();
|
||||
// This closes the underlying stream, so we needn't call close on the underlying stream explicitly
|
||||
jsonWriter.Close();
|
||||
}
|
||||
disposed = true;
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
//
|
||||
// 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.IO;
|
||||
using Microsoft.Kusto.ServiceLayer.QueryExecution.Contracts;
|
||||
|
||||
namespace Microsoft.Kusto.ServiceLayer.QueryExecution.DataStorage
|
||||
{
|
||||
/// <summary>
|
||||
/// Abstract class for implementing writers that save results to file. Stores some basic info
|
||||
/// that all save as writer would need.
|
||||
/// </summary>
|
||||
public abstract class SaveAsStreamWriter : IFileStreamWriter
|
||||
{
|
||||
/// <summary>
|
||||
/// Stores the internal state for the writer that will be necessary for any writer.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream that will be written to</param>
|
||||
/// <param name="requestParams">The SaveAs request parameters</param>
|
||||
protected SaveAsStreamWriter(Stream stream, SaveResultsRequestParams requestParams)
|
||||
{
|
||||
FileStream = stream;
|
||||
var saveParams = requestParams;
|
||||
if (requestParams.IsSaveSelection)
|
||||
{
|
||||
// ReSharper disable PossibleInvalidOperationException IsSaveSelection verifies these values exist
|
||||
ColumnStartIndex = saveParams.ColumnStartIndex.Value;
|
||||
ColumnEndIndex = saveParams.ColumnEndIndex.Value;
|
||||
ColumnCount = saveParams.ColumnEndIndex.Value - saveParams.ColumnStartIndex.Value + 1;
|
||||
// ReSharper restore PossibleInvalidOperationException
|
||||
}
|
||||
}
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// Index of the first column to write to the output file
|
||||
/// </summary>
|
||||
protected int? ColumnStartIndex { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of columns to write to the output file
|
||||
/// </summary>
|
||||
protected int? ColumnCount { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Index of the last column to write to the output file
|
||||
/// </summary>
|
||||
protected int? ColumnEndIndex { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The file stream to use to write the output file
|
||||
/// </summary>
|
||||
protected Stream FileStream { get; private set; }
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Not implemented, do not use.
|
||||
/// </summary>
|
||||
[Obsolete]
|
||||
public int WriteRow(StorageDataReader dataReader)
|
||||
{
|
||||
throw new InvalidOperationException("This type of writer is meant to write values from a list of cell values only.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a row of data to the output file using the format provided by the implementing class.
|
||||
/// </summary>
|
||||
/// <param name="row">The row of data to output</param>
|
||||
/// <param name="columns">The list of columns to output</param>
|
||||
public abstract void WriteRow(IList<DbCellValue> row, IList<DbColumnWrapper> columns);
|
||||
|
||||
/// <summary>
|
||||
/// Not implemented, do not use.
|
||||
/// </summary>
|
||||
[Obsolete]
|
||||
public void Seek(long offset)
|
||||
{
|
||||
throw new InvalidOperationException("SaveAs writers are meant to be written once contiguously.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flushes the file stream buffer
|
||||
/// </summary>
|
||||
public void FlushBuffer()
|
||||
{
|
||||
FileStream.Flush();
|
||||
}
|
||||
|
||||
#region IDisposable Implementation
|
||||
|
||||
private bool disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the instance by flushing and closing the file stream
|
||||
/// </summary>
|
||||
/// <param name="disposing"></param>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (disposed)
|
||||
return;
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
FileStream.Dispose();
|
||||
}
|
||||
disposed = true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
//
|
||||
// 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.IO;
|
||||
using Microsoft.Kusto.ServiceLayer.QueryExecution.Contracts;
|
||||
using Microsoft.Kusto.ServiceLayer.SqlContext;
|
||||
using Microsoft.Kusto.ServiceLayer.Utility;
|
||||
|
||||
namespace Microsoft.Kusto.ServiceLayer.QueryExecution.DataStorage
|
||||
{
|
||||
public class SaveAsXmlFileStreamFactory : IFileStreamFactory
|
||||
{
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// Settings for query execution
|
||||
/// </summary>
|
||||
public QueryExecutionSettings QueryExecutionSettings { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Parameters for the save as XML request
|
||||
/// </summary>
|
||||
public SaveResultsAsXmlRequestParams SaveRequestParams { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// File names are not meant to be created with this factory.
|
||||
/// </summary>
|
||||
/// <exception cref="NotImplementedException">Thrown all times</exception>
|
||||
[Obsolete]
|
||||
public string CreateFile()
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a new service buffer reader for reading results back in from the temporary buffer files, file share is ReadWrite to allow concurrent reads/writes to the file.
|
||||
/// </summary>
|
||||
/// <param name="fileName">Path to the temp buffer file</param>
|
||||
/// <returns>Stream reader</returns>
|
||||
public IFileStreamReader GetReader(string fileName)
|
||||
{
|
||||
return new ServiceBufferFileStreamReader(new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite), QueryExecutionSettings);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a new XML writer for writing results to a XML file, file share is ReadWrite to allow concurrent reads/writes to the file.
|
||||
/// </summary>
|
||||
/// <param name="fileName">Path to the XML output file</param>
|
||||
/// <returns>Stream writer</returns>
|
||||
public IFileStreamWriter GetWriter(string fileName)
|
||||
{
|
||||
return new SaveAsXmlFileStreamWriter(new FileStream(fileName, FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite), SaveRequestParams);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Safely deletes the file
|
||||
/// </summary>
|
||||
/// <param name="fileName">Path to the file to delete</param>
|
||||
public void DisposeFile(string fileName)
|
||||
{
|
||||
FileUtilities.SafeFileDelete(fileName);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
//
|
||||
// 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.IO;
|
||||
using System.Text;
|
||||
using System.Xml;
|
||||
using Microsoft.Kusto.ServiceLayer.QueryExecution.Contracts;
|
||||
|
||||
namespace Microsoft.Kusto.ServiceLayer.QueryExecution.DataStorage
|
||||
{
|
||||
/// <summary>
|
||||
/// Writer for writing rows of results to a XML file.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This implements its own IDisposable because the cleanup logic closes the element that was
|
||||
/// created when the writer was created. Since this behavior is different than the standard
|
||||
/// file stream cleanup, the extra Dispose method was added.
|
||||
/// </remarks>
|
||||
public class SaveAsXmlFileStreamWriter : SaveAsStreamWriter, IDisposable
|
||||
{
|
||||
// Root element name for the output XML
|
||||
private const string RootElementTag = "data";
|
||||
|
||||
// Item element name which will be used for every row
|
||||
private const string ItemElementTag = "row";
|
||||
|
||||
#region Member Variables
|
||||
|
||||
private readonly XmlTextWriter xmlTextWriter;
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Constructor, writes the header to the file, chains into the base constructor
|
||||
/// </summary>
|
||||
/// <param name="stream">FileStream to access the JSON file output</param>
|
||||
/// <param name="requestParams">XML save as request parameters</param>
|
||||
public SaveAsXmlFileStreamWriter(Stream stream, SaveResultsAsXmlRequestParams requestParams)
|
||||
: base(stream, requestParams)
|
||||
{
|
||||
// Setup the internal state
|
||||
var encoding = GetEncoding(requestParams);
|
||||
xmlTextWriter = new XmlTextWriter(stream, encoding);
|
||||
xmlTextWriter.Formatting = requestParams.Formatted ? Formatting.Indented : Formatting.None;
|
||||
|
||||
//Start the document and the root element
|
||||
xmlTextWriter.WriteStartDocument();
|
||||
xmlTextWriter.WriteStartElement(RootElementTag);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a row of data as a XML object
|
||||
/// </summary>
|
||||
/// <param name="row">The data of the row to output to the file</param>
|
||||
/// <param name="columns">
|
||||
/// The entire list of columns for the result set. They will be filtered down as per the
|
||||
/// request params.
|
||||
/// </param>
|
||||
public override void WriteRow(IList<DbCellValue> row, IList<DbColumnWrapper> columns)
|
||||
{
|
||||
// Write the header for the object
|
||||
xmlTextWriter.WriteStartElement(ItemElementTag);
|
||||
|
||||
// Write the items out as properties
|
||||
int columnStart = ColumnStartIndex ?? 0;
|
||||
int columnEnd = ColumnEndIndex + 1 ?? columns.Count;
|
||||
for (int i = columnStart; i < columnEnd; i++)
|
||||
{
|
||||
// Write the column name as item tag
|
||||
xmlTextWriter.WriteStartElement(columns[i].ColumnName);
|
||||
|
||||
if (row[i].RawObject != null)
|
||||
{
|
||||
xmlTextWriter.WriteString(row[i].DisplayValue);
|
||||
}
|
||||
|
||||
// End the item tag
|
||||
xmlTextWriter.WriteEndElement();
|
||||
}
|
||||
|
||||
// Write the footer for the object
|
||||
xmlTextWriter.WriteEndElement();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the encoding for the XML file according to <param name="requestParams"></param>
|
||||
/// </summary>
|
||||
/// <param name="requestParams">XML save as request parameters</param>
|
||||
/// <returns></returns>
|
||||
private Encoding GetEncoding(SaveResultsAsXmlRequestParams requestParams)
|
||||
{
|
||||
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||
Encoding encoding;
|
||||
try
|
||||
{
|
||||
if (int.TryParse(requestParams.Encoding, out var codepage))
|
||||
{
|
||||
encoding = Encoding.GetEncoding(codepage);
|
||||
}
|
||||
else
|
||||
{
|
||||
encoding = Encoding.GetEncoding(requestParams.Encoding);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Fallback encoding when specified codepage is invalid
|
||||
encoding = Encoding.GetEncoding("utf-8");
|
||||
}
|
||||
|
||||
return encoding;
|
||||
}
|
||||
|
||||
private bool disposed = false;
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the writer by closing up the element that contains the row objects
|
||||
/// </summary>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposed)
|
||||
return;
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
// Write the footer of the file
|
||||
xmlTextWriter.WriteEndElement();
|
||||
xmlTextWriter.WriteEndDocument();
|
||||
|
||||
xmlTextWriter.Close();
|
||||
xmlTextWriter.Dispose();
|
||||
}
|
||||
|
||||
disposed = true;
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using System.IO;
|
||||
using Microsoft.Kusto.ServiceLayer.SqlContext;
|
||||
using Microsoft.Kusto.ServiceLayer.Utility;
|
||||
|
||||
namespace Microsoft.Kusto.ServiceLayer.QueryExecution.DataStorage
|
||||
{
|
||||
/// <summary>
|
||||
/// Factory that creates file reader/writers that process rows in an internal, non-human readable file format
|
||||
/// </summary>
|
||||
public class ServiceBufferFileStreamFactory : IFileStreamFactory
|
||||
{
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// The settings for query execution
|
||||
/// </summary>
|
||||
public QueryExecutionSettings ExecutionSettings { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new temporary file
|
||||
/// </summary>
|
||||
/// <returns>The name of the temporary file</returns>
|
||||
public string CreateFile()
|
||||
{
|
||||
return Path.GetTempFileName();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="ServiceBufferFileStreamReader"/> for reading values back from
|
||||
/// an SSMS formatted buffer file, file share is ReadWrite to allow concurrent reads/writes to the file.
|
||||
/// </summary>
|
||||
/// <param name="fileName">The file to read values from</param>
|
||||
/// <returns>A <see cref="ServiceBufferFileStreamReader"/></returns>
|
||||
public IFileStreamReader GetReader(string fileName)
|
||||
{
|
||||
return new ServiceBufferFileStreamReader(new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite), ExecutionSettings);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="ServiceBufferFileStreamWriter"/> for writing values out to an
|
||||
/// SSMS formatted buffer file, file share is ReadWrite to allow concurrent reads/writes to the file.
|
||||
/// </summary>
|
||||
/// <param name="fileName">The file to write values to</param>
|
||||
/// <returns>A <see cref="ServiceBufferFileStreamWriter"/></returns>
|
||||
public IFileStreamWriter GetWriter(string fileName)
|
||||
{
|
||||
return new ServiceBufferFileStreamWriter(new FileStream(fileName, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite), ExecutionSettings);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes of a file created via this factory
|
||||
/// </summary>
|
||||
/// <param name="fileName">The file to dispose of</param>
|
||||
public void DisposeFile(string fileName)
|
||||
{
|
||||
FileUtilities.SafeFileDelete(fileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,552 @@
|
||||
//
|
||||
// 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.SqlTypes;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using Microsoft.Kusto.ServiceLayer.QueryExecution.Contracts;
|
||||
using Microsoft.Kusto.ServiceLayer.SqlContext;
|
||||
using Microsoft.SqlTools.Utility;
|
||||
|
||||
namespace Microsoft.Kusto.ServiceLayer.QueryExecution.DataStorage
|
||||
{
|
||||
/// <summary>
|
||||
/// Reader for service buffer formatted file streams
|
||||
/// </summary>
|
||||
public class ServiceBufferFileStreamReader : IFileStreamReader
|
||||
{
|
||||
|
||||
#region Constants
|
||||
|
||||
private const int DefaultBufferSize = 8192;
|
||||
private const string DateFormatString = "yyyy-MM-dd";
|
||||
private const string TimeFormatString = "HH:mm:ss";
|
||||
|
||||
#endregion
|
||||
|
||||
#region Member Variables
|
||||
|
||||
private delegate FileStreamReadResult ReadMethod(long fileOffset, long rowId, DbColumnWrapper column);
|
||||
|
||||
private byte[] buffer;
|
||||
|
||||
private readonly QueryExecutionSettings executionSettings;
|
||||
|
||||
private readonly Stream fileStream;
|
||||
|
||||
private readonly Dictionary<Type, ReadMethod> readMethods;
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new ServiceBufferFileStreamReader and initializes its state
|
||||
/// </summary>
|
||||
/// <param name="stream">The filestream to read from</param>
|
||||
/// <param name="settings">The query execution settings</param>
|
||||
public ServiceBufferFileStreamReader(Stream stream, QueryExecutionSettings settings)
|
||||
{
|
||||
Validate.IsNotNull(nameof(stream), stream);
|
||||
Validate.IsNotNull(nameof(settings), settings);
|
||||
|
||||
// Open file for reading/writing
|
||||
if (!stream.CanRead || !stream.CanSeek)
|
||||
{
|
||||
throw new InvalidOperationException("Stream must be readable and seekable");
|
||||
}
|
||||
fileStream = stream;
|
||||
|
||||
executionSettings = settings;
|
||||
|
||||
// Create internal buffer
|
||||
buffer = new byte[DefaultBufferSize];
|
||||
|
||||
// Create the methods that will be used to read back
|
||||
readMethods = new Dictionary<Type, ReadMethod>
|
||||
{
|
||||
{typeof(string), (o, id, col) => ReadString(o, id)},
|
||||
{typeof(short), (o, id, col) => ReadInt16(o, id)},
|
||||
{typeof(int), (o, id, col) => ReadInt32(o, id)},
|
||||
{typeof(long), (o, id, col) => ReadInt64(o, id)},
|
||||
{typeof(byte), (o, id, col) => ReadByte(o, id)},
|
||||
{typeof(char), (o, id, col) => ReadChar(o, id)},
|
||||
{typeof(bool), (o, id, col) => ReadBoolean(o, id)},
|
||||
{typeof(double), (o, id, col) => ReadDouble(o, id)},
|
||||
{typeof(float), (o, id, col) => ReadSingle(o, id)},
|
||||
{typeof(decimal), (o, id, col) => ReadDecimal(o, id)},
|
||||
{typeof(DateTime), ReadDateTime},
|
||||
{typeof(DateTimeOffset), (o, id, col) => ReadDateTimeOffset(o, id)},
|
||||
{typeof(TimeSpan), (o, id, col) => ReadTimeSpan(o, id)},
|
||||
{typeof(byte[]), (o, id, col) => ReadBytes(o, id)},
|
||||
{typeof(Guid), (o, id, col) => ReadGuid(o, id)},
|
||||
|
||||
{typeof(SqlString), (o, id, col) => ReadString(o, id)},
|
||||
{typeof(SqlInt16), (o, id, col) => ReadInt16(o, id)},
|
||||
{typeof(SqlInt32), (o, id, col) => ReadInt32(o, id)},
|
||||
{typeof(SqlInt64), (o, id, col) => ReadInt64(o, id)},
|
||||
{typeof(SqlByte), (o, id, col) => ReadByte(o, id)},
|
||||
{typeof(SqlBoolean), (o, id, col) => ReadBoolean(o, id)},
|
||||
{typeof(SqlDouble), (o, id, col) => ReadDouble(o, id)},
|
||||
{typeof(SqlSingle), (o, id, col) => ReadSingle(o, id)},
|
||||
{typeof(SqlDecimal), (o, id, col) => ReadSqlDecimal(o, id)},
|
||||
{typeof(SqlDateTime), ReadDateTime},
|
||||
{typeof(SqlBytes), (o, id, col) => ReadBytes(o, id)},
|
||||
{typeof(SqlBinary), (o, id, col) => ReadBytes(o, id)},
|
||||
{typeof(SqlGuid), (o, id, col) => ReadGuid(o, id)},
|
||||
{typeof(SqlMoney), (o, id, col) => ReadMoney(o, id)},
|
||||
};
|
||||
}
|
||||
|
||||
#region IFileStreamStorage Implementation
|
||||
|
||||
/// <summary>
|
||||
/// Reads a row from the file, based on the columns provided
|
||||
/// </summary>
|
||||
/// <param name="fileOffset">Offset into the file where the row starts</param>
|
||||
/// <param name="rowId">Internal ID of the row to set for all cells in this row</param>
|
||||
/// <param name="columns">The columns that were encoded</param>
|
||||
/// <returns>The objects from the row, ready for output to the client</returns>
|
||||
public IList<DbCellValue> ReadRow(long fileOffset, long rowId, IEnumerable<DbColumnWrapper> columns)
|
||||
{
|
||||
// Initialize for the loop
|
||||
long currentFileOffset = fileOffset;
|
||||
List<DbCellValue> results = new List<DbCellValue>();
|
||||
|
||||
// Iterate over the columns
|
||||
Type colType;
|
||||
foreach (DbColumnWrapper column in columns)
|
||||
{
|
||||
colType = column.DataType;
|
||||
|
||||
// Use the right read function for the type to read the data from the file
|
||||
ReadMethod readFunc;
|
||||
if (!readMethods.TryGetValue(colType, out readFunc))
|
||||
{
|
||||
// Treat everything else as a string
|
||||
readFunc = readMethods[typeof(string)];
|
||||
}
|
||||
FileStreamReadResult result = readFunc(currentFileOffset, rowId, column);
|
||||
currentFileOffset += result.TotalLength;
|
||||
results.Add(result.Value);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Helpers
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new buffer that is of the specified length if the buffer is not already
|
||||
/// at least as long as specified.
|
||||
/// </summary>
|
||||
/// <param name="newBufferLength">The minimum buffer size</param>
|
||||
private void AssureBufferLength(int newBufferLength)
|
||||
{
|
||||
if (buffer.Length < newBufferLength)
|
||||
{
|
||||
buffer = new byte[newBufferLength];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the value of a cell from the file wrapper, checks to see if it null using
|
||||
/// <paramref name="isNullFunc"/>, and converts it to the proper output type using
|
||||
/// <paramref name="convertFunc"/>.
|
||||
/// </summary>
|
||||
/// <param name="offset">Offset into the file to read from</param>
|
||||
/// <param name="rowId">Internal ID of the row to set on all cells in this row</param>
|
||||
/// <param name="convertFunc">Function to use to convert the buffer to the target type</param>
|
||||
/// <param name="isNullFunc">
|
||||
/// If provided, this function will be used to determine if the value is null
|
||||
/// </param>
|
||||
/// <param name="toStringFunc">Optional function to use to convert the object to a string.</param>
|
||||
/// <param name="setInvariantCultureDisplayValue">Optional parameter indicates whether the culture invariant display value should be provided.</param>
|
||||
/// <typeparam name="T">The expected type of the cell. Used to keep the code honest</typeparam>
|
||||
/// <returns>The object, a display value, and the length of the value + its length</returns>
|
||||
private FileStreamReadResult ReadCellHelper<T>(long offset, long rowId,
|
||||
Func<int, T> convertFunc,
|
||||
Func<int, bool> isNullFunc = null,
|
||||
Func<T, string> toStringFunc = null,
|
||||
bool setInvariantCultureDisplayValue = false)
|
||||
{
|
||||
LengthResult length = ReadLength(offset);
|
||||
DbCellValue result = new DbCellValue { RowId = rowId };
|
||||
|
||||
if (isNullFunc == null ? length.ValueLength == 0 : isNullFunc(length.TotalLength))
|
||||
{
|
||||
result.RawObject = null;
|
||||
result.DisplayValue = SR.QueryServiceCellNull;
|
||||
result.IsNull = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
AssureBufferLength(length.ValueLength);
|
||||
fileStream.Read(buffer, 0, length.ValueLength);
|
||||
T resultObject = convertFunc(length.ValueLength);
|
||||
result.RawObject = resultObject;
|
||||
result.DisplayValue = toStringFunc == null ? result.RawObject.ToString() : toStringFunc(resultObject);
|
||||
if (setInvariantCultureDisplayValue)
|
||||
{
|
||||
string icDisplayValue = string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0}", result.RawObject);
|
||||
|
||||
// Only set the value when it is different from the DisplayValue to reduce the size of the result
|
||||
//
|
||||
if (icDisplayValue != result.DisplayValue)
|
||||
{
|
||||
result.InvariantCultureDisplayValue = icDisplayValue;
|
||||
}
|
||||
}
|
||||
result.IsNull = false;
|
||||
}
|
||||
|
||||
return new FileStreamReadResult(result, length.TotalLength);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a short from the file at the offset provided
|
||||
/// </summary>
|
||||
/// <param name="fileOffset">Offset into the file to read the short from</param>
|
||||
/// <param name="rowId">Internal ID of the row that will be stored in the cell</param>
|
||||
/// <returns>A short</returns>
|
||||
internal FileStreamReadResult ReadInt16(long fileOffset, long rowId)
|
||||
{
|
||||
return ReadCellHelper(fileOffset, rowId, length => BitConverter.ToInt16(buffer, 0));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a int from the file at the offset provided
|
||||
/// </summary>
|
||||
/// <param name="fileOffset">Offset into the file to read the int from</param>
|
||||
/// <param name="rowId">Internal ID of the row that will be stored in the cell</param>
|
||||
/// <returns>An int</returns>
|
||||
internal FileStreamReadResult ReadInt32(long fileOffset, long rowId)
|
||||
{
|
||||
return ReadCellHelper(fileOffset, rowId, length => BitConverter.ToInt32(buffer, 0));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a long from the file at the offset provided
|
||||
/// </summary>
|
||||
/// <param name="fileOffset">Offset into the file to read the long from</param>
|
||||
/// <param name="rowId">Internal ID of the row that will be stored in the cell</param>
|
||||
/// <returns>A long</returns>
|
||||
internal FileStreamReadResult ReadInt64(long fileOffset, long rowId)
|
||||
{
|
||||
return ReadCellHelper(fileOffset, rowId, length => BitConverter.ToInt64(buffer, 0));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a byte from the file at the offset provided
|
||||
/// </summary>
|
||||
/// <param name="fileOffset">Offset into the file to read the byte from</param>
|
||||
/// <param name="rowId">Internal ID of the row that will be stored in the cell</param>
|
||||
/// <returns>A byte</returns>
|
||||
internal FileStreamReadResult ReadByte(long fileOffset, long rowId)
|
||||
{
|
||||
return ReadCellHelper(fileOffset, rowId, length => buffer[0]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a char from the file at the offset provided
|
||||
/// </summary>
|
||||
/// <param name="fileOffset">Offset into the file to read the char from</param>
|
||||
/// <param name="rowId">Internal ID of the row that will be stored in the cell</param>
|
||||
/// <returns>A char</returns>
|
||||
internal FileStreamReadResult ReadChar(long fileOffset, long rowId)
|
||||
{
|
||||
return ReadCellHelper(fileOffset, rowId, length => BitConverter.ToChar(buffer, 0));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a bool from the file at the offset provided
|
||||
/// </summary>
|
||||
/// <param name="fileOffset">Offset into the file to read the bool from</param>
|
||||
/// <param name="rowId">Internal ID of the row that will be stored in the cell</param>
|
||||
/// <returns>A bool</returns>
|
||||
internal FileStreamReadResult ReadBoolean(long fileOffset, long rowId)
|
||||
{
|
||||
// Override the stringifier with numeric values if the user prefers that
|
||||
return ReadCellHelper(fileOffset, rowId, length => buffer[0] == 0x1,
|
||||
toStringFunc: val => executionSettings.DisplayBitAsNumber
|
||||
? val ? "1" : "0"
|
||||
: val.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a single from the file at the offset provided
|
||||
/// </summary>
|
||||
/// <param name="fileOffset">Offset into the file to read the single from</param>
|
||||
/// <param name="rowId">Internal ID of the row that will be stored in the cell</param>
|
||||
/// <returns>A single</returns>
|
||||
internal FileStreamReadResult ReadSingle(long fileOffset, long rowId)
|
||||
{
|
||||
return ReadCellHelper(fileOffset, rowId, length => BitConverter.ToSingle(buffer, 0), setInvariantCultureDisplayValue: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a double from the file at the offset provided
|
||||
/// </summary>
|
||||
/// <param name="fileOffset">Offset into the file to read the double from</param>
|
||||
/// <param name="rowId">Internal ID of the row that will be stored in the cell</param>
|
||||
/// <returns>A double</returns>
|
||||
internal FileStreamReadResult ReadDouble(long fileOffset, long rowId)
|
||||
{
|
||||
return ReadCellHelper(fileOffset, rowId, length => BitConverter.ToDouble(buffer, 0), setInvariantCultureDisplayValue: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a SqlDecimal from the file at the offset provided
|
||||
/// </summary>
|
||||
/// <param name="offset">Offset into the file to read the SqlDecimal from</param>
|
||||
/// <returns>A SqlDecimal</returns>
|
||||
internal FileStreamReadResult ReadSqlDecimal(long offset, long rowId)
|
||||
{
|
||||
return ReadCellHelper(offset, rowId, length =>
|
||||
{
|
||||
int[] arrInt32 = new int[(length - 3) / 4];
|
||||
Buffer.BlockCopy(buffer, 3, arrInt32, 0, length - 3);
|
||||
return new SqlDecimal(buffer[0], buffer[1], buffer[2] == 1, arrInt32);
|
||||
}, setInvariantCultureDisplayValue: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a decimal from the file at the offset provided
|
||||
/// </summary>
|
||||
/// <param name="offset">Offset into the file to read the decimal from</param>
|
||||
/// <returns>A decimal</returns>
|
||||
internal FileStreamReadResult ReadDecimal(long offset, long rowId)
|
||||
{
|
||||
return ReadCellHelper(offset, rowId, length =>
|
||||
{
|
||||
int[] arrInt32 = new int[length / 4];
|
||||
Buffer.BlockCopy(buffer, 0, arrInt32, 0, length);
|
||||
return new decimal(arrInt32);
|
||||
}, setInvariantCultureDisplayValue: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a DateTime from the file at the offset provided
|
||||
/// </summary>
|
||||
/// <param name="offset">Offset into the file to read the DateTime from</param>
|
||||
/// <param name="rowId">Internal ID of the row that will be stored in the cell</param>
|
||||
/// <param name="col">Column metadata, used for determining what precision to output</param>
|
||||
/// <returns>A DateTime</returns>
|
||||
internal FileStreamReadResult ReadDateTime(long offset, long rowId, DbColumnWrapper col)
|
||||
{
|
||||
return ReadCellHelper(offset, rowId, length =>
|
||||
{
|
||||
long ticks = BitConverter.ToInt64(buffer, 0);
|
||||
return new DateTime(ticks);
|
||||
|
||||
}, null, dt =>
|
||||
{
|
||||
// Switch based on the type of column
|
||||
string formatString;
|
||||
// For anything else that returns as a CLR DateTime, just show date and time
|
||||
formatString = $"{DateFormatString} {TimeFormatString}";
|
||||
|
||||
return dt.ToString(formatString);
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a DateTimeOffset from the file at the offset provided
|
||||
/// </summary>
|
||||
/// <param name="offset">Offset into the file to read the DateTimeOffset from</param>
|
||||
/// <param name="rowId">Internal ID of the row that will be stored in the cell</param>
|
||||
/// <returns>A DateTimeOffset</returns>
|
||||
internal FileStreamReadResult ReadDateTimeOffset(long offset, long rowId)
|
||||
{
|
||||
// DateTimeOffset is represented by DateTime.Ticks followed by TimeSpan.Ticks
|
||||
// both as Int64 values
|
||||
return ReadCellHelper(offset, rowId, length =>
|
||||
{
|
||||
long dtTicks = BitConverter.ToInt64(buffer, 0);
|
||||
long dtOffset = BitConverter.ToInt64(buffer, 8);
|
||||
return new DateTimeOffset(new DateTime(dtTicks), new TimeSpan(dtOffset));
|
||||
}, null, dt =>
|
||||
{
|
||||
string formatString = $"{DateFormatString} {TimeFormatString}.fffffff zzz";
|
||||
|
||||
return dt.ToString(formatString);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a TimeSpan from the file at the offset provided
|
||||
/// </summary>
|
||||
/// <param name="offset">Offset into the file to read the TimeSpan from</param>
|
||||
/// <param name="rowId">Internal ID of the row that will be stored in the cell</param>
|
||||
/// <returns>A TimeSpan</returns>
|
||||
internal FileStreamReadResult ReadTimeSpan(long offset, long rowId)
|
||||
{
|
||||
return ReadCellHelper(offset, rowId, length =>
|
||||
{
|
||||
long ticks = BitConverter.ToInt64(buffer, 0);
|
||||
return new TimeSpan(ticks);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a string from the file at the offset provided
|
||||
/// </summary>
|
||||
/// <param name="offset">Offset into the file to read the string from</param>
|
||||
/// <param name="rowId">Internal ID of the row that will be stored in the cell</param>
|
||||
/// <returns>A string</returns>
|
||||
internal FileStreamReadResult ReadString(long offset, long rowId)
|
||||
{
|
||||
return ReadCellHelper(offset, rowId, length =>
|
||||
length > 0
|
||||
? Encoding.Unicode.GetString(buffer, 0, length)
|
||||
: string.Empty, totalLength => totalLength == 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads bytes from the file at the offset provided
|
||||
/// </summary>
|
||||
/// <param name="offset">Offset into the file to read the bytes from</param>
|
||||
/// <param name="rowId">Internal ID of the row that will be stored in the cell</param>
|
||||
/// <returns>A byte array</returns>
|
||||
internal FileStreamReadResult ReadBytes(long offset, long rowId)
|
||||
{
|
||||
return ReadCellHelper(offset, rowId, length =>
|
||||
{
|
||||
byte[] output = new byte[length];
|
||||
Buffer.BlockCopy(buffer, 0, output, 0, length);
|
||||
return output;
|
||||
}, totalLength => totalLength == 1,
|
||||
bytes =>
|
||||
{
|
||||
StringBuilder sb = new StringBuilder("0x");
|
||||
foreach (byte b in bytes)
|
||||
{
|
||||
sb.AppendFormat("{0:X2}", b);
|
||||
}
|
||||
return sb.ToString();
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the bytes that make up a GUID at the offset provided
|
||||
/// </summary>
|
||||
/// <param name="offset">Offset into the file to read the bytes from</param>
|
||||
/// <param name="rowId">Internal ID of the row that will be stored in the cell</param>
|
||||
/// <returns>A system guid type object</returns>
|
||||
internal FileStreamReadResult ReadGuid(long offset, long rowId)
|
||||
{
|
||||
return ReadCellHelper(offset, rowId, length =>
|
||||
{
|
||||
byte[] output = new byte[length];
|
||||
Buffer.BlockCopy(buffer, 0, output, 0, length);
|
||||
return new Guid(output);
|
||||
}, totalLength => totalLength == 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a SqlMoney type from the offset provided
|
||||
/// into a
|
||||
/// </summary>
|
||||
/// <param name="offset">Offset into the file to read the value</param>
|
||||
/// <param name="rowId">Internal ID of the row that will be stored in the cell</param>
|
||||
/// <returns>A sql money type object</returns>
|
||||
internal FileStreamReadResult ReadMoney(long offset, long rowId)
|
||||
{
|
||||
return ReadCellHelper(offset, rowId, length =>
|
||||
{
|
||||
int[] arrInt32 = new int[length / 4];
|
||||
Buffer.BlockCopy(buffer, 0, arrInt32, 0, length);
|
||||
return new SqlMoney(new decimal(arrInt32));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the length of a field at the specified offset in the file
|
||||
/// </summary>
|
||||
/// <param name="offset">Offset into the file to read the field length from</param>
|
||||
/// <returns>A LengthResult</returns>
|
||||
private LengthResult ReadLength(long offset)
|
||||
{
|
||||
// read in length information
|
||||
int lengthValue;
|
||||
fileStream.Seek(offset, SeekOrigin.Begin);
|
||||
int lengthLength = fileStream.Read(buffer, 0, 1);
|
||||
if (buffer[0] != 0xFF)
|
||||
{
|
||||
// one byte is enough
|
||||
lengthValue = Convert.ToInt32(buffer[0]);
|
||||
}
|
||||
else
|
||||
{
|
||||
// read in next 4 bytes
|
||||
lengthLength += fileStream.Read(buffer, 0, 4);
|
||||
|
||||
// reconstruct the length
|
||||
lengthValue = BitConverter.ToInt32(buffer, 0);
|
||||
}
|
||||
|
||||
return new LengthResult { LengthLength = lengthLength, ValueLength = lengthValue };
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Internal struct used for representing the length of a field from the file
|
||||
/// </summary>
|
||||
internal struct LengthResult
|
||||
{
|
||||
/// <summary>
|
||||
/// How many bytes the length takes up
|
||||
/// </summary>
|
||||
public int LengthLength { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// How many bytes the value takes up
|
||||
/// </summary>
|
||||
public int ValueLength { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="LengthLength"/> + <see cref="ValueLength"/>
|
||||
/// </summary>
|
||||
public int TotalLength => LengthLength + ValueLength;
|
||||
}
|
||||
|
||||
#region IDisposable Implementation
|
||||
|
||||
private bool disposed;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
fileStream.Dispose();
|
||||
}
|
||||
|
||||
disposed = true;
|
||||
}
|
||||
|
||||
~ServiceBufferFileStreamReader()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,587 @@
|
||||
//
|
||||
// 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.SqlTypes;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using Microsoft.Kusto.ServiceLayer.QueryExecution.Contracts;
|
||||
using Microsoft.Kusto.ServiceLayer.SqlContext;
|
||||
using Microsoft.Kusto.ServiceLayer.Utility;
|
||||
using Microsoft.SqlTools.Utility;
|
||||
|
||||
namespace Microsoft.Kusto.ServiceLayer.QueryExecution.DataStorage
|
||||
{
|
||||
/// <summary>
|
||||
/// Writer for service buffer formatted file streams
|
||||
/// </summary>
|
||||
public class ServiceBufferFileStreamWriter : IFileStreamWriter
|
||||
{
|
||||
private const int DefaultBufferLength = 8192;
|
||||
|
||||
#region Member Variables
|
||||
|
||||
private readonly Stream fileStream;
|
||||
private readonly QueryExecutionSettings executionSettings;
|
||||
|
||||
private byte[] byteBuffer;
|
||||
private readonly short[] shortBuffer;
|
||||
private readonly int[] intBuffer;
|
||||
private readonly long[] longBuffer;
|
||||
private readonly char[] charBuffer;
|
||||
private readonly double[] doubleBuffer;
|
||||
private readonly float[] floatBuffer;
|
||||
|
||||
/// <summary>
|
||||
/// Functions to use for writing various types to a file
|
||||
/// </summary>
|
||||
private readonly Dictionary<Type, Func<object, int>> writeMethods;
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new writer
|
||||
/// </summary>
|
||||
/// <param name="stream">The file wrapper to use as the underlying file stream</param>
|
||||
/// <param name="settings">The query execution settings</param>
|
||||
public ServiceBufferFileStreamWriter(Stream stream, QueryExecutionSettings settings)
|
||||
{
|
||||
Validate.IsNotNull(nameof(stream), stream);
|
||||
Validate.IsNotNull(nameof(settings), settings);
|
||||
|
||||
// open file for reading/writing
|
||||
if (!stream.CanWrite || !stream.CanSeek)
|
||||
{
|
||||
throw new InvalidOperationException("Stream must be writable and seekable.");
|
||||
}
|
||||
fileStream = stream;
|
||||
executionSettings = settings;
|
||||
|
||||
// create internal buffer
|
||||
byteBuffer = new byte[DefaultBufferLength];
|
||||
|
||||
// Create internal buffers for blockcopy of contents to byte array
|
||||
// Note: We create them now to avoid the overhead of creating a new array for every write call
|
||||
shortBuffer = new short[1];
|
||||
intBuffer = new int[1];
|
||||
longBuffer = new long[1];
|
||||
charBuffer = new char[1];
|
||||
doubleBuffer = new double[1];
|
||||
floatBuffer = new float[1];
|
||||
|
||||
// Define what methods to use to write a type to the file
|
||||
writeMethods = new Dictionary<Type, Func<object, int>>
|
||||
{
|
||||
{typeof(string), val => WriteString((string) val)},
|
||||
{typeof(short), val => WriteInt16((short) val)},
|
||||
{typeof(int), val => WriteInt32((int) val)},
|
||||
{typeof(long), val => WriteInt64((long) val)},
|
||||
{typeof(byte), val => WriteByte((byte) val)},
|
||||
{typeof(char), val => WriteChar((char) val)},
|
||||
{typeof(bool), val => WriteBoolean((bool) val)},
|
||||
{typeof(double), val => WriteDouble((double) val) },
|
||||
{typeof(float), val => WriteSingle((float) val) },
|
||||
{typeof(decimal), val => WriteDecimal((decimal) val) },
|
||||
{typeof(DateTime), val => WriteDateTime((DateTime) val) },
|
||||
{typeof(DateTimeOffset), val => WriteDateTimeOffset((DateTimeOffset) val) },
|
||||
{typeof(TimeSpan), val => WriteTimeSpan((TimeSpan) val) },
|
||||
{typeof(byte[]), val => WriteBytes((byte[]) val)},
|
||||
{typeof(Guid), val => WriteGuid((Guid) val)},
|
||||
|
||||
{typeof(SqlString), val => WriteNullable((SqlString) val, obj => WriteString((string) obj))},
|
||||
{typeof(SqlInt16), val => WriteNullable((SqlInt16) val, obj => WriteInt16((short) obj))},
|
||||
{typeof(SqlInt32), val => WriteNullable((SqlInt32) val, obj => WriteInt32((int) obj))},
|
||||
{typeof(SqlInt64), val => WriteNullable((SqlInt64) val, obj => WriteInt64((long) obj)) },
|
||||
{typeof(SqlByte), val => WriteNullable((SqlByte) val, obj => WriteByte((byte) obj)) },
|
||||
{typeof(SqlBoolean), val => WriteNullable((SqlBoolean) val, obj => WriteBoolean((bool) obj)) },
|
||||
{typeof(SqlDouble), val => WriteNullable((SqlDouble) val, obj => WriteDouble((double) obj)) },
|
||||
{typeof(SqlSingle), val => WriteNullable((SqlSingle) val, obj => WriteSingle((float) obj)) },
|
||||
{typeof(SqlDecimal), val => WriteNullable((SqlDecimal) val, obj => WriteSqlDecimal((SqlDecimal) obj)) },
|
||||
{typeof(SqlDateTime), val => WriteNullable((SqlDateTime) val, obj => WriteDateTime((DateTime) obj)) },
|
||||
{typeof(SqlBytes), val => WriteNullable((SqlBytes) val, obj => WriteBytes((byte[]) obj)) },
|
||||
{typeof(SqlBinary), val => WriteNullable((SqlBinary) val, obj => WriteBytes((byte[]) obj)) },
|
||||
{typeof(SqlGuid), val => WriteNullable((SqlGuid) val, obj => WriteGuid((Guid) obj)) },
|
||||
{typeof(SqlMoney), val => WriteNullable((SqlMoney) val, obj => WriteMoney((SqlMoney) obj)) }
|
||||
};
|
||||
}
|
||||
|
||||
#region IFileStreamWriter Implementation
|
||||
|
||||
/// <summary>
|
||||
/// Writes an entire row to the file stream
|
||||
/// </summary>
|
||||
/// <param name="reader">A primed reader</param>
|
||||
/// <returns>Number of bytes used to write the row</returns>
|
||||
public int WriteRow(StorageDataReader reader)
|
||||
{
|
||||
// Read the values in from the db
|
||||
object[] values = new object[reader.Columns.Length];
|
||||
if (!reader.HasLongColumns)
|
||||
{
|
||||
// get all record values in one shot if there are no extra long fields
|
||||
reader.GetValues(values);
|
||||
}
|
||||
|
||||
// Loop over all the columns and write the values to the temp file
|
||||
int rowBytes = 0;
|
||||
for (int i = 0; i < reader.Columns.Length; i++)
|
||||
{
|
||||
DbColumnWrapper ci = reader.Columns[i];
|
||||
if (reader.HasLongColumns)
|
||||
{
|
||||
if (reader.IsDBNull(i))
|
||||
{
|
||||
// Need special case for DBNull because
|
||||
// reader.GetValue doesn't return DBNull in case of SqlXml and CLR type
|
||||
values[i] = DBNull.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ci.IsLong.HasValue && ci.IsLong.Value)
|
||||
{
|
||||
// this is a long field
|
||||
if (ci.IsBytes)
|
||||
{
|
||||
values[i] = reader.GetBytesWithMaxCapacity(i, executionSettings.MaxCharsToStore);
|
||||
}
|
||||
else if (ci.IsChars)
|
||||
{
|
||||
int maxChars = ci.IsXml
|
||||
? executionSettings.MaxXmlCharsToStore
|
||||
: executionSettings.MaxCharsToStore;
|
||||
values[i] = reader.GetCharsWithMaxCapacity(i, maxChars);
|
||||
}
|
||||
else if (ci.IsXml)
|
||||
{
|
||||
values[i] = reader.GetXmlWithMaxCapacity(i, executionSettings.MaxXmlCharsToStore);
|
||||
}
|
||||
else
|
||||
{
|
||||
// we should never get here
|
||||
Debug.Assert(false);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// not a long field
|
||||
values[i] = reader.GetValue(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get true type of the object
|
||||
Type tVal = values[i].GetType();
|
||||
|
||||
// Write the object to a file
|
||||
if (tVal == typeof(DBNull))
|
||||
{
|
||||
rowBytes += WriteNull();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ci.IsSqlVariant)
|
||||
{
|
||||
// serialize type information as a string before the value
|
||||
string val = tVal.ToString();
|
||||
rowBytes += WriteString(val);
|
||||
}
|
||||
|
||||
// Use the appropriate writing method for the type
|
||||
Func<object, int> writeMethod;
|
||||
if (writeMethods.TryGetValue(tVal, out writeMethod))
|
||||
{
|
||||
rowBytes += writeMethod(values[i]);
|
||||
}
|
||||
else
|
||||
{
|
||||
rowBytes += WriteString(values[i].ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Flush the buffer after every row
|
||||
FlushBuffer();
|
||||
return rowBytes;
|
||||
}
|
||||
|
||||
[Obsolete]
|
||||
public void WriteRow(IList<DbCellValue> row, IList<DbColumnWrapper> columns)
|
||||
{
|
||||
throw new InvalidOperationException("This type of writer is meant to write values from a DbDataReader only.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Seeks to a given offset in the file, relative to the beginning of the file
|
||||
/// </summary>
|
||||
public void Seek(long offset)
|
||||
{
|
||||
fileStream.Seek(offset, SeekOrigin.Begin);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flushes the internal buffer to the file stream
|
||||
/// </summary>
|
||||
public void FlushBuffer()
|
||||
{
|
||||
fileStream.Flush();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Helpers
|
||||
|
||||
/// <summary>
|
||||
/// Writes null to the file as one 0x00 byte
|
||||
/// </summary>
|
||||
/// <returns>Number of bytes used to store the null</returns>
|
||||
internal int WriteNull()
|
||||
{
|
||||
byteBuffer[0] = 0x00;
|
||||
return FileUtilities.WriteWithLength(fileStream, byteBuffer, 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a short to the file
|
||||
/// </summary>
|
||||
/// <returns>Number of bytes used to store the short</returns>
|
||||
internal int WriteInt16(short val)
|
||||
{
|
||||
byteBuffer[0] = 0x02; // length
|
||||
shortBuffer[0] = val;
|
||||
Buffer.BlockCopy(shortBuffer, 0, byteBuffer, 1, 2);
|
||||
return FileUtilities.WriteWithLength(fileStream, byteBuffer, 3);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a int to the file
|
||||
/// </summary>
|
||||
/// <returns>Number of bytes used to store the int</returns>
|
||||
internal int WriteInt32(int val)
|
||||
{
|
||||
byteBuffer[0] = 0x04; // length
|
||||
intBuffer[0] = val;
|
||||
Buffer.BlockCopy(intBuffer, 0, byteBuffer, 1, 4);
|
||||
return FileUtilities.WriteWithLength(fileStream, byteBuffer, 5);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a long to the file
|
||||
/// </summary>
|
||||
/// <returns>Number of bytes used to store the long</returns>
|
||||
internal int WriteInt64(long val)
|
||||
{
|
||||
byteBuffer[0] = 0x08; // length
|
||||
longBuffer[0] = val;
|
||||
Buffer.BlockCopy(longBuffer, 0, byteBuffer, 1, 8);
|
||||
return FileUtilities.WriteWithLength(fileStream, byteBuffer, 9);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a char to the file
|
||||
/// </summary>
|
||||
/// <returns>Number of bytes used to store the char</returns>
|
||||
internal int WriteChar(char val)
|
||||
{
|
||||
byteBuffer[0] = 0x02; // length
|
||||
charBuffer[0] = val;
|
||||
Buffer.BlockCopy(charBuffer, 0, byteBuffer, 1, 2);
|
||||
return FileUtilities.WriteWithLength(fileStream, byteBuffer, 3);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a bool to the file
|
||||
/// </summary>
|
||||
/// <returns>Number of bytes used to store the bool</returns>
|
||||
internal int WriteBoolean(bool val)
|
||||
{
|
||||
byteBuffer[0] = 0x01; // length
|
||||
byteBuffer[1] = (byte) (val ? 0x01 : 0x00);
|
||||
return FileUtilities.WriteWithLength(fileStream, byteBuffer, 2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a byte to the file
|
||||
/// </summary>
|
||||
/// <returns>Number of bytes used to store the byte</returns>
|
||||
internal int WriteByte(byte val)
|
||||
{
|
||||
byteBuffer[0] = 0x01; // length
|
||||
byteBuffer[1] = val;
|
||||
return FileUtilities.WriteWithLength(fileStream, byteBuffer, 2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a float to the file
|
||||
/// </summary>
|
||||
/// <returns>Number of bytes used to store the float</returns>
|
||||
internal int WriteSingle(float val)
|
||||
{
|
||||
byteBuffer[0] = 0x04; // length
|
||||
floatBuffer[0] = val;
|
||||
Buffer.BlockCopy(floatBuffer, 0, byteBuffer, 1, 4);
|
||||
return FileUtilities.WriteWithLength(fileStream, byteBuffer, 5);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a double to the file
|
||||
/// </summary>
|
||||
/// <returns>Number of bytes used to store the double</returns>
|
||||
internal int WriteDouble(double val)
|
||||
{
|
||||
byteBuffer[0] = 0x08; // length
|
||||
doubleBuffer[0] = val;
|
||||
Buffer.BlockCopy(doubleBuffer, 0, byteBuffer, 1, 8);
|
||||
return FileUtilities.WriteWithLength(fileStream, byteBuffer, 9);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a SqlDecimal to the file
|
||||
/// </summary>
|
||||
/// <returns>Number of bytes used to store the SqlDecimal</returns>
|
||||
internal int WriteSqlDecimal(SqlDecimal val)
|
||||
{
|
||||
int[] arrInt32 = val.Data;
|
||||
int iLen = 3 + (arrInt32.Length * 4);
|
||||
int iTotalLen = WriteLength(iLen); // length
|
||||
|
||||
// precision
|
||||
byteBuffer[0] = val.Precision;
|
||||
|
||||
// scale
|
||||
byteBuffer[1] = val.Scale;
|
||||
|
||||
// positive
|
||||
byteBuffer[2] = (byte)(val.IsPositive ? 0x01 : 0x00);
|
||||
|
||||
// data value
|
||||
Buffer.BlockCopy(arrInt32, 0, byteBuffer, 3, iLen - 3);
|
||||
iTotalLen += FileUtilities.WriteWithLength(fileStream, byteBuffer, iLen);
|
||||
return iTotalLen; // len+data
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a decimal to the file
|
||||
/// </summary>
|
||||
/// <returns>Number of bytes used to store the decimal</returns>
|
||||
internal int WriteDecimal(decimal val)
|
||||
{
|
||||
int[] arrInt32 = decimal.GetBits(val);
|
||||
|
||||
int iLen = arrInt32.Length * 4;
|
||||
int iTotalLen = WriteLength(iLen); // length
|
||||
|
||||
Buffer.BlockCopy(arrInt32, 0, byteBuffer, 0, iLen);
|
||||
iTotalLen += FileUtilities.WriteWithLength(fileStream, byteBuffer, iLen);
|
||||
|
||||
return iTotalLen; // len+data
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a DateTime to the file
|
||||
/// </summary>
|
||||
/// <returns>Number of bytes used to store the DateTime</returns>
|
||||
public int WriteDateTime(DateTime dtVal)
|
||||
{
|
||||
return WriteInt64(dtVal.Ticks);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a DateTimeOffset to the file
|
||||
/// </summary>
|
||||
/// <returns>Number of bytes used to store the DateTimeOffset</returns>
|
||||
internal int WriteDateTimeOffset(DateTimeOffset dtoVal)
|
||||
{
|
||||
// Write the length, which is the 2*sizeof(long)
|
||||
byteBuffer[0] = 0x10; // length (16)
|
||||
|
||||
// Write the two longs, the datetime and the offset
|
||||
long[] longBufferOffset = new long[2];
|
||||
longBufferOffset[0] = dtoVal.Ticks;
|
||||
longBufferOffset[1] = dtoVal.Offset.Ticks;
|
||||
Buffer.BlockCopy(longBufferOffset, 0, byteBuffer, 1, 16);
|
||||
return FileUtilities.WriteWithLength(fileStream, byteBuffer, 17);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a TimeSpan to the file
|
||||
/// </summary>
|
||||
/// <returns>Number of bytes used to store the TimeSpan</returns>
|
||||
internal int WriteTimeSpan(TimeSpan timeSpan)
|
||||
{
|
||||
return WriteInt64(timeSpan.Ticks);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a string to the file
|
||||
/// </summary>
|
||||
/// <returns>Number of bytes used to store the string</returns>
|
||||
internal int WriteString(string sVal)
|
||||
{
|
||||
Validate.IsNotNull(nameof(sVal), sVal);
|
||||
|
||||
int iTotalLen;
|
||||
if (0 == sVal.Length) // special case of 0 length string
|
||||
{
|
||||
const int iLen = 5;
|
||||
|
||||
AssureBufferLength(iLen);
|
||||
byteBuffer[0] = 0xFF;
|
||||
byteBuffer[1] = 0x00;
|
||||
byteBuffer[2] = 0x00;
|
||||
byteBuffer[3] = 0x00;
|
||||
byteBuffer[4] = 0x00;
|
||||
|
||||
iTotalLen = FileUtilities.WriteWithLength(fileStream, byteBuffer, 5);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Convert to a unicode byte array
|
||||
byte[] bytes = Encoding.Unicode.GetBytes(sVal);
|
||||
|
||||
// convert char array into byte array and write it out
|
||||
iTotalLen = WriteLength(bytes.Length);
|
||||
iTotalLen += FileUtilities.WriteWithLength(fileStream, bytes, bytes.Length);
|
||||
}
|
||||
return iTotalLen; // len+data
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a byte[] to the file
|
||||
/// </summary>
|
||||
/// <returns>Number of bytes used to store the byte[]</returns>
|
||||
internal int WriteBytes(byte[] bytesVal)
|
||||
{
|
||||
Validate.IsNotNull(nameof(bytesVal), bytesVal);
|
||||
|
||||
int iTotalLen;
|
||||
if (bytesVal.Length == 0) // special case of 0 length byte array "0x"
|
||||
{
|
||||
AssureBufferLength(5);
|
||||
byteBuffer[0] = 0xFF;
|
||||
byteBuffer[1] = 0x00;
|
||||
byteBuffer[2] = 0x00;
|
||||
byteBuffer[3] = 0x00;
|
||||
byteBuffer[4] = 0x00;
|
||||
|
||||
iTotalLen = FileUtilities.WriteWithLength(fileStream, byteBuffer, 5);
|
||||
}
|
||||
else
|
||||
{
|
||||
iTotalLen = WriteLength(bytesVal.Length);
|
||||
iTotalLen += FileUtilities.WriteWithLength(fileStream, bytesVal, bytesVal.Length);
|
||||
}
|
||||
return iTotalLen; // len+data
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stores a GUID value to the file by treating it as a byte array
|
||||
/// </summary>
|
||||
/// <param name="val">The GUID to write to the file</param>
|
||||
/// <returns>Number of bytes written to the file</returns>
|
||||
internal int WriteGuid(Guid val)
|
||||
{
|
||||
byte[] guidBytes = val.ToByteArray();
|
||||
return WriteBytes(guidBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stores a SqlMoney value to the file by treating it as a decimal
|
||||
/// </summary>
|
||||
/// <param name="val">The SqlMoney value to write to the file</param>
|
||||
/// <returns>Number of bytes written to the file</returns>
|
||||
internal int WriteMoney(SqlMoney val)
|
||||
{
|
||||
return WriteDecimal(val.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new buffer that is of the specified length if the buffer is not already
|
||||
/// at least as long as specified.
|
||||
/// </summary>
|
||||
/// <param name="newBufferLength">The minimum buffer size</param>
|
||||
private void AssureBufferLength(int newBufferLength)
|
||||
{
|
||||
if (newBufferLength > byteBuffer.Length)
|
||||
{
|
||||
byteBuffer = new byte[byteBuffer.Length];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes the length of the field using the appropriate number of bytes (ie, 1 if the
|
||||
/// length is <255, 5 if the length is >=255)
|
||||
/// </summary>
|
||||
/// <returns>Number of bytes used to store the length</returns>
|
||||
private int WriteLength(int iLen)
|
||||
{
|
||||
if (iLen < 0xFF)
|
||||
{
|
||||
// fits in one byte of memory only need to write one byte
|
||||
int iTmp = iLen & 0x000000FF;
|
||||
|
||||
byteBuffer[0] = Convert.ToByte(iTmp);
|
||||
return FileUtilities.WriteWithLength(fileStream, byteBuffer, 1);
|
||||
}
|
||||
// The length won't fit in 1 byte, so we need to use 1 byte to signify that the length
|
||||
// is a full 4 bytes.
|
||||
byteBuffer[0] = 0xFF;
|
||||
|
||||
// convert int32 into array of bytes
|
||||
intBuffer[0] = iLen;
|
||||
Buffer.BlockCopy(intBuffer, 0, byteBuffer, 1, 4);
|
||||
return FileUtilities.WriteWithLength(fileStream, byteBuffer, 5);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a Nullable type (generally a Sql* type) to the file. The function provided by
|
||||
/// <paramref name="valueWriteFunc"/> is used to write to the file if <paramref name="val"/>
|
||||
/// is not null. <see cref="WriteNull"/> is used if <paramref name="val"/> is null.
|
||||
/// </summary>
|
||||
/// <param name="val">The value to write to the file</param>
|
||||
/// <param name="valueWriteFunc">The function to use if val is not null</param>
|
||||
/// <returns>Number of bytes used to write value to the file</returns>
|
||||
private int WriteNullable(INullable val, Func<object, int> valueWriteFunc)
|
||||
{
|
||||
return val.IsNull ? WriteNull() : valueWriteFunc(val);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IDisposable Implementation
|
||||
|
||||
private bool disposed;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
fileStream.Flush();
|
||||
fileStream.Dispose();
|
||||
}
|
||||
|
||||
disposed = true;
|
||||
}
|
||||
|
||||
~ServiceBufferFileStreamWriter()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
//
|
||||
// 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.Data.SqlTypes;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml;
|
||||
using Microsoft.Kusto.ServiceLayer.QueryExecution.Contracts;
|
||||
using Microsoft.SqlTools.Utility;
|
||||
|
||||
namespace Microsoft.Kusto.ServiceLayer.QueryExecution.DataStorage
|
||||
{
|
||||
/// <summary>
|
||||
/// Wrapper around a DbData reader to perform some special operations more simply
|
||||
/// </summary>
|
||||
public class StorageDataReader
|
||||
{
|
||||
/// <summary>
|
||||
/// Constructs a new wrapper around the provided reader
|
||||
/// </summary>
|
||||
/// <param name="reader">The reader to wrap around</param>
|
||||
public StorageDataReader(IDataReader reader)
|
||||
{
|
||||
// Sanity check to make sure there is a data reader
|
||||
Validate.IsNotNull(nameof(reader), reader);
|
||||
|
||||
DataReader = reader;
|
||||
|
||||
// Read the columns into a set of wrappers
|
||||
List<DbColumnWrapper> columnList = new List<DbColumnWrapper>();
|
||||
var rows = DataReader.GetSchemaTable().Rows;
|
||||
|
||||
foreach (DataRow row in rows)
|
||||
{
|
||||
columnList.Add(new DbColumnWrapper(row));
|
||||
}
|
||||
|
||||
Columns = columnList.ToArray();
|
||||
HasLongColumns = Columns.Any(column => column.IsLong.HasValue && column.IsLong.Value);
|
||||
}
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// All the columns that this reader currently contains
|
||||
/// </summary>
|
||||
public DbColumnWrapper[] Columns { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="DataReader"/> that will be read from
|
||||
/// </summary>
|
||||
public IDataReader DataReader { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not any of the columns of this reader are 'long', such as nvarchar(max)
|
||||
/// </summary>
|
||||
public bool HasLongColumns { get; private set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region DbDataReader Methods
|
||||
|
||||
/// <summary>
|
||||
/// Pass-through to DbDataReader.ReadAsync()
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token to use for cancelling a query</param>
|
||||
/// <returns></returns>
|
||||
public Task<bool> ReadAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.Run(() => DataReader.Read());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a value
|
||||
/// </summary>
|
||||
/// <param name="i">Column ordinal</param>
|
||||
/// <returns>The value of the given column</returns>
|
||||
public object GetValue(int i)
|
||||
{
|
||||
return DataReader.GetValue(i);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stores all values of the current row into the provided object array
|
||||
/// </summary>
|
||||
/// <param name="values">Where to store the values from this row</param>
|
||||
public void GetValues(object[] values)
|
||||
{
|
||||
DataReader.GetValues(values);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not the cell of the given column at the current row is a DBNull
|
||||
/// </summary>
|
||||
/// <param name="i">Column ordinal</param>
|
||||
/// <returns>True if the cell is DBNull, false otherwise</returns>
|
||||
public bool IsDBNull(int i)
|
||||
{
|
||||
return DataReader.IsDBNull(i);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves bytes with a maximum number of bytes to return
|
||||
/// </summary>
|
||||
/// <param name="iCol">Column ordinal</param>
|
||||
/// <param name="maxNumBytesToReturn">Number of bytes to return at maximum</param>
|
||||
/// <returns>Byte array</returns>
|
||||
public byte[] GetBytesWithMaxCapacity(int iCol, int maxNumBytesToReturn)
|
||||
{
|
||||
if (maxNumBytesToReturn <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(maxNumBytesToReturn), SR.QueryServiceDataReaderByteCountInvalid);
|
||||
}
|
||||
|
||||
//first, ask provider how much data it has and calculate the final # of bytes
|
||||
//NOTE: -1 means that it doesn't know how much data it has
|
||||
long neededLength;
|
||||
long origLength = neededLength = GetBytes(iCol, 0, null, 0, 0);
|
||||
if (neededLength == -1 || neededLength > maxNumBytesToReturn)
|
||||
{
|
||||
neededLength = maxNumBytesToReturn;
|
||||
}
|
||||
|
||||
//get the data up to the maxNumBytesToReturn
|
||||
byte[] bytesBuffer = new byte[neededLength];
|
||||
GetBytes(iCol, 0, bytesBuffer, 0, (int)neededLength);
|
||||
|
||||
//see if server sent back more data than we should return
|
||||
if (origLength == -1 || origLength > neededLength)
|
||||
{
|
||||
//pump the rest of data from the reader and discard it right away
|
||||
long dataIndex = neededLength;
|
||||
const int tmpBufSize = 100000;
|
||||
byte[] tmpBuf = new byte[tmpBufSize];
|
||||
while (GetBytes(iCol, dataIndex, tmpBuf, 0, tmpBufSize) == tmpBufSize)
|
||||
{
|
||||
dataIndex += tmpBufSize;
|
||||
}
|
||||
}
|
||||
|
||||
return bytesBuffer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves characters with a maximum number of charss to return
|
||||
/// </summary>
|
||||
/// <param name="iCol">Column ordinal</param>
|
||||
/// <param name="maxCharsToReturn">Number of chars to return at maximum</param>
|
||||
/// <returns>String</returns>
|
||||
public string GetCharsWithMaxCapacity(int iCol, int maxCharsToReturn)
|
||||
{
|
||||
if (maxCharsToReturn <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(maxCharsToReturn), SR.QueryServiceDataReaderCharCountInvalid);
|
||||
}
|
||||
|
||||
//first, ask provider how much data it has and calculate the final # of chars
|
||||
//NOTE: -1 means that it doesn't know how much data it has
|
||||
long neededLength;
|
||||
long origLength = neededLength = GetChars(iCol, 0, null, 0, 0);
|
||||
if (neededLength == -1 || neededLength > maxCharsToReturn)
|
||||
{
|
||||
neededLength = maxCharsToReturn;
|
||||
}
|
||||
Debug.Assert(neededLength < int.MaxValue);
|
||||
|
||||
//get the data up to maxCharsToReturn
|
||||
char[] buffer = new char[neededLength];
|
||||
if (neededLength > 0)
|
||||
{
|
||||
GetChars(iCol, 0, buffer, 0, (int)neededLength);
|
||||
}
|
||||
|
||||
//see if server sent back more data than we should return
|
||||
if (origLength == -1 || origLength > neededLength)
|
||||
{
|
||||
//pump the rest of data from the reader and discard it right away
|
||||
long dataIndex = neededLength;
|
||||
const int tmpBufSize = 100000;
|
||||
char[] tmpBuf = new char[tmpBufSize];
|
||||
while (GetChars(iCol, dataIndex, tmpBuf, 0, tmpBufSize) == tmpBufSize)
|
||||
{
|
||||
dataIndex += tmpBufSize;
|
||||
}
|
||||
}
|
||||
string res = new string(buffer);
|
||||
return res;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves xml with a maximum number of bytes to return
|
||||
/// </summary>
|
||||
/// <param name="iCol">Column ordinal</param>
|
||||
/// <param name="maxCharsToReturn">Number of chars to return at maximum</param>
|
||||
/// <returns>String</returns>
|
||||
public string GetXmlWithMaxCapacity(int iCol, int maxCharsToReturn)
|
||||
{
|
||||
if (maxCharsToReturn <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(maxCharsToReturn), SR.QueryServiceDataReaderXmlCountInvalid);
|
||||
}
|
||||
|
||||
object o = GetValue(iCol);
|
||||
return o?.ToString();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Helpers
|
||||
|
||||
private long GetBytes(int i, long dataIndex, byte[] buffer, int bufferIndex, int length)
|
||||
{
|
||||
return DataReader.GetBytes(i, dataIndex, buffer, bufferIndex, length);
|
||||
}
|
||||
|
||||
private long GetChars(int i, long dataIndex, char[] buffer, int bufferIndex, int length)
|
||||
{
|
||||
return DataReader.GetChars(i, dataIndex, buffer, bufferIndex, length);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Internal class for writing strings with a maximum capacity
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This code is take almost verbatim from Microsoft.SqlServer.Management.UI.Grid, SSMS
|
||||
/// DataStorage, StorageDataReader class.
|
||||
/// </remarks>
|
||||
internal class StringWriterWithMaxCapacity : StringWriter
|
||||
{
|
||||
private bool stopWriting;
|
||||
|
||||
private int CurrentLength
|
||||
{
|
||||
get { return GetStringBuilder().Length; }
|
||||
}
|
||||
|
||||
public StringWriterWithMaxCapacity(IFormatProvider formatProvider, int capacity) : base(formatProvider)
|
||||
{
|
||||
MaximumCapacity = capacity;
|
||||
}
|
||||
|
||||
private int MaximumCapacity { get; set; }
|
||||
|
||||
public override void Write(char value)
|
||||
{
|
||||
if (stopWriting) { return; }
|
||||
|
||||
if (CurrentLength < MaximumCapacity)
|
||||
{
|
||||
base.Write(value);
|
||||
}
|
||||
else
|
||||
{
|
||||
stopWriting = true;
|
||||
}
|
||||
}
|
||||
|
||||
public override void Write(char[] buffer, int index, int count)
|
||||
{
|
||||
if (stopWriting) { return; }
|
||||
|
||||
int curLen = CurrentLength;
|
||||
if (curLen + (count - index) > MaximumCapacity)
|
||||
{
|
||||
stopWriting = true;
|
||||
|
||||
count = MaximumCapacity - curLen + index;
|
||||
if (count < 0)
|
||||
{
|
||||
count = 0;
|
||||
}
|
||||
}
|
||||
base.Write(buffer, index, count);
|
||||
}
|
||||
|
||||
public override void Write(string value)
|
||||
{
|
||||
if (stopWriting) { return; }
|
||||
|
||||
int curLen = CurrentLength;
|
||||
if (value.Length + curLen > MaximumCapacity)
|
||||
{
|
||||
stopWriting = true;
|
||||
base.Write(value.Substring(0, MaximumCapacity - curLen));
|
||||
}
|
||||
else
|
||||
{
|
||||
base.Write(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
456
src/Microsoft.Kusto.ServiceLayer/QueryExecution/Query.cs
Normal file
456
src/Microsoft.Kusto.ServiceLayer/QueryExecution/Query.cs
Normal file
@@ -0,0 +1,456 @@
|
||||
//
|
||||
// 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.Common;
|
||||
using System.Data.SqlClient;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.SqlTools.ServiceLayer.BatchParser;
|
||||
using Microsoft.Kusto.ServiceLayer.Connection;
|
||||
using Microsoft.Kusto.ServiceLayer.QueryExecution.Contracts;
|
||||
using Microsoft.Kusto.ServiceLayer.QueryExecution.DataStorage;
|
||||
using Microsoft.Kusto.ServiceLayer.SqlContext;
|
||||
using Microsoft.SqlTools.Utility;
|
||||
using Microsoft.SqlTools.ServiceLayer.BatchParser.ExecutionEngineCode;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using Microsoft.Kusto.ServiceLayer.Utility;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.Kusto.ServiceLayer.QueryExecution
|
||||
{
|
||||
/// <summary>
|
||||
/// Internal representation of an active query
|
||||
/// </summary>
|
||||
public class Query : IDisposable
|
||||
{
|
||||
#region Member Variables
|
||||
|
||||
/// <summary>
|
||||
/// Cancellation token source, used for cancelling async db actions
|
||||
/// </summary>
|
||||
private readonly CancellationTokenSource cancellationSource;
|
||||
|
||||
/// <summary>
|
||||
/// For IDisposable implementation, whether or not this object has been disposed
|
||||
/// </summary>
|
||||
private bool disposed;
|
||||
|
||||
/// <summary>
|
||||
/// The connection info associated with the file editor owner URI, used to create a new
|
||||
/// connection upon execution of the query
|
||||
/// </summary>
|
||||
private readonly ConnectionInfo editorConnection;
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not the execute method has been called for this query
|
||||
/// </summary>
|
||||
private bool hasExecuteBeenCalled;
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for a query
|
||||
/// </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>
|
||||
/// <param name="settings">Settings for how to execute the query, from the user</param>
|
||||
/// <param name="outputFactory">Factory for creating output files</param>
|
||||
public Query(
|
||||
string queryText,
|
||||
ConnectionInfo connection,
|
||||
QueryExecutionSettings settings,
|
||||
IFileStreamFactory outputFactory,
|
||||
bool getFullColumnSchema = false,
|
||||
bool applyExecutionSettings = false)
|
||||
{
|
||||
// Sanity check for input
|
||||
Validate.IsNotNull(nameof(queryText), queryText);
|
||||
Validate.IsNotNull(nameof(connection), connection);
|
||||
Validate.IsNotNull(nameof(settings), settings);
|
||||
Validate.IsNotNull(nameof(outputFactory), outputFactory);
|
||||
|
||||
// Initialize the internal state
|
||||
QueryText = queryText;
|
||||
editorConnection = connection;
|
||||
cancellationSource = new CancellationTokenSource();
|
||||
|
||||
// Process the query into batches
|
||||
BatchParserWrapper parser = new BatchParserWrapper();
|
||||
ExecutionEngineConditions conditions = null;
|
||||
List<BatchDefinition> parserResult = parser.GetBatches(queryText, conditions);
|
||||
|
||||
var batchSelection = parserResult
|
||||
.Select((batchDefinition, index) =>
|
||||
new Batch(batchDefinition.BatchText,
|
||||
new SelectionData(
|
||||
batchDefinition.StartLine-1,
|
||||
batchDefinition.StartColumn-1,
|
||||
batchDefinition.EndLine-1,
|
||||
batchDefinition.EndColumn-1),
|
||||
index, outputFactory,
|
||||
batchDefinition.BatchExecutionCount,
|
||||
getFullColumnSchema));
|
||||
|
||||
Batches = batchSelection.ToArray();
|
||||
|
||||
// Create our batch lists
|
||||
BeforeBatches = new List<Batch>();
|
||||
AfterBatches = new List<Batch>();
|
||||
}
|
||||
|
||||
#region Events
|
||||
|
||||
/// <summary>
|
||||
/// Delegate type for callback when a query completes or fails
|
||||
/// </summary>
|
||||
/// <param name="query">The query that completed</param>
|
||||
public delegate Task QueryAsyncEventHandler(Query query);
|
||||
|
||||
/// <summary>
|
||||
/// Delegate type for callback when a query fails
|
||||
/// </summary>
|
||||
/// <param name="query">Query that raised the event</param>
|
||||
/// <param name="exception">Exception that caused the query to fail</param>
|
||||
public delegate Task QueryAsyncErrorEventHandler(Query query, Exception exception);
|
||||
|
||||
/// <summary>
|
||||
/// Event to be called when a batch is completed.
|
||||
/// </summary>
|
||||
public event Batch.BatchAsyncEventHandler BatchCompleted;
|
||||
|
||||
/// <summary>
|
||||
/// Event that will be called when a message has been emitted
|
||||
/// </summary>
|
||||
public event Batch.BatchAsyncMessageHandler BatchMessageSent;
|
||||
|
||||
/// <summary>
|
||||
/// Event to be called when a batch starts execution.
|
||||
/// </summary>
|
||||
public event Batch.BatchAsyncEventHandler BatchStarted;
|
||||
|
||||
/// <summary>
|
||||
/// Callback for when the query has completed successfully
|
||||
/// </summary>
|
||||
public event QueryAsyncEventHandler QueryCompleted;
|
||||
|
||||
/// <summary>
|
||||
/// Callback for when the query has failed
|
||||
/// </summary>
|
||||
public event QueryAsyncErrorEventHandler QueryFailed;
|
||||
|
||||
/// <summary>
|
||||
/// Event to be called when a resultset has completed.
|
||||
/// </summary>
|
||||
public event ResultSet.ResultSetAsyncEventHandler ResultSetCompleted;
|
||||
|
||||
/// <summary>
|
||||
/// Event that will be called when the resultSet first becomes available. This is as soon as we start reading the results.
|
||||
/// </summary>
|
||||
public event ResultSet.ResultSetAsyncEventHandler ResultSetAvailable;
|
||||
|
||||
/// <summary>
|
||||
/// Event that will be called when additional rows in the result set are available (rowCount available has increased)
|
||||
/// </summary>
|
||||
public event ResultSet.ResultSetAsyncEventHandler ResultSetUpdated;
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// The batches which should run before the user batches
|
||||
/// </summary>
|
||||
private List<Batch> BeforeBatches { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The batches underneath this query
|
||||
/// </summary>
|
||||
internal Batch[] Batches { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The batches which should run after the user batches
|
||||
/// </summary>
|
||||
internal List<Batch> AfterBatches { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The summaries of the batches underneath this query
|
||||
/// </summary>
|
||||
public BatchSummary[] BatchSummaries
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!HasExecuted && !HasCancelled && !HasErrored)
|
||||
{
|
||||
throw new InvalidOperationException("Query has not been executed.");
|
||||
}
|
||||
return Batches.Select(b => b.Summary).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Storage for the async task for execution. Set as internal in order to await completion
|
||||
/// in unit tests.
|
||||
/// </summary>
|
||||
internal Task ExecutionTask { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not the query has completed executed, regardless of success or failure
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// if the query has been cancelled (before execution started)
|
||||
/// </summary>
|
||||
public bool HasCancelled { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// if the query has errored out (before batch execution started)
|
||||
/// </summary>
|
||||
public bool HasErrored { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The text of the query to execute
|
||||
/// </summary>
|
||||
public string QueryText { get; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>
|
||||
/// Cancels the query by issuing the cancellation token
|
||||
/// </summary>
|
||||
public void Cancel()
|
||||
{
|
||||
// Make sure that the query hasn't completed execution
|
||||
if (HasExecuted)
|
||||
{
|
||||
throw new InvalidOperationException(SR.QueryServiceCancelAlreadyCompleted);
|
||||
}
|
||||
|
||||
// Issue the cancellation token for the query
|
||||
this.HasCancelled = true;
|
||||
cancellationSource.Cancel();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Launches the asynchronous process for executing the query
|
||||
/// </summary>
|
||||
public void Execute()
|
||||
{
|
||||
ExecutionTask = Task.Run(ExecuteInternal)
|
||||
.ContinueWithOnFaulted(async t =>
|
||||
{
|
||||
if (QueryFailed != null)
|
||||
{
|
||||
await QueryFailed(this, t.Exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <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 Task<ResultSetSubset> GetSubset(int batchIndex, int resultSetIndex, long startRow, int rowCount)
|
||||
{
|
||||
Logger.Write(TraceEventType.Start, $"Starting GetSubset execution for batchIndex:'{batchIndex}', resultSetIndex:'{resultSetIndex}', startRow:'{startRow}', rowCount:'{rowCount}'");
|
||||
// Sanity check to make sure that the batch is within bounds
|
||||
if (batchIndex < 0 || batchIndex >= Batches.Length)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(batchIndex), SR.QueryServiceSubsetBatchOutOfRange);
|
||||
}
|
||||
|
||||
return Batches[batchIndex].GetSubset(resultSetIndex, startRow, rowCount);
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// <returns>The Execution Plan, if the result set has one</returns>
|
||||
public Task<ExecutionPlan> GetExecutionPlan(int batchIndex, int resultSetIndex)
|
||||
{
|
||||
// Sanity check to make sure that the batch is within bounds
|
||||
if (batchIndex < 0 || batchIndex >= Batches.Length)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(batchIndex), SR.QueryServiceSubsetBatchOutOfRange);
|
||||
}
|
||||
|
||||
return Batches[batchIndex].GetExecutionPlan(resultSetIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves the requested results to a file format of the user's choice
|
||||
/// </summary>
|
||||
/// <param name="saveParams">Parameters for the save as request</param>
|
||||
/// <param name="fileFactory">
|
||||
/// Factory for creating the reader/writer pair for the requested output format
|
||||
/// </param>
|
||||
/// <param name="successHandler">Delegate to call when the request completes successfully</param>
|
||||
/// <param name="failureHandler">Delegate to call if the request fails</param>
|
||||
public void SaveAs(SaveResultsRequestParams saveParams, IFileStreamFactory fileFactory,
|
||||
ResultSet.SaveAsAsyncEventHandler successHandler, ResultSet.SaveAsFailureAsyncEventHandler failureHandler)
|
||||
{
|
||||
// Sanity check to make sure that the batch is within bounds
|
||||
if (saveParams.BatchIndex < 0 || saveParams.BatchIndex >= Batches.Length)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(saveParams.BatchIndex), SR.QueryServiceSubsetBatchOutOfRange);
|
||||
}
|
||||
|
||||
Batches[saveParams.BatchIndex].SaveAs(saveParams, fileFactory, successHandler, failureHandler);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Helpers
|
||||
|
||||
/// <summary>
|
||||
/// Executes this query asynchronously and collects all result sets
|
||||
/// </summary>
|
||||
private async Task ExecuteInternal()
|
||||
{
|
||||
ReliableDataSourceConnection sqlConn = null;
|
||||
try
|
||||
{
|
||||
// check for cancellation token before actually making connection
|
||||
cancellationSource.Token.ThrowIfCancellationRequested();
|
||||
|
||||
// Mark that we've internally executed
|
||||
hasExecuteBeenCalled = true;
|
||||
|
||||
// Don't actually execute if there aren't any batches to execute
|
||||
if (Batches.Length == 0)
|
||||
{
|
||||
if (BatchMessageSent != null)
|
||||
{
|
||||
await BatchMessageSent(new ResultMessage(SR.QueryServiceCompletedSuccessfully, false, null));
|
||||
}
|
||||
if (QueryCompleted != null)
|
||||
{
|
||||
await QueryCompleted(this);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Locate and setup the connection
|
||||
ReliableDataSourceConnection queryConnection = await ConnectionService.Instance.GetOrOpenConnection(editorConnection.OwnerUri, ConnectionType.Query);
|
||||
|
||||
// Execute beforeBatches synchronously, before the user defined batches
|
||||
foreach (Batch b in BeforeBatches)
|
||||
{
|
||||
await b.Execute(queryConnection, cancellationSource.Token);
|
||||
}
|
||||
|
||||
// We need these to execute synchronously, otherwise the user will be very unhappy
|
||||
foreach (Batch b in Batches)
|
||||
{
|
||||
// Add completion callbacks
|
||||
b.BatchStart += BatchStarted;
|
||||
b.BatchCompletion += BatchCompleted;
|
||||
b.BatchMessageSent += BatchMessageSent;
|
||||
b.ResultSetCompletion += ResultSetCompleted;
|
||||
b.ResultSetAvailable += ResultSetAvailable;
|
||||
b.ResultSetUpdated += ResultSetUpdated;
|
||||
await b.Execute(queryConnection, cancellationSource.Token);
|
||||
}
|
||||
|
||||
// Execute afterBatches synchronously, after the user defined batches
|
||||
foreach (Batch b in AfterBatches)
|
||||
{
|
||||
await b.Execute(queryConnection, cancellationSource.Token);
|
||||
}
|
||||
|
||||
// Call the query execution callback
|
||||
if (QueryCompleted != null)
|
||||
{
|
||||
await QueryCompleted(this);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
HasErrored = true;
|
||||
if (e is OperationCanceledException)
|
||||
{
|
||||
await BatchMessageSent(new ResultMessage(SR.QueryServiceQueryCancelled, false, null));
|
||||
}
|
||||
// Call the query failure callback
|
||||
if (QueryFailed != null)
|
||||
{
|
||||
await QueryFailed(this, e);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
foreach (Batch b in Batches)
|
||||
{
|
||||
if (b.HasError)
|
||||
{
|
||||
ConnectionService.EnsureConnectionIsOpen(sqlConn);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Function to add a new batch to a Batch set
|
||||
/// </summary>
|
||||
private static void AddBatch(string query, ICollection<Batch> batchSet, IFileStreamFactory outputFactory)
|
||||
{
|
||||
batchSet.Add(new Batch(query, null, batchSet.Count, outputFactory, 1));
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region IDisposable Implementation
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
cancellationSource.Dispose();
|
||||
foreach (Batch b in Batches)
|
||||
{
|
||||
b.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
disposed = true;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using Microsoft.SqlTools.Hosting.Protocol.Contracts;
|
||||
using Microsoft.Kusto.ServiceLayer.SqlContext;
|
||||
|
||||
namespace Microsoft.Kusto.ServiceLayer.QueryExecution.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// Parameters for the query execution options request
|
||||
/// </summary>
|
||||
public class QueryExecutionOptionsParams
|
||||
{
|
||||
public string OwnerUri { get; set; }
|
||||
|
||||
public QueryExecutionSettings Options { get; set; }
|
||||
}
|
||||
|
||||
public class QueryExecutionOptionsRequest
|
||||
{
|
||||
public static readonly
|
||||
RequestType<QueryExecutionOptionsParams, bool> Type =
|
||||
RequestType<QueryExecutionOptionsParams, bool>.Create("query/setexecutionoptions");
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,223 @@
|
||||
//
|
||||
// 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 Microsoft.Kusto.ServiceLayer.SqlContext;
|
||||
|
||||
namespace Microsoft.Kusto.ServiceLayer.QueryExecution
|
||||
{
|
||||
/// <summary>
|
||||
/// Service for executing queries
|
||||
/// </summary>
|
||||
public class QuerySettingsHelper
|
||||
{
|
||||
//strings for various "SET <option> ON/OFF" statements
|
||||
private static readonly string s_On = "ON";
|
||||
private static readonly string s_Off = "OFF";
|
||||
private static readonly string s_Low = "LOW";
|
||||
private static readonly string s_Normal = "NORMAL";
|
||||
private static readonly string s_SetNoCount = "SET NOCOUNT {0}";
|
||||
private static readonly string s_SetConcatenationNull = "SET CONCAT_NULL_YIELDS_NULL {0}";
|
||||
private static readonly string s_SetNumericAbort = "SET NUMERIC_ROUNDABORT {0}";
|
||||
private static readonly string s_SetXACTAbort = "SET XACT_ABORT {0}";
|
||||
private static readonly string s_SetArithAbort = "SET ARITHABORT {0}";
|
||||
private static readonly string s_SetRowCount = "SET ROWCOUNT {0}";
|
||||
private static readonly string s_SetLockTimeout = "SET LOCK_TIMEOUT {0}";
|
||||
private static readonly string s_SetTextSize = "SET TEXTSIZE {0}";
|
||||
private static readonly string s_SetQueryGovernorCost = "SET QUERY_GOVERNOR_COST_LIMIT {0}";
|
||||
private static readonly string s_SetDeadlockPriority = "SET DEADLOCK_PRIORITY {0}";
|
||||
private static readonly string s_SetTranIsolationLevel = "SET TRANSACTION ISOLATION LEVEL {0}";
|
||||
private static readonly string s_SetAnsiNulls = "SET ANSI_NULLS {0}";
|
||||
private static readonly string s_SetAnsiNullDefault = "SET ANSI_NULL_DFLT_ON {0}";
|
||||
private static readonly string s_SetAnsiPadding = "SET ANSI_PADDING {0}";
|
||||
private static readonly string s_SetAnsiWarnings = "SET ANSI_WARNINGS {0}";
|
||||
private static readonly string s_SetCursorCloseOnCommit = "SET CURSOR_CLOSE_ON_COMMIT {0}";
|
||||
private static readonly string s_SetImplicitTransaction = "SET IMPLICIT_TRANSACTIONS {0}";
|
||||
private static readonly string s_SetQuotedIdentifier = "SET QUOTED_IDENTIFIER {0}";
|
||||
private static readonly string s_SetNoExec = "SET NOEXEC {0}";
|
||||
private static readonly string s_SetStatisticsTime = "SET STATISTICS TIME {0}";
|
||||
private static readonly string s_SetStatisticsIO = "SET STATISTICS IO {0}";
|
||||
private static readonly string s_SetParseOnly = "SET PARSEONLY {0}";
|
||||
|
||||
private QueryExecutionSettings settings;
|
||||
|
||||
public QuerySettingsHelper(QueryExecutionSettings settings)
|
||||
{
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
public string SetNoCountString
|
||||
{
|
||||
get
|
||||
{
|
||||
return string.Format(System.Globalization.CultureInfo.InvariantCulture, s_SetNoCount, (this.settings.NoCount ? s_On : s_Off));
|
||||
}
|
||||
}
|
||||
|
||||
public string SetConcatenationNullString
|
||||
{
|
||||
get
|
||||
{
|
||||
return string.Format(System.Globalization.CultureInfo.InvariantCulture, s_SetConcatenationNull, (this.settings.ConcatNullYieldsNull ? s_On : s_Off));
|
||||
}
|
||||
}
|
||||
|
||||
public string SetNumericAbortString
|
||||
{
|
||||
get
|
||||
{
|
||||
return string.Format(System.Globalization.CultureInfo.InvariantCulture, s_SetNumericAbort, (this.settings.ArithAbort ? s_On : s_Off));
|
||||
}
|
||||
}
|
||||
|
||||
public string SetXactAbortString
|
||||
{
|
||||
get
|
||||
{
|
||||
return string.Format(System.Globalization.CultureInfo.InvariantCulture, s_SetXACTAbort, (this.settings.XactAbortOn ? s_On : s_Off));
|
||||
}
|
||||
}
|
||||
|
||||
public string SetArithAbortString
|
||||
{
|
||||
get
|
||||
{
|
||||
return string.Format(System.Globalization.CultureInfo.InvariantCulture, s_SetArithAbort, (this.settings.ArithAbort ? s_On : s_Off));
|
||||
}
|
||||
}
|
||||
|
||||
public string SetRowCountString
|
||||
{
|
||||
get
|
||||
{
|
||||
return string.Format(System.Globalization.CultureInfo.InvariantCulture, s_SetRowCount, this.settings.RowCount);
|
||||
}
|
||||
}
|
||||
|
||||
public string SetLockTimeoutString
|
||||
{
|
||||
get
|
||||
{
|
||||
return string.Format(System.Globalization.CultureInfo.InvariantCulture, s_SetLockTimeout, this.settings.LockTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
public string SetTextSizeString
|
||||
{
|
||||
get
|
||||
{
|
||||
return string.Format(System.Globalization.CultureInfo.InvariantCulture, s_SetTextSize, this.settings.TextSize);
|
||||
}
|
||||
}
|
||||
|
||||
public string SetQueryGovernorCostString
|
||||
{
|
||||
get
|
||||
{
|
||||
return string.Format(System.Globalization.CultureInfo.InvariantCulture, s_SetQueryGovernorCost, this.settings.QueryGovernorCostLimit);
|
||||
}
|
||||
}
|
||||
|
||||
public string SetDeadlockPriorityString
|
||||
{
|
||||
get
|
||||
{
|
||||
|
||||
bool isDeadlockPriorityLow = string.Compare(this.settings.DeadlockPriority, "low", StringComparison.OrdinalIgnoreCase) == 0;
|
||||
return string.Format(System.Globalization.CultureInfo.InvariantCulture, s_SetDeadlockPriority, (isDeadlockPriorityLow ? s_Low : s_Normal));
|
||||
}
|
||||
}
|
||||
|
||||
public string SetTransactionIsolationLevelString
|
||||
{
|
||||
get
|
||||
{
|
||||
return string.Format(System.Globalization.CultureInfo.InvariantCulture, s_SetTranIsolationLevel, this.settings.TransactionIsolationLevel);
|
||||
}
|
||||
}
|
||||
|
||||
public string SetAnsiNullsString
|
||||
{
|
||||
get
|
||||
{
|
||||
return string.Format(System.Globalization.CultureInfo.InvariantCulture, s_SetAnsiNulls, (this.settings.AnsiNulls ? s_On : s_Off));
|
||||
}
|
||||
}
|
||||
|
||||
public string SetAnsiNullDefaultString
|
||||
{
|
||||
get
|
||||
{
|
||||
return string.Format(System.Globalization.CultureInfo.InvariantCulture, s_SetAnsiNullDefault, (this.settings.AnsiNullDefaultOn ? s_On : s_Off));
|
||||
}
|
||||
}
|
||||
|
||||
public string SetAnsiPaddingString
|
||||
{
|
||||
get
|
||||
{
|
||||
return string.Format(System.Globalization.CultureInfo.InvariantCulture, s_SetAnsiPadding, (this.settings.AnsiPadding ? s_On : s_Off));
|
||||
}
|
||||
}
|
||||
|
||||
public string SetAnsiWarningsString
|
||||
{
|
||||
get
|
||||
{
|
||||
return string.Format(System.Globalization.CultureInfo.InvariantCulture, s_SetAnsiWarnings, (this.settings.AnsiWarnings ? s_On : s_Off));
|
||||
}
|
||||
}
|
||||
|
||||
public string SetCursorCloseOnCommitString
|
||||
{
|
||||
get
|
||||
{
|
||||
return string.Format(System.Globalization.CultureInfo.InvariantCulture, s_SetCursorCloseOnCommit, (this.settings.CursorCloseOnCommit ? s_On : s_Off));
|
||||
}
|
||||
}
|
||||
|
||||
public string SetImplicitTransactionString
|
||||
{
|
||||
get
|
||||
{
|
||||
return string.Format(System.Globalization.CultureInfo.InvariantCulture, s_SetImplicitTransaction, (this.settings.ImplicitTransactions ? s_On : s_Off));
|
||||
}
|
||||
}
|
||||
|
||||
public string SetQuotedIdentifierString
|
||||
{
|
||||
get
|
||||
{
|
||||
return string.Format(System.Globalization.CultureInfo.InvariantCulture, s_SetQuotedIdentifier, (this.settings.QuotedIdentifier ? s_On : s_Off));
|
||||
}
|
||||
}
|
||||
|
||||
public string SetNoExecString
|
||||
{
|
||||
get
|
||||
{
|
||||
return string.Format(System.Globalization.CultureInfo.InvariantCulture, s_SetNoExec, (this.settings.NoExec ? s_On : s_Off));
|
||||
}
|
||||
}
|
||||
|
||||
public string GetSetStatisticsTimeString(bool? on)
|
||||
{
|
||||
on = on ?? this.settings.StatisticsTime;
|
||||
return string.Format(System.Globalization.CultureInfo.InvariantCulture, s_SetStatisticsTime, (on.Value ? s_On : s_Off));
|
||||
}
|
||||
|
||||
public string GetSetStatisticsIOString(bool? on)
|
||||
{
|
||||
on = on ?? this.settings.StatisticsIO;
|
||||
return string.Format(System.Globalization.CultureInfo.InvariantCulture, s_SetStatisticsIO, (on.Value ? s_On : s_Off));
|
||||
}
|
||||
|
||||
public string GetSetParseOnlyString(bool? on)
|
||||
{
|
||||
on = on ?? this.settings.ParseOnly;
|
||||
return string.Format(System.Globalization.CultureInfo.InvariantCulture, s_SetParseOnly, (on.Value ? s_On : s_Off));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
using Microsoft.SqlTools.Hosting.Protocol;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.SqlTools.Hosting.Protocol.Contracts;
|
||||
|
||||
namespace Microsoft.Kusto.ServiceLayer.QueryExecution
|
||||
{
|
||||
/// <summary>
|
||||
/// Implementation of IEventSender that swallows events without doing anything with them.
|
||||
/// In the future this class could be used to roll up all the events and send
|
||||
/// them all at once
|
||||
/// </summary>
|
||||
public class ResultOnlyContext<TResult> : IEventSender
|
||||
{
|
||||
private readonly RequestContext<TResult> OrigContext;
|
||||
|
||||
public ResultOnlyContext(RequestContext<TResult> context) {
|
||||
OrigContext = context;
|
||||
}
|
||||
|
||||
public virtual Task SendEvent<TParams>(EventType<TParams> eventType, TParams eventParams)
|
||||
{
|
||||
// no op to swallow events
|
||||
// in the future this could be used to roll up events and send them back in the result
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
public virtual Task SendError(string errorMessage, int errorCode = 0)
|
||||
{
|
||||
return OrigContext.SendError(errorMessage, errorCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
748
src/Microsoft.Kusto.ServiceLayer/QueryExecution/ResultSet.cs
Normal file
748
src/Microsoft.Kusto.ServiceLayer/QueryExecution/ResultSet.cs
Normal file
@@ -0,0 +1,748 @@
|
||||
//
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using Microsoft.Kusto.ServiceLayer.QueryExecution.Contracts;
|
||||
using Microsoft.Kusto.ServiceLayer.QueryExecution.DataStorage;
|
||||
using Microsoft.Kusto.ServiceLayer.Utility;
|
||||
using Microsoft.SqlTools.Utility;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Data.Common;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Kusto.ServiceLayer.QueryExecution
|
||||
{
|
||||
/// <summary>
|
||||
/// Class that represents a resultset the was generated from a query. Contains logic for
|
||||
/// storing and retrieving results. Is contained by a Batch class.
|
||||
/// </summary>
|
||||
public class ResultSet : IDisposable
|
||||
{
|
||||
#region Constants
|
||||
|
||||
// Column names of 'for xml' and 'for json' queries
|
||||
private const string NameOfForXmlColumn = "XML_F52E2B61-18A1-11d1-B105-00805F49916B";
|
||||
private const string NameOfForJsonColumn = "JSON_F52E2B61-18A1-11d1-B105-00805F49916B";
|
||||
private const string YukonXmlShowPlanColumn = "Microsoft SQL Server 2005 XML Showplan";
|
||||
private const uint MaxResultsTimerPulseMilliseconds = 1000;
|
||||
private const uint MinResultTimerPulseMilliseconds = 10;
|
||||
#endregion
|
||||
|
||||
#region Member Variables
|
||||
|
||||
/// <summary>
|
||||
/// For IDisposable pattern, whether or not object has been disposed
|
||||
/// </summary>
|
||||
private bool disposed;
|
||||
|
||||
/// <summary>
|
||||
/// A list of offsets into the buffer file that correspond to where rows start
|
||||
/// </summary>
|
||||
private readonly LongList<long> fileOffsets;
|
||||
|
||||
/// <summary>
|
||||
/// The factory to use to get reading/writing handlers
|
||||
/// </summary>
|
||||
private readonly IFileStreamFactory fileStreamFactory;
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not the result set has been read in from the database,
|
||||
/// set as internal in order to fake value in unit tests.
|
||||
/// This gets set as soon as we start reading.
|
||||
/// </summary>
|
||||
internal bool hasStartedRead = false;
|
||||
|
||||
/// <summary>
|
||||
/// Set when all results have been read for this resultSet from the server
|
||||
/// </summary>
|
||||
private bool hasCompletedRead = false;
|
||||
|
||||
/// <summary>
|
||||
/// The name of the temporary file we're using to output these results in
|
||||
/// </summary>
|
||||
private readonly string outputFileName;
|
||||
|
||||
/// <summary>
|
||||
/// Row count to use in special scenarios where we want to override the number of rows.
|
||||
/// </summary>
|
||||
private long? rowCountOverride=null;
|
||||
|
||||
/// <summary>
|
||||
/// The special action which applied to this result set
|
||||
/// </summary>
|
||||
private readonly SpecialAction specialAction;
|
||||
|
||||
/// <summary>
|
||||
/// Total number of bytes written to the file. Used to jump to end of the file for append
|
||||
/// scenarios. Internal for unit test validation.
|
||||
/// </summary>
|
||||
internal long totalBytesWritten;
|
||||
|
||||
private readonly Timer resultsTimer;
|
||||
|
||||
private readonly SemaphoreSlim sendResultsSemphore = new SemaphoreSlim(1);
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new result set and initializes its state
|
||||
/// </summary>
|
||||
/// <param name="ordinal">The ID of the resultset, the ordinal of the result within the batch</param>
|
||||
/// <param name="batchOrdinal">The ID of the batch, the ordinal of the batch within the query</param>
|
||||
/// <param name="factory">Factory for creating a reader/writer</param>
|
||||
public ResultSet(int ordinal, int batchOrdinal, IFileStreamFactory factory)
|
||||
{
|
||||
Id = ordinal;
|
||||
BatchId = batchOrdinal;
|
||||
|
||||
// Initialize the storage
|
||||
totalBytesWritten = 0;
|
||||
outputFileName = factory.CreateFile();
|
||||
fileOffsets = new LongList<long>();
|
||||
specialAction = new SpecialAction();
|
||||
|
||||
// Store the factory
|
||||
fileStreamFactory = factory;
|
||||
hasStartedRead = false;
|
||||
hasCompletedRead = false;
|
||||
SaveTasks = new ConcurrentDictionary<string, Task>();
|
||||
resultsTimer = new Timer(SendResultAvailableOrUpdated);
|
||||
}
|
||||
|
||||
#region Eventing
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronous handler for when saving query results succeeds
|
||||
/// </summary>
|
||||
/// <param name="parameters">Request parameters for identifying the request</param>
|
||||
public delegate Task SaveAsAsyncEventHandler(SaveResultsRequestParams parameters);
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronous handler for when saving query results fails
|
||||
/// </summary>
|
||||
/// <param name="parameters">Request parameters for identifying the request</param>
|
||||
/// <param name="message">Message to send back describing why the request failed</param>
|
||||
public delegate Task SaveAsFailureAsyncEventHandler(SaveResultsRequestParams parameters, string message);
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronous handler for when a resultset is available/updated/completed
|
||||
/// </summary>
|
||||
/// <param name="resultSet">The result set that completed</param>
|
||||
public delegate Task ResultSetAsyncEventHandler(ResultSet resultSet);
|
||||
|
||||
/// <summary>
|
||||
/// Event that will be called when the result set has completed execution
|
||||
/// </summary>
|
||||
public event ResultSetAsyncEventHandler ResultCompletion;
|
||||
|
||||
/// <summary>
|
||||
/// Event that will be called when the resultSet first becomes available. This is as soon as we start reading the results.
|
||||
/// </summary>
|
||||
public event ResultSetAsyncEventHandler ResultAvailable;
|
||||
|
||||
/// <summary>
|
||||
/// Event that will be called when additional rows in the result set are available (rowCount available has increased)
|
||||
/// </summary>
|
||||
public event ResultSetAsyncEventHandler ResultUpdated;
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// The columns for this result set
|
||||
/// </summary>
|
||||
public DbColumnWrapper[] Columns { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// ID of the result set, relative to the batch
|
||||
/// </summary>
|
||||
public int Id { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// ID of the batch set, relative to the query
|
||||
/// </summary>
|
||||
public int BatchId { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of rows for this result set
|
||||
/// </summary>
|
||||
public long RowCount => rowCountOverride != null ? Math.Min(rowCountOverride.Value, fileOffsets.Count) : fileOffsets.Count;
|
||||
|
||||
/// <summary>
|
||||
/// All save tasks currently saving this ResultSet
|
||||
/// </summary>
|
||||
internal ConcurrentDictionary<string, Task> SaveTasks { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Generates a summary of this result set
|
||||
/// </summary>
|
||||
public ResultSetSummary Summary
|
||||
{
|
||||
get
|
||||
{
|
||||
return new ResultSetSummary
|
||||
{
|
||||
ColumnInfo = Columns,
|
||||
Id = Id,
|
||||
BatchId = BatchId,
|
||||
RowCount = RowCount,
|
||||
Complete = hasCompletedRead,
|
||||
SpecialAction = hasCompletedRead ? ProcessSpecialAction() : null
|
||||
};
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>
|
||||
/// Returns a specific row from the result set.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Creates a new file reader for a single reader. This method should only be used for one
|
||||
/// off requests, not for requesting a large subset of the results.
|
||||
/// </remarks>
|
||||
/// <param name="rowId">The internal ID of the row to read</param>
|
||||
/// <returns>The requested row</returns>
|
||||
public IList<DbCellValue> GetRow(long rowId)
|
||||
{
|
||||
// Sanity check to make sure that results read has started
|
||||
if (!hasStartedRead)
|
||||
{
|
||||
throw new InvalidOperationException(SR.QueryServiceResultSetNotRead);
|
||||
}
|
||||
|
||||
// Sanity check to make sure that the row exists
|
||||
if (rowId >= RowCount)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(rowId), SR.QueryServiceResultSetStartRowOutOfRange);
|
||||
}
|
||||
|
||||
using (IFileStreamReader fileStreamReader = fileStreamFactory.GetReader(outputFileName))
|
||||
{
|
||||
return fileStreamReader.ReadRow(fileOffsets[rowId], rowId, Columns);
|
||||
}
|
||||
}
|
||||
|
||||
/// <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 Task<ResultSetSubset> GetSubset(long startRow, int rowCount)
|
||||
{
|
||||
// Sanity check to make sure that results read has started
|
||||
if (!hasStartedRead)
|
||||
{
|
||||
throw new InvalidOperationException(SR.QueryServiceResultSetNotRead);
|
||||
}
|
||||
|
||||
// Sanity check to make sure that the row and the row count are within bounds
|
||||
if (startRow < 0 || startRow >= RowCount)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(startRow), SR.QueryServiceResultSetStartRowOutOfRange);
|
||||
}
|
||||
if (rowCount <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(rowCount), SR.QueryServiceResultSetRowCountOutOfRange);
|
||||
}
|
||||
|
||||
return Task.Factory.StartNew(() =>
|
||||
{
|
||||
|
||||
DbCellValue[][] rows = null;
|
||||
|
||||
using (IFileStreamReader fileStreamReader = fileStreamFactory.GetReader(outputFileName))
|
||||
{
|
||||
// Figure out which rows we need to read back
|
||||
IEnumerable<long> rowOffsets = fileOffsets.LongSkip(startRow).Take(rowCount);
|
||||
|
||||
// Iterate over the rows we need and process them into output
|
||||
// ReSharper disable once AccessToDisposedClosure The lambda is used immediately in .ToArray call
|
||||
rows = rowOffsets.Select((offset, id) => fileStreamReader.ReadRow(offset, id, Columns).ToArray()).ToArray();
|
||||
}
|
||||
// Retrieve the subset of the results as per the request
|
||||
return new ResultSetSubset
|
||||
{
|
||||
Rows = rows,
|
||||
RowCount = rows.Length
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates the execution plan from the table returned
|
||||
/// </summary>
|
||||
/// <returns>An execution plan object</returns>
|
||||
public Task<ExecutionPlan> GetExecutionPlan()
|
||||
{
|
||||
// Process the action just in case it hasn't been yet
|
||||
ProcessSpecialAction();
|
||||
|
||||
// Sanity check to make sure that results read has started
|
||||
if (!hasStartedRead)
|
||||
{
|
||||
throw new InvalidOperationException(SR.QueryServiceResultSetNotRead);
|
||||
}
|
||||
// Check that we this result set contains a showplan
|
||||
if (!specialAction.ExpectYukonXMLShowPlan)
|
||||
{
|
||||
throw new Exception(SR.QueryServiceExecutionPlanNotFound);
|
||||
}
|
||||
|
||||
|
||||
return Task.Factory.StartNew(() =>
|
||||
{
|
||||
string content;
|
||||
string format = null;
|
||||
|
||||
using (IFileStreamReader fileStreamReader = fileStreamFactory.GetReader(outputFileName))
|
||||
{
|
||||
// Determine the format and get the first col/row of XML
|
||||
content = fileStreamReader.ReadRow(0, 0, Columns)[0].DisplayValue;
|
||||
|
||||
if (specialAction.ExpectYukonXMLShowPlan)
|
||||
{
|
||||
format = "xml";
|
||||
}
|
||||
}
|
||||
|
||||
return new ExecutionPlan
|
||||
{
|
||||
Format = format,
|
||||
Content = content
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads from the reader until there are no more results to read
|
||||
/// </summary>
|
||||
/// <param name="dbDataReader">The data reader for getting results from the db</param>
|
||||
/// <param name="cancellationToken">Cancellation token for cancelling the query</param>
|
||||
public async Task ReadResultToEnd(IDataReader dbDataReader, CancellationToken cancellationToken)
|
||||
{
|
||||
// Sanity check to make sure we got a reader
|
||||
//
|
||||
Validate.IsNotNull(nameof(dbDataReader), dbDataReader);
|
||||
|
||||
Task availableTask = null;
|
||||
try
|
||||
{
|
||||
// Verify the request hasn't been cancelled
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
StorageDataReader dataReader = new StorageDataReader(dbDataReader);
|
||||
|
||||
// Open a writer for the file
|
||||
//
|
||||
var fileWriter = fileStreamFactory.GetWriter(outputFileName);
|
||||
using (fileWriter)
|
||||
{
|
||||
Columns = dataReader.Columns;
|
||||
|
||||
// Mark that read of result has started
|
||||
//
|
||||
hasStartedRead = true;
|
||||
|
||||
// Invoke the SendCurrentResults() asynchronously that will send the results available notification
|
||||
// and also trigger the timer to send periodic updates.
|
||||
//
|
||||
availableTask = SendCurrentResults();
|
||||
|
||||
while (await dataReader.ReadAsync(cancellationToken))
|
||||
{
|
||||
fileOffsets.Add(totalBytesWritten);
|
||||
totalBytesWritten += fileWriter.WriteRow(dataReader);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
// await the completion of available notification in case it is not already done before proceeding
|
||||
//
|
||||
await availableTask;
|
||||
|
||||
// now set the flag to indicate that we are done reading. this equates to Complete flag to be marked 'True' in any future notifications.
|
||||
//
|
||||
hasCompletedRead = true;
|
||||
|
||||
|
||||
// Make a final call to SendCurrentResults() and await its completion. If the previously scheduled task already took care of latest status send then this should be a no-op
|
||||
//
|
||||
await SendCurrentResults();
|
||||
|
||||
|
||||
// and finally:
|
||||
// Make a call to send ResultCompletion and await its completion. This is just for backward compatibility with older protocol
|
||||
//
|
||||
await (ResultCompletion?.Invoke(this) ?? Task.CompletedTask);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a row from the result set cache
|
||||
/// </summary>
|
||||
/// <param name="internalId">Internal ID of the row</param>
|
||||
public void RemoveRow(long internalId)
|
||||
{
|
||||
// Sanity check to make sure that results read has started
|
||||
if (!hasStartedRead)
|
||||
{
|
||||
throw new InvalidOperationException(SR.QueryServiceResultSetNotRead);
|
||||
}
|
||||
|
||||
// Simply remove the row from the list of row offsets
|
||||
fileOffsets.RemoveAt(internalId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new row to the result set by reading the row from the provided db data reader
|
||||
/// </summary>
|
||||
/// <param name="dbDataReader">The result of a command to insert a new row should be UNREAD</param>
|
||||
public async Task AddRow(DbDataReader dbDataReader)
|
||||
{
|
||||
// Write the new row to the end of the file
|
||||
long newOffset = await AppendRowToBuffer(dbDataReader);
|
||||
|
||||
// Add the row to file offset list
|
||||
fileOffsets.Add(newOffset);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the values in a row with the
|
||||
/// </summary>
|
||||
/// <param name="rowId"></param>
|
||||
/// <param name="dbDataReader"></param>
|
||||
/// <returns></returns>
|
||||
public async Task UpdateRow(long rowId, DbDataReader dbDataReader)
|
||||
{
|
||||
// Write the updated row to the end of the file
|
||||
long newOffset = await AppendRowToBuffer(dbDataReader);
|
||||
|
||||
// Update the file offset of the row in question
|
||||
fileOffsets[rowId] = newOffset;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves the contents of this result set to a file using the IFileStreamFactory provided
|
||||
/// </summary>
|
||||
/// <param name="saveParams">Parameters for saving the results to a file</param>
|
||||
/// <param name="fileFactory">
|
||||
/// Factory for creating a stream reader/writer combo for writing results to disk
|
||||
/// </param>
|
||||
/// <param name="successHandler">Handler for a successful write of all rows</param>
|
||||
/// <param name="failureHandler">Handler for unsuccessful write of all rows</param>
|
||||
public void SaveAs(SaveResultsRequestParams saveParams, IFileStreamFactory fileFactory,
|
||||
SaveAsAsyncEventHandler successHandler, SaveAsFailureAsyncEventHandler failureHandler)
|
||||
{
|
||||
// Sanity check the save params and file factory
|
||||
Validate.IsNotNull(nameof(saveParams), saveParams);
|
||||
Validate.IsNotNull(nameof(fileFactory), fileFactory);
|
||||
|
||||
// Make sure the resultset has finished being read
|
||||
if (!hasCompletedRead)
|
||||
{
|
||||
throw new InvalidOperationException(SR.QueryServiceSaveAsResultSetNotComplete);
|
||||
}
|
||||
|
||||
// Make sure there isn't a task for this file already
|
||||
Task existingTask;
|
||||
if (SaveTasks.TryGetValue(saveParams.FilePath, out existingTask))
|
||||
{
|
||||
if (existingTask.IsCompleted)
|
||||
{
|
||||
// The task has completed, so let's attempt to remove it
|
||||
if (!SaveTasks.TryRemove(saveParams.FilePath, out existingTask))
|
||||
{
|
||||
throw new InvalidOperationException(SR.QueryServiceSaveAsMiscStartingError);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// The task hasn't completed, so we shouldn't continue
|
||||
throw new InvalidOperationException(SR.QueryServiceSaveAsInProgress);
|
||||
}
|
||||
}
|
||||
|
||||
// Create the new task
|
||||
Task saveAsTask = new Task(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
// Set row counts depending on whether save request is for entire set or a subset
|
||||
long rowEndIndex = RowCount;
|
||||
int rowStartIndex = 0;
|
||||
if (saveParams.IsSaveSelection)
|
||||
{
|
||||
// ReSharper disable PossibleInvalidOperationException IsSaveSelection verifies these values exist
|
||||
rowEndIndex = saveParams.RowEndIndex.Value + 1;
|
||||
rowStartIndex = saveParams.RowStartIndex.Value;
|
||||
// ReSharper restore PossibleInvalidOperationException
|
||||
}
|
||||
|
||||
using (var fileReader = fileFactory.GetReader(outputFileName))
|
||||
using (var fileWriter = fileFactory.GetWriter(saveParams.FilePath))
|
||||
{
|
||||
// Iterate over the rows that are in the selected row set
|
||||
for (long i = rowStartIndex; i < rowEndIndex; ++i)
|
||||
{
|
||||
var row = fileReader.ReadRow(fileOffsets[i], i, Columns);
|
||||
fileWriter.WriteRow(row, Columns);
|
||||
}
|
||||
if (successHandler != null)
|
||||
{
|
||||
await successHandler(saveParams);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
fileFactory.DisposeFile(saveParams.FilePath);
|
||||
if (failureHandler != null)
|
||||
{
|
||||
await failureHandler(saveParams, e.Message);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Add exception handling to the save task
|
||||
Task taskWithHandling = saveAsTask.ContinueWithOnFaulted(async t =>
|
||||
{
|
||||
if (failureHandler != null)
|
||||
{
|
||||
await failureHandler(saveParams, t.Exception.Message);
|
||||
}
|
||||
});
|
||||
|
||||
// If saving the task fails, return a failure
|
||||
if (!SaveTasks.TryAdd(saveParams.FilePath, taskWithHandling))
|
||||
{
|
||||
throw new InvalidOperationException(SR.QueryServiceSaveAsMiscStartingError);
|
||||
}
|
||||
|
||||
// Task was saved, so start up the task
|
||||
saveAsTask.Start();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IDisposable Implementation
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
resultsTimer.Dispose();
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if saveTasks are running for this ResultSet
|
||||
if (!SaveTasks.IsEmpty)
|
||||
{
|
||||
// Wait for tasks to finish before disposing ResultSet
|
||||
Task.WhenAll(SaveTasks.Values.ToArray()).ContinueWith(antecedent =>
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
fileStreamFactory.DisposeFile(outputFileName);
|
||||
}
|
||||
disposed = true;
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
// If saveTasks is empty, continue with dispose
|
||||
if (disposing)
|
||||
{
|
||||
fileStreamFactory.DisposeFile(outputFileName);
|
||||
}
|
||||
disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Helper Methods
|
||||
/// <summary>
|
||||
/// Sends the ResultsUpdated message if the number of rows has changed since last send.
|
||||
/// </summary>
|
||||
/// <param name="stateInfo"></param>
|
||||
private void SendResultAvailableOrUpdated (object stateInfo = null)
|
||||
{
|
||||
// Make the call to send current results and synchronously wait for it to finish
|
||||
//
|
||||
SendCurrentResults().Wait();
|
||||
}
|
||||
|
||||
private async Task SendCurrentResults()
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
// Wait to acquire the sendResultsSemphore before proceeding, as we want only one instance of this method executing at any given time.
|
||||
//
|
||||
sendResultsSemphore.Wait();
|
||||
|
||||
ResultSet currentResultSetSnapshot = (ResultSet) MemberwiseClone();
|
||||
if (LastUpdatedSummary == null) // We need to send results available message.
|
||||
{
|
||||
// Fire off results Available task and await it
|
||||
//
|
||||
await (ResultAvailable?.Invoke(currentResultSetSnapshot) ?? Task.CompletedTask);
|
||||
}
|
||||
else if (LastUpdatedSummary.Complete) // If last result summary sent had already set the Complete flag
|
||||
{
|
||||
// We do not need to do anything except that make sure that RowCount has not update since last send.
|
||||
Debug.Assert(LastUpdatedSummary.RowCount == currentResultSetSnapshot.RowCount,
|
||||
$"Already reported rows should be equal to current RowCount, if had already sent completion flag as set in last message, countReported:{LastUpdatedSummary.RowCount}, current total row count: {currentResultSetSnapshot.RowCount}, row count override: {currentResultSetSnapshot.rowCountOverride}, this.rowCountOverride: {this.rowCountOverride} and this.RowCount: {this.RowCount}, LastUpdatedSummary: {LastUpdatedSummary}");
|
||||
}
|
||||
else // We need to send results updated message.
|
||||
{
|
||||
// Previously reported rows should be less than or equal to current number of rows about to be reported
|
||||
//
|
||||
Debug.Assert(LastUpdatedSummary.RowCount <= currentResultSetSnapshot.RowCount,
|
||||
$"Already reported rows should less than or equal to current total RowCount, countReported:{LastUpdatedSummary.RowCount}, current total row count: {currentResultSetSnapshot.RowCount}, row count override: {currentResultSetSnapshot.rowCountOverride}, this.rowCountOverride: {this.rowCountOverride} and this.RowCount: {this.RowCount}, LastUpdatedSummary: {LastUpdatedSummary}");
|
||||
|
||||
// If there has been no change in rowCount since last update and we have not yet completed read then log and increase the timer duration
|
||||
//
|
||||
if (!currentResultSetSnapshot.hasCompletedRead &&
|
||||
LastUpdatedSummary.RowCount == currentResultSetSnapshot.RowCount)
|
||||
{
|
||||
Logger.Write(TraceEventType.Warning,
|
||||
$"The result set:{Summary} has not made any progress in last {ResultTimerInterval} milliseconds and the read of this result set is not yet complete!");
|
||||
ResultsIntervalMultiplier++;
|
||||
}
|
||||
|
||||
// Fire off results updated task and await it
|
||||
//
|
||||
await (ResultUpdated?.Invoke(currentResultSetSnapshot) ?? Task.CompletedTask);
|
||||
}
|
||||
|
||||
// Update the LastUpdatedSummary to be the value captured in current snapshot
|
||||
//
|
||||
LastUpdatedSummary = currentResultSetSnapshot.Summary;
|
||||
|
||||
// Setup timer for the next callback
|
||||
//
|
||||
if (currentResultSetSnapshot.hasCompletedRead)
|
||||
{
|
||||
// If we have already completed reading then we are done and we do not need to send any more updates. Switch off timer.
|
||||
//
|
||||
resultsTimer.Change(Timeout.Infinite, Timeout.Infinite);
|
||||
}
|
||||
else
|
||||
{
|
||||
// If we have not yet completed reading then set the timer so this method gets called again after ResultTimerInterval milliseconds
|
||||
//
|
||||
resultsTimer.Change(ResultTimerInterval, Timeout.Infinite);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Release the sendResultsSemphore so the next invocation gets unblocked
|
||||
//
|
||||
sendResultsSemphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private uint ResultsIntervalMultiplier { get; set; } = 1;
|
||||
|
||||
internal uint ResultTimerInterval => Math.Max(Math.Min(MaxResultsTimerPulseMilliseconds, (uint)RowCount / 500 /* 1 millisec per 500 rows*/), MinResultTimerPulseMilliseconds * ResultsIntervalMultiplier);
|
||||
|
||||
internal ResultSetSummary LastUpdatedSummary { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Check columns for json type and set isJson if needed
|
||||
/// </summary>
|
||||
private void CheckForIsJson()
|
||||
{
|
||||
if (Columns?.Length > 0 && RowCount != 0)
|
||||
{
|
||||
Regex regex = new Regex(@"({.*?})");
|
||||
var row = GetRow(0);
|
||||
for (int i = 0; i < Columns.Length; i++)
|
||||
{
|
||||
if (Columns[i].DataTypeName.Equals("nvarchar"))
|
||||
{
|
||||
if (regex.IsMatch(row[i].DisplayValue))
|
||||
{
|
||||
Columns[i].IsJson = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determine the special action, if any, for this result set
|
||||
/// </summary>
|
||||
private SpecialAction ProcessSpecialAction()
|
||||
{
|
||||
|
||||
// Check if this result set is a showplan
|
||||
if (Columns.Length == 1 && string.Compare(Columns[0].ColumnName, YukonXmlShowPlanColumn, StringComparison.OrdinalIgnoreCase) == 0)
|
||||
{
|
||||
specialAction.ExpectYukonXMLShowPlan = true;
|
||||
}
|
||||
|
||||
return specialAction;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a single row to the end of the buffer file. INTENDED FOR SINGLE ROW INSERTION ONLY.
|
||||
/// </summary>
|
||||
/// <param name="dbDataReader">An UNREAD db data reader</param>
|
||||
/// <returns>The offset into the file where the row was inserted</returns>
|
||||
private async Task<long> AppendRowToBuffer(DbDataReader dbDataReader)
|
||||
{
|
||||
Validate.IsNotNull(nameof(dbDataReader), dbDataReader);
|
||||
// Sanity check to make sure that results read has started
|
||||
if (!hasStartedRead)
|
||||
{
|
||||
throw new InvalidOperationException(SR.QueryServiceResultSetNotRead);
|
||||
}
|
||||
// NOTE: We are no longer checking to see if the data reader has rows before reading
|
||||
// b/c of a quirk in SqlClient. In some scenarios, a SqlException isn't thrown until we
|
||||
// read. In order to get appropriate errors back to the user, we'll read first.
|
||||
// Returning false from .ReadAsync means there aren't any rows.
|
||||
|
||||
// Create a storage data reader, read it, make sure there were results
|
||||
StorageDataReader dataReader = new StorageDataReader(dbDataReader);
|
||||
if (!await dataReader.ReadAsync(CancellationToken.None))
|
||||
{
|
||||
throw new InvalidOperationException(SR.QueryServiceResultSetAddNoRows);
|
||||
}
|
||||
|
||||
using (IFileStreamWriter writer = fileStreamFactory.GetWriter(outputFileName))
|
||||
{
|
||||
// Write the row to the end of the file
|
||||
long currentFileOffset = totalBytesWritten;
|
||||
writer.Seek(currentFileOffset);
|
||||
totalBytesWritten += writer.WriteRow(dataReader);
|
||||
return currentFileOffset;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,318 @@
|
||||
//
|
||||
// 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.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Composition;
|
||||
using System.Diagnostics;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.SqlTools.Extensibility;
|
||||
using Microsoft.SqlTools.Hosting;
|
||||
using Microsoft.SqlTools.Hosting.Protocol;
|
||||
using Microsoft.Kusto.ServiceLayer.QueryExecution.Contracts;
|
||||
using Microsoft.Kusto.ServiceLayer.QueryExecution.DataStorage;
|
||||
using Microsoft.Kusto.ServiceLayer.Utility;
|
||||
using Microsoft.SqlTools.Utility;
|
||||
|
||||
|
||||
namespace Microsoft.Kusto.ServiceLayer.QueryExecution
|
||||
{
|
||||
|
||||
[Export(typeof(IHostedService))]
|
||||
public class SerializationService : HostedService<SerializationService>, IComposableService
|
||||
{
|
||||
private ConcurrentDictionary<string, DataSerializer> inProgressSerializations;
|
||||
|
||||
public SerializationService()
|
||||
{
|
||||
inProgressSerializations = new ConcurrentDictionary<string, DataSerializer>();
|
||||
}
|
||||
|
||||
public override void InitializeService(IProtocolEndpoint serviceHost)
|
||||
{
|
||||
Logger.Write(TraceEventType.Verbose, "SerializationService initialized");
|
||||
serviceHost.SetRequestHandler(SerializeStartRequest.Type, HandleSerializeStartRequest);
|
||||
serviceHost.SetRequestHandler(SerializeContinueRequest.Type, HandleSerializeContinueRequest);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Begin to process request to save a resultSet to a file in CSV format
|
||||
/// </summary>
|
||||
internal Task HandleSerializeStartRequest(SerializeDataStartRequestParams serializeParams,
|
||||
RequestContext<SerializeDataResult> requestContext)
|
||||
{
|
||||
// Run in separate thread so that message thread isn't held up by a potentially time consuming file write
|
||||
Task.Run(async () => {
|
||||
await RunSerializeStartRequest(serializeParams, requestContext);
|
||||
}).ContinueWithOnFaulted(async t => await SendErrorAndCleanup(serializeParams?.FilePath, requestContext, t.Exception));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
internal async Task RunSerializeStartRequest(SerializeDataStartRequestParams serializeParams, RequestContext<SerializeDataResult> requestContext)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Verify we have sensible inputs and there isn't a task running for this file already
|
||||
Validate.IsNotNull(nameof(serializeParams), serializeParams);
|
||||
Validate.IsNotNullOrWhitespaceString("FilePath", serializeParams.FilePath);
|
||||
|
||||
DataSerializer serializer = null;
|
||||
if (inProgressSerializations.TryGetValue(serializeParams.FilePath, out serializer))
|
||||
{
|
||||
// Cannot proceed as there is an in progress serialization happening
|
||||
throw new Exception(SR.SerializationServiceRequestInProgress(serializeParams.FilePath));
|
||||
}
|
||||
|
||||
// Create a new serializer, save for future calls if needed, and write the request out
|
||||
serializer = new DataSerializer(serializeParams);
|
||||
if (!serializeParams.IsLastBatch)
|
||||
{
|
||||
inProgressSerializations.AddOrUpdate(serializer.FilePath, serializer, (key, old) => serializer);
|
||||
}
|
||||
|
||||
Logger.Write(TraceEventType.Verbose, "HandleSerializeStartRequest");
|
||||
SerializeDataResult result = serializer.ProcessRequest(serializeParams);
|
||||
await requestContext.SendResult(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await SendErrorAndCleanup(serializeParams.FilePath, requestContext, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SendErrorAndCleanup(string filePath, RequestContext<SerializeDataResult> requestContext, Exception ex)
|
||||
{
|
||||
if (filePath != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
DataSerializer removed;
|
||||
inProgressSerializations.TryRemove(filePath, out removed);
|
||||
if (removed != null)
|
||||
{
|
||||
// Flush any contents to disk and remove the writer
|
||||
removed.CloseStreams();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Do not care if there was an error removing this, must always delete if something failed
|
||||
}
|
||||
}
|
||||
await requestContext.SendError(ex.Message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process request to save a resultSet to a file in CSV format
|
||||
/// </summary>
|
||||
internal Task HandleSerializeContinueRequest(SerializeDataContinueRequestParams serializeParams,
|
||||
RequestContext<SerializeDataResult> requestContext)
|
||||
{
|
||||
// Run in separate thread so that message thread isn't held up by a potentially time consuming file write
|
||||
Task.Run(async () =>
|
||||
{
|
||||
await RunSerializeContinueRequest(serializeParams, requestContext);
|
||||
}).ContinueWithOnFaulted(async t => await SendErrorAndCleanup(serializeParams?.FilePath, requestContext, t.Exception));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
internal async Task RunSerializeContinueRequest(SerializeDataContinueRequestParams serializeParams, RequestContext<SerializeDataResult> requestContext)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Verify we have sensible inputs and some data has already been sent for the file
|
||||
Validate.IsNotNull(nameof(serializeParams), serializeParams);
|
||||
Validate.IsNotNullOrWhitespaceString("FilePath", serializeParams.FilePath);
|
||||
|
||||
DataSerializer serializer = null;
|
||||
if (!inProgressSerializations.TryGetValue(serializeParams.FilePath, out serializer))
|
||||
{
|
||||
throw new Exception(SR.SerializationServiceRequestNotFound(serializeParams.FilePath));
|
||||
}
|
||||
|
||||
// Write to file and cleanup if needed
|
||||
Logger.Write(TraceEventType.Verbose, "HandleSerializeContinueRequest");
|
||||
SerializeDataResult result = serializer.ProcessRequest(serializeParams);
|
||||
if (serializeParams.IsLastBatch)
|
||||
{
|
||||
// Cleanup the serializer
|
||||
this.inProgressSerializations.TryRemove(serializer.FilePath, out serializer);
|
||||
}
|
||||
await requestContext.SendResult(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await SendErrorAndCleanup(serializeParams.FilePath, requestContext, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
class DataSerializer
|
||||
{
|
||||
private IFileStreamWriter writer;
|
||||
private SerializeDataStartRequestParams requestParams;
|
||||
private IList<DbColumnWrapper> columns;
|
||||
|
||||
public string FilePath { get; private set; }
|
||||
|
||||
public DataSerializer(SerializeDataStartRequestParams requestParams)
|
||||
{
|
||||
this.requestParams = requestParams;
|
||||
this.columns = this.MapColumns(requestParams.Columns);
|
||||
this.FilePath = requestParams.FilePath;
|
||||
}
|
||||
|
||||
private IList<DbColumnWrapper> MapColumns(ColumnInfo[] columns)
|
||||
{
|
||||
List<DbColumnWrapper> columnWrappers = new List<DbColumnWrapper>();
|
||||
foreach (ColumnInfo column in columns)
|
||||
{
|
||||
DbColumnWrapper wrapper = new DbColumnWrapper(column);
|
||||
columnWrappers.Add(wrapper);
|
||||
}
|
||||
return columnWrappers;
|
||||
}
|
||||
|
||||
|
||||
public SerializeDataResult ProcessRequest(ISerializationParams serializeParams)
|
||||
{
|
||||
SerializeDataResult result = new SerializeDataResult();
|
||||
try
|
||||
{
|
||||
this.WriteData(serializeParams.Rows, serializeParams.IsLastBatch);
|
||||
if (serializeParams.IsLastBatch)
|
||||
{
|
||||
this.CloseStreams();
|
||||
}
|
||||
result.Succeeded = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
result.Messages = ex.Message;
|
||||
result.Succeeded = false;
|
||||
this.CloseStreams();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public void WriteData(DbCellValue[][] rows, bool isComplete)
|
||||
{
|
||||
this.EnsureWriterCreated();
|
||||
foreach (var row in rows)
|
||||
{
|
||||
SetRawObjects(row);
|
||||
writer.WriteRow(row, this.columns);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetRawObjects(DbCellValue[] row)
|
||||
{
|
||||
for (int i = 0; i < row.Length; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Try to set as the "correct" type
|
||||
var value = Convert.ChangeType(row[i].DisplayValue, columns[i].DataType);
|
||||
row[i].RawObject = value;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
row[i].RawObject = row[i].DisplayValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureWriterCreated()
|
||||
{
|
||||
if (this.writer == null)
|
||||
{
|
||||
IFileStreamFactory factory;
|
||||
switch (this.requestParams.SaveFormat.ToLowerInvariant())
|
||||
{
|
||||
case "json":
|
||||
factory = new SaveAsJsonFileStreamFactory()
|
||||
{
|
||||
SaveRequestParams = CreateJsonRequestParams()
|
||||
};
|
||||
break;
|
||||
case "csv":
|
||||
factory = new SaveAsCsvFileStreamFactory()
|
||||
{
|
||||
SaveRequestParams = CreateCsvRequestParams()
|
||||
};
|
||||
break;
|
||||
case "xml":
|
||||
factory = new SaveAsXmlFileStreamFactory()
|
||||
{
|
||||
SaveRequestParams = CreateXmlRequestParams()
|
||||
};
|
||||
break;
|
||||
case "excel":
|
||||
factory = new SaveAsExcelFileStreamFactory()
|
||||
{
|
||||
SaveRequestParams = CreateExcelRequestParams()
|
||||
};
|
||||
break;
|
||||
default:
|
||||
throw new Exception(SR.SerializationServiceUnsupportedFormat(this.requestParams.SaveFormat));
|
||||
}
|
||||
this.writer = factory.GetWriter(requestParams.FilePath);
|
||||
}
|
||||
}
|
||||
public void CloseStreams()
|
||||
{
|
||||
if (this.writer != null)
|
||||
{
|
||||
this.writer.Dispose();
|
||||
this.writer = null;
|
||||
}
|
||||
}
|
||||
|
||||
private SaveResultsAsJsonRequestParams CreateJsonRequestParams()
|
||||
{
|
||||
return new SaveResultsAsJsonRequestParams
|
||||
{
|
||||
FilePath = this.requestParams.FilePath,
|
||||
BatchIndex = 0,
|
||||
ResultSetIndex = 0
|
||||
};
|
||||
}
|
||||
private SaveResultsAsExcelRequestParams CreateExcelRequestParams()
|
||||
{
|
||||
return new SaveResultsAsExcelRequestParams
|
||||
{
|
||||
FilePath = this.requestParams.FilePath,
|
||||
BatchIndex = 0,
|
||||
ResultSetIndex = 0,
|
||||
IncludeHeaders = this.requestParams.IncludeHeaders
|
||||
};
|
||||
}
|
||||
private SaveResultsAsCsvRequestParams CreateCsvRequestParams()
|
||||
{
|
||||
return new SaveResultsAsCsvRequestParams
|
||||
{
|
||||
FilePath = this.requestParams.FilePath,
|
||||
BatchIndex = 0,
|
||||
ResultSetIndex = 0,
|
||||
IncludeHeaders = this.requestParams.IncludeHeaders,
|
||||
Delimiter = this.requestParams.Delimiter,
|
||||
LineSeperator = this.requestParams.LineSeparator,
|
||||
TextIdentifier = this.requestParams.TextIdentifier,
|
||||
Encoding = this.requestParams.Encoding
|
||||
};
|
||||
}
|
||||
private SaveResultsAsXmlRequestParams CreateXmlRequestParams()
|
||||
{
|
||||
return new SaveResultsAsXmlRequestParams
|
||||
{
|
||||
FilePath = this.requestParams.FilePath,
|
||||
BatchIndex = 0,
|
||||
ResultSetIndex = 0,
|
||||
Formatted = this.requestParams.Formatted,
|
||||
Encoding = this.requestParams.Encoding
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
using System;
|
||||
|
||||
namespace Microsoft.Kusto.ServiceLayer.QueryExecution
|
||||
{
|
||||
/// <summary>
|
||||
/// Class that represents a Special Action which occured by user request during the query
|
||||
/// </summary>
|
||||
public class SpecialAction {
|
||||
|
||||
#region Private Class variables
|
||||
|
||||
// Underlying representation as bitwise flags to simplify logic
|
||||
[Flags]
|
||||
private enum ActionFlags
|
||||
{
|
||||
None = 0,
|
||||
// All added options must be powers of 2
|
||||
ExpectYukonXmlShowPlan = 1
|
||||
}
|
||||
|
||||
private ActionFlags flags;
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// The type of XML execution plan that is contained with in a result set
|
||||
/// </summary>
|
||||
public SpecialAction()
|
||||
{
|
||||
flags = ActionFlags.None;
|
||||
}
|
||||
|
||||
#region Public Functions
|
||||
/// <summary>
|
||||
/// No Special action performed
|
||||
/// </summary>
|
||||
public bool None
|
||||
{
|
||||
get { return flags == ActionFlags.None; }
|
||||
set
|
||||
{
|
||||
flags = ActionFlags.None;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contains an XML execution plan result set
|
||||
/// </summary>
|
||||
public bool ExpectYukonXMLShowPlan
|
||||
{
|
||||
get { return flags.HasFlag(ActionFlags.ExpectYukonXmlShowPlan); }
|
||||
set
|
||||
{
|
||||
if (value)
|
||||
{
|
||||
// OR flags with value to apply
|
||||
flags |= ActionFlags.ExpectYukonXmlShowPlan;
|
||||
}
|
||||
else
|
||||
{
|
||||
// AND flags with the inverse of the value we want to remove
|
||||
flags &= ~(ActionFlags.ExpectYukonXmlShowPlan);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Aggregate this special action with the input
|
||||
/// </summary>
|
||||
public void CombineSpecialAction(SpecialAction action)
|
||||
{
|
||||
flags |= ((action?.flags) ?? ActionFlags.None);
|
||||
}
|
||||
public override string ToString() => $"ActionFlag:'{flags}', ExpectYukonXMLShowPlan:'{ExpectYukonXMLShowPlan}'";
|
||||
#endregion
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user